fix(chat): BYOK dialect defaults to Passthrough; dedicated Mistral dialect

C1 (decision): custom/BYOK ChatCompletions entries defaulted to the Kimi
dialect, leaking Kimi-specific body mutations (thinking:{…} control,
replayed reasoning_content, schema rewrites) to arbitrary third-party
OpenAI-compatible servers. The default is now Passthrough (vanilla
OpenAI semantics), with ONE exception mirroring Pi's base-url quirk
sniffing: entries pointed at the house/Kimi coding endpoint keep the
Kimi dialect. Registry platforms are unaffected (all declare
explicitly).

C2 (Pi mistral-conversations normalizer): Mistral requires tool-call ids
of EXACTLY nine [a-zA-Z0-9] chars; even same-session synthesized UUIDs
violate it. New ChatCompat::Mistral = StrictOpenAi behavior + the
normalizer — strip non-alphanumerics, keep exact-9 ids, else FNV-1a →
base36 (build-stable, deterministic across requests for prefix-cache
stability) with collision retry; ONE map covers tool_calls[].id and
tool_call_id so pairing survives. The mistral registry row and the
persisted 'mistral' serde value both resolve to it (pre-rename Mistral
sessions gain the contract automatically); Cerebras/NVIDIA stay on
StrictOpenAi untouched.

Verified: models+sampling-types+sampler+chat-state+shell all green,
clippy clean.
This commit is contained in:
2026-07-23 10:31:45 -04:00
parent d3c9380307
commit 301eb61de4
5 changed files with 247 additions and 15 deletions
@@ -1260,15 +1260,21 @@ pub enum ChatCompat {
DeepSeek,
/// Leave the body as-is (OpenAI-style `reasoning_effort` passes through).
Passthrough,
/// Strict OpenAI-compatible validators (Mistral, Cerebras) reject any
/// Strict OpenAI-compatible validators (Cerebras, NVIDIA) reject any
/// out-of-schema request field with a 4xx (`additionalProperties:false`).
/// kigi injects `stream_options.include_usage` on every streaming
/// request, which such validators reject, so it is stripped (streaming
/// usage falls back to token estimation). `reasoning_effort` passes
/// through; private message fields are stripped like Passthrough.
/// (Serde alias `mistral` keeps sessions persisted before the rename.)
#[serde(alias = "mistral")]
StrictOpenAi,
/// Mistral: [`Self::StrictOpenAi`] behavior plus its exactly-nine
/// `[a-zA-Z0-9]` tool-call id contract — foreign/OpenAI-style ids are
/// deterministically remapped on call+result in one shared map (the
/// Pi `mistral-conversations` normalizer). Serializes as `mistral`, so
/// sessions persisted before the StrictOpenAi rename (which carried
/// the `mistral` alias) resolve here — correct, they were Mistral
/// sessions.
Mistral,
}
pub const REASONING_EFFORT_META_KEY: &str = "reasoningEffort";
@@ -1731,9 +1737,15 @@ mod tests {
/// persisted before the rename still deserialize.
#[test]
fn chat_compat_mistral_alias_deserializes_to_strict_openai() {
// `mistral` resolves to the dedicated Mistral dialect — including
// sessions persisted before the StrictOpenAi rename (they were
// Mistral sessions and now get the 9-char id contract too).
let v: ChatCompat = serde_json::from_str("\"mistral\"").unwrap();
assert_eq!(v, ChatCompat::StrictOpenAi);
// New value round-trips as strict_open_ai.
assert_eq!(v, ChatCompat::Mistral);
assert_eq!(
serde_json::to_string(&ChatCompat::Mistral).unwrap(),
"\"mistral\""
);
let v: ChatCompat = serde_json::from_str("\"strict_open_ai\"").unwrap();
assert_eq!(v, ChatCompat::StrictOpenAi);
assert_eq!(