feat(providers): add Qwen Token Plan (global + China)
Providers 14-15 (17th & 18th registry variants), Alibaba DashScope
compatible-mode. Global: token-plan.ap-southeast-1.maas.aliyuncs.com,
QWEN_TOKEN_PLAN_API_KEY, models.dev alibaba-token-plan. China:
token-plan.cn-beijing.maas.aliyuncs.com, QWEN_TOKEN_PLAN_CN_API_KEY,
alibaba-token-plan-cn. Both Bearer + OpenAI listing + ChatCompletions +
Passthrough (stream_options.include_usage is documented-supported).
/models is auth-gated (401 without a key) so it doubles as the validator;
metadata from models.dev enrichment. The token plan is a multi-vendor
catalog (deepseek/kimi/minimax/glm/qwen); restrict_to_enriched keeps the 15
tool-calling chat models and drops the 4 qwen-image/wan image generators.
Review downgraded two flagged concerns: the enable_thinking non-streaming
400 cannot occur (Kigi never issues non-streaming ChatCompletions in
production — all inference streams), and Qwen thinking is NOT invisible
(reasoning_content is parsed regardless of the Passthrough dialect). No
defects. Residual (logged): restrict does an exact id-match of live /models
ids vs the models.dev keys; a mismatch fails safe (0 models) — verify with a
real key. Snapshot already bundles both providers (regenerated in ebf1105).
Tests: global e2e proves enrichment-supplied context (wire carries none),
non-vacuous tool_call restriction (qwen-image dropped), bare-id round-trip,
Passthrough; both variants' validation tests hit /models (401 reject) and
assert the correct per-variant console host. Registry at 18; picker 19 rows.
This commit is contained in:
@@ -2084,6 +2084,97 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Qwen-Token-Plan e2e: DashScope compatible-mode /models is auth-gated and
|
||||
/// minimal (ids only), so enrichment supplies context; the restriction drops
|
||||
/// the non-tool-calling qwen-image / wan generators the token plan lists;
|
||||
/// bare id round-trips under the platform key; Passthrough dialect.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial_test::serial]
|
||||
async fn qwen_token_plan_enriches_restricts_and_maps_passthrough() {
|
||||
let platform_server = wiremock::MockServer::start().await;
|
||||
wiremock::Mock::given(wiremock::matchers::method("GET"))
|
||||
.and(wiremock::matchers::path("/models"))
|
||||
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(
|
||||
// DashScope /models is minimal: id/object only, NO context.
|
||||
serde_json::json!({ "data": [
|
||||
{ "id": "qwen3.7-max", "object": "model" },
|
||||
{ "id": "qwen-image-2.0", "object": "model" }
|
||||
]}),
|
||||
))
|
||||
.expect(1)
|
||||
.mount(&platform_server)
|
||||
.await;
|
||||
let modelsdev_server = wiremock::MockServer::start().await;
|
||||
wiremock::Mock::given(wiremock::matchers::method("GET"))
|
||||
.and(wiremock::matchers::path("/api.json"))
|
||||
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(
|
||||
serde_json::json!({ "alibaba-token-plan": { "models": {
|
||||
"qwen3.7-max": {
|
||||
"limit": {"context": 1000000, "output": 32768},
|
||||
"tool_call": true
|
||||
},
|
||||
// present in enrichment too, but not tool-calling → dropped.
|
||||
"qwen-image-2.0": { "limit": {"context": 8192} }
|
||||
}}}),
|
||||
))
|
||||
.expect(1)
|
||||
.mount(&modelsdev_server)
|
||||
.await;
|
||||
let cache_dir = tempfile::tempdir().unwrap();
|
||||
let _base = kigi_test_support::EnvGuard::set(
|
||||
kigi_models::QWEN_TOKEN_PLAN_BASE_URL_ENV,
|
||||
platform_server.uri(),
|
||||
);
|
||||
let _mdev = kigi_test_support::EnvGuard::set(
|
||||
crate::agent::enrichment_fetch::MODELS_DEV_URL_ENV,
|
||||
format!("{}/api.json", modelsdev_server.uri()),
|
||||
);
|
||||
let _mdev_cache = kigi_test_support::EnvGuard::set(
|
||||
crate::agent::enrichment_fetch::MODELS_DEV_CACHE_DIR_ENV,
|
||||
cache_dir.path(),
|
||||
);
|
||||
let endpoints = crate::agent::config::EndpointsConfig::default();
|
||||
let keys = crate::agent::models::PlatformApiKeys::test_single(
|
||||
kigi_models::PlatformId::QwenTokenPlan,
|
||||
"qtp-1",
|
||||
);
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
fetch_platform_models_blocking(&endpoints, None, &keys)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("fetch must succeed");
|
||||
assert_eq!(
|
||||
result
|
||||
.models
|
||||
.iter()
|
||||
.map(|m| m.id.as_deref().unwrap_or_default())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["qwen-token-plan/qwen3.7-max"],
|
||||
"qwen-image (not tool-calling) dropped; bare id kept under the platform key"
|
||||
);
|
||||
let entry = &result.models[0];
|
||||
assert_eq!(
|
||||
entry.context_window.get(),
|
||||
1_000_000,
|
||||
"context comes from enrichment (the wire listing carries none)"
|
||||
);
|
||||
assert_eq!(entry.max_completion_tokens, Some(32_768));
|
||||
assert_eq!(entry.model, "qwen3.7-max", "the bare id rides the wire");
|
||||
let model_entry = crate::agent::config::ModelEntry::from_config_entry(entry);
|
||||
let creds = crate::agent::config::ResolvedCredentials {
|
||||
api_key: Some("qtp-1".into()),
|
||||
base_url: entry.base_url.clone(),
|
||||
auth_type: kigi_chat_state::AuthType::ApiKey,
|
||||
auth_scheme: Default::default(),
|
||||
};
|
||||
let cfg = crate::agent::config::sampling_config_for_model(&model_entry, creds, None);
|
||||
assert_eq!(
|
||||
cfg.chat_compat,
|
||||
kigi_sampling_types::ChatCompat::Passthrough
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_env_keys_parses_strings_and_rejects_non_strings() {
|
||||
use crate::agent::config::EnvKeys;
|
||||
|
||||
Reference in New Issue
Block a user