Add DeepSeek platform + ChatCompletions dialect system (provider 3)
The 6th registry row: id "deepseek", DEEPSEEK_API_KEY > auth.json "deepseek" scope, base https://api.deepseek.com (chat rides {base}/chat/completions per official docs) with KIGI_DEEPSEEK_BASE_URL override, enrichment-backed metadata (1M context, 384k output cap, high/max effort menu). Structural fix the cycle exposed: kigi's Kimi-specific body adaptation ran UNCONDITIONALLY on every ChatCompletions request. New ChatCompat dialect, declared per platform row and threaded through SamplerConfig, ClientDefaults, and the session-persisted SamplingConfig (serde-default Kimi keeps restored pre-field sessions and BYOK endpoints byte-identical; production persist seams copy it; subagents inherit it): - Kimi: full legacy pipeline (dispatch ≡ legacy pinned) - DeepSeek: thinking:{type, reasoning_effort} per api-docs.deepseek.com (server maps low/medium→high, xhigh→max itself; none disables; absent leaves the server default) - Passthrough: OpenAI-style reasoning_effort scalar untouched (unblocks Groq and the rest of the OpenAI-compatible list) Review-confirmed release blocker fixed: kigi replays Kimi's reasoning_content (and its private model_id) on input assistant messages — Kimi consumes these, but DeepSeek documents input reasoning_content as prefix-mode-only (historically a 400) and other providers don't know either field. The DeepSeek and Passthrough arms now strip both; Kimi's own pipeline is untouched. Pinned on both message shapes.
This commit is contained in:
@@ -593,7 +593,8 @@ mod tests {
|
||||
MOONSHOT_CN_METHOD_ID,
|
||||
MOONSHOT_AI_METHOD_ID,
|
||||
"openai",
|
||||
"anthropic"
|
||||
"anthropic",
|
||||
"deepseek"
|
||||
]
|
||||
);
|
||||
assert_eq!(default_id(&built), Some(XAI_API_KEY_METHOD_ID));
|
||||
@@ -621,7 +622,8 @@ mod tests {
|
||||
MOONSHOT_CN_METHOD_ID,
|
||||
MOONSHOT_AI_METHOD_ID,
|
||||
"openai",
|
||||
"anthropic"
|
||||
"anthropic",
|
||||
"deepseek"
|
||||
]
|
||||
);
|
||||
assert_eq!(default_id(&built), Some(CACHED_TOKEN_AUTH_METHOD_ID));
|
||||
@@ -642,7 +644,8 @@ mod tests {
|
||||
MOONSHOT_CN_METHOD_ID,
|
||||
MOONSHOT_AI_METHOD_ID,
|
||||
"openai",
|
||||
"anthropic"
|
||||
"anthropic",
|
||||
"deepseek"
|
||||
]
|
||||
);
|
||||
assert_eq!(default_id(&built), Some(CACHED_TOKEN_AUTH_METHOD_ID));
|
||||
@@ -666,7 +669,8 @@ mod tests {
|
||||
MOONSHOT_CN_METHOD_ID,
|
||||
MOONSHOT_AI_METHOD_ID,
|
||||
"openai",
|
||||
"anthropic"
|
||||
"anthropic",
|
||||
"deepseek"
|
||||
]
|
||||
);
|
||||
assert_eq!(default_id(&built), None);
|
||||
|
||||
@@ -4062,6 +4062,20 @@ pub fn sampling_config_for_model(
|
||||
&credentials.base_url,
|
||||
);
|
||||
let api_backend = info.api_backend.clone();
|
||||
// Managed platform entries speak their registry dialect; BYOK/custom
|
||||
// entries keep the historical Kimi body adaptation.
|
||||
let chat_compat = info
|
||||
.id
|
||||
.as_deref()
|
||||
.and_then(kigi_models::parse_managed_model_key)
|
||||
.map(|(platform, _)| match platform.chat_compat() {
|
||||
kigi_models::PlatformChatCompat::Kimi => kigi_sampling_types::ChatCompat::Kimi,
|
||||
kigi_models::PlatformChatCompat::DeepSeek => kigi_sampling_types::ChatCompat::DeepSeek,
|
||||
kigi_models::PlatformChatCompat::Passthrough => {
|
||||
kigi_sampling_types::ChatCompat::Passthrough
|
||||
}
|
||||
})
|
||||
.unwrap_or_default();
|
||||
SamplerConfig {
|
||||
api_key: credentials.api_key,
|
||||
model: model_name,
|
||||
@@ -4071,6 +4085,7 @@ pub fn sampling_config_for_model(
|
||||
top_p,
|
||||
api_backend,
|
||||
auth_scheme: credentials.auth_scheme,
|
||||
chat_compat,
|
||||
extra_headers,
|
||||
context_window: info.context_window.get(),
|
||||
reasoning_effort: info.reasoning_effort,
|
||||
|
||||
@@ -1022,6 +1022,107 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// DeepSeek-cycle e2e: bare OpenAI-shape listing + enrichment efforts
|
||||
/// (high/max) produce ChatCompletions entries whose sampler config
|
||||
/// speaks the DeepSeek thinking dialect.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial_test::serial]
|
||||
async fn deepseek_listing_enriches_and_maps_dialect() {
|
||||
let platform_server = wiremock::MockServer::start().await;
|
||||
wiremock::Mock::given(wiremock::matchers::method("GET"))
|
||||
.and(wiremock::matchers::path("/models"))
|
||||
.and(wiremock::matchers::header("Authorization", "Bearer sk-ds"))
|
||||
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(
|
||||
serde_json::json!({ "data": [
|
||||
{ "id": "deepseek-v4-pro", "object": "model", "owned_by": "deepseek" }
|
||||
]}),
|
||||
))
|
||||
.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!({ "deepseek": { "models": { "deepseek-v4-pro": {
|
||||
"reasoning": true,
|
||||
"reasoning_options": [
|
||||
{"type": "toggle"},
|
||||
{"type": "effort", "values": ["high", "max"]}
|
||||
],
|
||||
"limit": {"context": 1000000, "output": 384000},
|
||||
"tool_call": true
|
||||
}}}}),
|
||||
))
|
||||
.expect(1)
|
||||
.mount(&modelsdev_server)
|
||||
.await;
|
||||
let cache_dir = tempfile::tempdir().unwrap();
|
||||
let _base = kigi_test_support::EnvGuard::set(
|
||||
kigi_models::DEEPSEEK_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::DeepSeek,
|
||||
"sk-ds",
|
||||
);
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
fetch_platform_models_blocking(&endpoints, None, &keys)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("fetch must succeed");
|
||||
assert_eq!(result.models.len(), 1);
|
||||
let entry = &result.models[0];
|
||||
assert_eq!(entry.id.as_deref(), Some("deepseek/deepseek-v4-pro"));
|
||||
assert_eq!(entry.context_window.get(), 1_000_000);
|
||||
assert_eq!(entry.max_completion_tokens, Some(384_000));
|
||||
assert_eq!(
|
||||
entry.api_backend,
|
||||
crate::sampling::ApiBackend::ChatCompletions
|
||||
);
|
||||
assert_eq!(
|
||||
entry
|
||||
.reasoning_efforts
|
||||
.iter()
|
||||
.map(|o| o.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["high", "max"]
|
||||
);
|
||||
|
||||
// The managed id maps to the DeepSeek chat dialect; a BYOK entry
|
||||
// (no managed key) keeps the historical Kimi adaptation.
|
||||
let model_entry = crate::agent::config::ModelEntry::from_config_entry(entry);
|
||||
let creds = crate::agent::config::ResolvedCredentials {
|
||||
api_key: Some("sk-ds".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::DeepSeek);
|
||||
let mut byok = entry.clone();
|
||||
byok.id = Some("my-custom".into());
|
||||
let byok_entry = crate::agent::config::ModelEntry::from_config_entry(&byok);
|
||||
let creds = crate::agent::config::ResolvedCredentials {
|
||||
api_key: Some("sk-x".into()),
|
||||
base_url: byok.base_url.clone(),
|
||||
auth_type: kigi_chat_state::AuthType::ApiKey,
|
||||
auth_scheme: Default::default(),
|
||||
};
|
||||
let cfg = crate::agent::config::sampling_config_for_model(&byok_entry, creds, None);
|
||||
assert_eq!(cfg.chat_compat, kigi_sampling_types::ChatCompat::Kimi);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_env_keys_parses_strings_and_rejects_non_strings() {
|
||||
use crate::agent::config::EnvKeys;
|
||||
|
||||
@@ -889,6 +889,7 @@ async fn read_parent_sampling_config(
|
||||
top_p: cfg.top_p,
|
||||
api_backend: cfg.api_backend,
|
||||
auth_scheme,
|
||||
chat_compat: cfg.chat_compat,
|
||||
extra_headers,
|
||||
context_window: cfg.context_window.get(),
|
||||
reasoning_effort: cfg.reasoning_effort,
|
||||
|
||||
@@ -3272,6 +3272,7 @@ fn test_sampling_config(model_slug: &str) -> kigi_sampling_types::SamplingConfig
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: NonZeroU64::new(256_000).expect("non-zero context window"),
|
||||
reasoning_effort: None,
|
||||
|
||||
@@ -53,6 +53,7 @@ impl SessionActor {
|
||||
temperature: sampling_config.temperature,
|
||||
top_p: sampling_config.top_p,
|
||||
api_backend: sampling_config.api_backend.clone(),
|
||||
chat_compat: sampling_config.chat_compat,
|
||||
extra_headers: sampling_config.extra_headers.clone(),
|
||||
context_window: new_context_window,
|
||||
reasoning_effort: sampling_config.reasoning_effort,
|
||||
|
||||
@@ -260,6 +260,7 @@ impl SessionActor {
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: std::num::NonZeroU64::new(256_000).unwrap(),
|
||||
reasoning_effort: None,
|
||||
@@ -312,6 +313,7 @@ impl SessionActor {
|
||||
top_p: cfg.top_p,
|
||||
api_backend: cfg.api_backend,
|
||||
auth_scheme,
|
||||
chat_compat: cfg.chat_compat,
|
||||
extra_headers,
|
||||
context_window: cfg.context_window.get(),
|
||||
reasoning_effort: cfg.reasoning_effort,
|
||||
|
||||
@@ -346,6 +346,7 @@ pub(crate) async fn spawn_session_actor(
|
||||
temperature: sampling_config.temperature,
|
||||
top_p: sampling_config.top_p,
|
||||
api_backend: sampling_config.api_backend.clone(),
|
||||
chat_compat: sampling_config.chat_compat,
|
||||
extra_headers: sampling_config.extra_headers.clone(),
|
||||
context_window: context_window_override.unwrap_or(baseline_context_window),
|
||||
reasoning_effort: sampling_config.reasoning_effort,
|
||||
|
||||
@@ -847,6 +847,7 @@ async fn set_session_model_invalidates_byok_memo_for_same_model_id() {
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: crate::sampling::ApiBackend::ChatCompletions,
|
||||
chat_compat: Default::default(),
|
||||
auth_scheme: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: 256_000,
|
||||
|
||||
@@ -44,6 +44,7 @@ async fn persist_ack_waits_for_disk_flush_before_success() {
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
auth_scheme: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: 100_000,
|
||||
@@ -87,6 +88,7 @@ async fn persist_ack_waits_for_disk_flush_before_success() {
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: std::num::NonZeroU64::new(100_000).unwrap(),
|
||||
reasoning_effort: None,
|
||||
@@ -338,6 +340,7 @@ async fn first_turn_memory_injection_persists_to_chat_history() {
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
auth_scheme: Default::default(),
|
||||
context_window: 100_000,
|
||||
force_http1: false,
|
||||
@@ -381,6 +384,7 @@ async fn first_turn_memory_injection_persists_to_chat_history() {
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: std::num::NonZeroU64::new(100_000).unwrap(),
|
||||
reasoning_effort: None,
|
||||
@@ -466,6 +470,7 @@ async fn first_turn_memory_injection_disabled_does_not_persist_to_chat_history()
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
auth_scheme: Default::default(),
|
||||
context_window: 100_000,
|
||||
force_http1: false,
|
||||
@@ -513,6 +518,7 @@ async fn first_turn_memory_injection_disabled_does_not_persist_to_chat_history()
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: std::num::NonZeroU64::new(100_000).unwrap(),
|
||||
reasoning_effort: None,
|
||||
@@ -1732,6 +1738,7 @@ async fn cancel_propagates_to_sampler_handle_so_no_further_emission() {
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: kigi_sampler::ApiBackend::Responses,
|
||||
chat_compat: Default::default(),
|
||||
auth_scheme: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: 100_000,
|
||||
|
||||
@@ -104,6 +104,7 @@ async fn test_e2e_idle_resume_refreshes_model_metadata() {
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: std::num::NonZeroU64::new(200_000).unwrap(),
|
||||
reasoning_effort: None,
|
||||
|
||||
+3
@@ -52,6 +52,7 @@ async fn create_test_actor(
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: std::num::NonZeroU64::new(context_window)
|
||||
.expect("test context_window must be non-zero"),
|
||||
@@ -486,6 +487,7 @@ async fn create_test_actor_with_memory(
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: std::num::NonZeroU64::new(context_window)
|
||||
.expect("test context_window must be non-zero"),
|
||||
@@ -1240,6 +1242,7 @@ async fn test_e2e_idle_resume_refreshes_model_metadata() {
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: std::num::NonZeroU64::new(200_000).unwrap(),
|
||||
reasoning_effort: None,
|
||||
|
||||
@@ -104,6 +104,7 @@ async fn create_test_actor_with_memory(
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: std::num::NonZeroU64::new(context_window)
|
||||
.expect("test context_window must be non-zero"),
|
||||
|
||||
@@ -168,6 +168,7 @@ pub(crate) async fn create_test_actor_ex(
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: std::num::NonZeroU64::new(context_window)
|
||||
.expect("test context_window must be non-zero"),
|
||||
|
||||
@@ -2149,6 +2149,7 @@ mod inline_auto_compact_flow_tests {
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: std::num::NonZeroU64::new(context_window)
|
||||
.expect("test context_window must be non-zero"),
|
||||
|
||||
@@ -1588,6 +1588,7 @@ mod reasoning_compaction_regression_tests {
|
||||
temperature: Some(0.7),
|
||||
top_p: None,
|
||||
api_backend: ApiBackend::ChatCompletions,
|
||||
chat_compat: Default::default(),
|
||||
auth_scheme: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: 256_000,
|
||||
|
||||
@@ -44,6 +44,7 @@ pub(crate) fn ctx_with_toggle(toggle: HashMap<String, bool>) -> SubagentSpawnCon
|
||||
temperature: None,
|
||||
top_p: None,
|
||||
api_backend: Default::default(),
|
||||
chat_compat: Default::default(),
|
||||
auth_scheme: Default::default(),
|
||||
extra_headers: Default::default(),
|
||||
context_window: 256_000,
|
||||
|
||||
Reference in New Issue
Block a user