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:
@@ -29,6 +29,73 @@ pub(crate) fn adapt_chat_completions_body(body: &mut Value) {
|
||||
adapt_tool_schemas(body);
|
||||
}
|
||||
|
||||
/// Dialect-dispatched body adaptation. Kimi keeps the full historical
|
||||
/// pipeline (thinking + message hygiene + schema normalization — all built
|
||||
/// for the Kimi wire's strictness); DeepSeek differs ONLY in how thinking
|
||||
/// rides the body; Passthrough providers take OpenAI-style bodies verbatim
|
||||
/// (their `reasoning_effort` scalar is already the wire form).
|
||||
pub(crate) fn adapt_chat_completions_body_for(
|
||||
compat: kigi_sampling_types::ChatCompat,
|
||||
body: &mut Value,
|
||||
) {
|
||||
match compat {
|
||||
kigi_sampling_types::ChatCompat::Kimi => adapt_chat_completions_body(body),
|
||||
kigi_sampling_types::ChatCompat::DeepSeek => {
|
||||
adapt_thinking_deepseek(body);
|
||||
strip_kigi_private_message_fields(body);
|
||||
}
|
||||
kigi_sampling_types::ChatCompat::Passthrough => {
|
||||
strip_kigi_private_message_fields(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove kigi-internal history artifacts from input messages before they
|
||||
/// reach a non-Kimi wire. `reasoning_content` is Kimi's replayed-thinking
|
||||
/// field (Kimi consumes it; DeepSeek documents it as prefix-mode-only and
|
||||
/// historically 400s on it; other providers don't know it) and `model_id`
|
||||
/// is kigi's private per-message provenance. Kimi's own pipeline handles
|
||||
/// these in `adapt_messages`.
|
||||
fn strip_kigi_private_message_fields(body: &mut Value) {
|
||||
let Some(messages) = body.get_mut("messages").and_then(|m| m.as_array_mut()) else {
|
||||
return;
|
||||
};
|
||||
for message in messages {
|
||||
if let Some(obj) = message.as_object_mut() {
|
||||
obj.remove("reasoning_content");
|
||||
obj.remove("model_id");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// DeepSeek spells the thinking control `thinking:{type, reasoning_effort}`
|
||||
/// (api-docs.deepseek.com create-chat-completion; the server maps
|
||||
/// low/medium→high and xhigh→max itself, so the canonical level passes
|
||||
/// through verbatim). `none` disables thinking; absent leaves the server
|
||||
/// default (enabled).
|
||||
fn adapt_thinking_deepseek(body: &mut Value) {
|
||||
let Some(obj) = body.as_object_mut() else {
|
||||
return;
|
||||
};
|
||||
let Some(effort) = obj.remove("reasoning_effort") else {
|
||||
return;
|
||||
};
|
||||
let Some(level) = effort.as_str().map(str::to_owned) else {
|
||||
return;
|
||||
};
|
||||
if level == "none" {
|
||||
obj.insert(
|
||||
"thinking".to_string(),
|
||||
serde_json::json!({ "type": "disabled" }),
|
||||
);
|
||||
} else {
|
||||
obj.insert(
|
||||
"thinking".to_string(),
|
||||
serde_json::json!({ "type": "enabled", "reasoning_effort": level }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Map the OpenAI-style `reasoning_effort` knob onto Kimi's `thinking`
|
||||
/// request field and drop `reasoning_effort` from the wire.
|
||||
///
|
||||
@@ -280,6 +347,91 @@ mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn deepseek_dialect_spells_thinking_reasoning_effort() {
|
||||
use kigi_sampling_types::ChatCompat;
|
||||
// Official docs: thinking:{type, reasoning_effort}; server maps
|
||||
// low/medium→high, xhigh→max itself — levels pass through verbatim.
|
||||
let mut body = json!({ "model": "deepseek-v4-pro", "reasoning_effort": "high" });
|
||||
adapt_chat_completions_body_for(ChatCompat::DeepSeek, &mut body);
|
||||
assert_eq!(body.get("reasoning_effort"), None);
|
||||
assert_eq!(
|
||||
body["thinking"],
|
||||
json!({ "type": "enabled", "reasoning_effort": "high" })
|
||||
);
|
||||
|
||||
let mut body = json!({ "model": "deepseek-v4-flash", "reasoning_effort": "max" });
|
||||
adapt_chat_completions_body_for(ChatCompat::DeepSeek, &mut body);
|
||||
assert_eq!(
|
||||
body["thinking"],
|
||||
json!({ "type": "enabled", "reasoning_effort": "max" })
|
||||
);
|
||||
|
||||
// none disables; absent leaves the server default (no thinking key).
|
||||
let mut body = json!({ "reasoning_effort": "none" });
|
||||
adapt_chat_completions_body_for(ChatCompat::DeepSeek, &mut body);
|
||||
assert_eq!(body["thinking"], json!({ "type": "disabled" }));
|
||||
let mut body = json!({ "model": "deepseek-chat" });
|
||||
adapt_chat_completions_body_for(ChatCompat::DeepSeek, &mut body);
|
||||
assert_eq!(body.get("thinking"), None);
|
||||
|
||||
// DeepSeek does NOT get kimi's message/tool-schema rewrites (empty
|
||||
// assistant tool-call content survives — DeepSeek's documented
|
||||
// function-calling round-trip uses that shape), but kigi-private
|
||||
// fields are stripped: replayed reasoning_content is prefix-mode-only
|
||||
// on the DeepSeek wire (historically a 400 in input messages).
|
||||
let mut body = json!({
|
||||
"reasoning_effort": "high",
|
||||
"messages": [
|
||||
{ "role": "assistant", "content": "", "tool_calls": [{}],
|
||||
"reasoning_content": "replayed thinking", "model_id": "kigi/x" }
|
||||
]
|
||||
});
|
||||
adapt_chat_completions_body_for(ChatCompat::DeepSeek, &mut body);
|
||||
assert_eq!(body["messages"][0]["content"], json!(""));
|
||||
assert_eq!(body["messages"][0].get("reasoning_content"), None);
|
||||
assert_eq!(body["messages"][0].get("model_id"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passthrough_dialect_leaves_openai_body_verbatim() {
|
||||
use kigi_sampling_types::ChatCompat;
|
||||
// Verbatim EXCEPT kigi-private history artifacts, which no non-Kimi
|
||||
// wire understands.
|
||||
let mut body = json!({
|
||||
"model": "gpt-oss",
|
||||
"reasoning_effort": "high",
|
||||
"messages": [
|
||||
{ "role": "user", "content": "hi" },
|
||||
{ "role": "assistant", "content": "yo",
|
||||
"reasoning_content": "internal", "model_id": "kigi/x" }
|
||||
]
|
||||
});
|
||||
adapt_chat_completions_body_for(ChatCompat::Passthrough, &mut body);
|
||||
assert_eq!(
|
||||
body,
|
||||
json!({
|
||||
"model": "gpt-oss",
|
||||
"reasoning_effort": "high",
|
||||
"messages": [
|
||||
{ "role": "user", "content": "hi" },
|
||||
{ "role": "assistant", "content": "yo" }
|
||||
]
|
||||
}),
|
||||
"reasoning_effort stays OpenAI-style; private fields are stripped"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kimi_dialect_dispatch_matches_legacy_pipeline() {
|
||||
use kigi_sampling_types::ChatCompat;
|
||||
let mut via_dispatch = json!({ "model": "k3", "reasoning_effort": "max" });
|
||||
adapt_chat_completions_body_for(ChatCompat::Kimi, &mut via_dispatch);
|
||||
let mut via_legacy = json!({ "model": "k3", "reasoning_effort": "max" });
|
||||
adapt_chat_completions_body(&mut via_legacy);
|
||||
assert_eq!(via_dispatch, via_legacy, "Kimi dispatch = legacy pipeline");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reasoning_effort_maps_to_kimi_thinking_field() {
|
||||
// Level rides along as thinking.effort (live wire: 200 with
|
||||
|
||||
Reference in New Issue
Block a user