Add Mistral platform + Mistral dialect + array-content handling (provider 5)

The 8th registry row: id "mistral", MISTRAL_API_KEY > auth.json
"mistral" scope, https://api.mistral.ai/v1 with KIGI_MISTRAL_BASE_URL
override, enrichment-backed metadata with the tool-calling listing
restriction (embed/moderation/OCR noise).

Mistral is NOT a pure-pattern provider — an adversarial review found two
doc-confirmed blockers that no test exercises (no e2e covers a chat POST),
so the gate-green registry row alone would have shipped it DOA. A research
workflow pinned the exact wire shapes against the mistralai/client-python
SDK source (adversarially verified), then both were fixed:

1. stream_options 422: Mistral's strict Pydantic validator rejects the
   stream_options.include_usage field kigi injects on every streaming
   request (the SDK's request model has no such field). New
   ChatCompat::Mistral dialect strips it (plus the kigi-private message
   fields, like Passthrough). Streaming usage falls back to token
   estimation.
2. Reasoning content arrays: Mistral reasoning models return content as
   Union[str, List[ContentChunk]] on both streaming and non-streaming,
   which the flat Option<String> path could not decode -> aborted turn.
   A UNIVERSAL lenient deserializer (#[serde(from = "Raw..")] on
   ChatResponseMessage + ChatChunkDelta) accepts string-or-array, routing
   {type:text} chunks to the answer and the nested text of {type:thinking}
   chunks to reasoning_content, tolerant of the OPEN chunk union (unknown
   types ignored, never fatal). String content stays byte-identical for
   every other provider (kimi/deepseek/groq/BYOK).

Review refuted all seven attack lines (no regression, no crash, exhaustive)
and flagged one coverage gap, now closed: a stream-consumer integration
test drives a full thinking -> transition -> answer chunk sequence and
proves it yields the same reasoning-sibling + assistant-answer result as
the reasoning_content string path.

Also folds a verified quirk matrix for all 23 remaining API providers into
providers-plan.md, tiered by real difficulty (self-enriching OpenRouter/
Vercel; bare-array Together listing; Messages-dialect MiniMax reusing the
Anthropic machinery; non-Bearer Azure/Bedrock; router wildcards; the OAuth
block).
This commit is contained in:
2026-07-21 10:36:33 -04:00
parent 49e6414c29
commit 9953a26b8d
8 changed files with 491 additions and 14 deletions
@@ -47,6 +47,22 @@ pub(crate) fn adapt_chat_completions_body_for(
kigi_sampling_types::ChatCompat::Passthrough => {
strip_kigi_private_message_fields(body);
}
kigi_sampling_types::ChatCompat::Mistral => {
strip_kigi_private_message_fields(body);
strip_stream_options(body);
}
}
}
/// Mistral's strict Pydantic validator 422-rejects `stream_options`
/// (`extra_forbidden` on `stream_options.include_usage`; its request model
/// has no such field). kigi injects `stream_options.include_usage` on every
/// streaming request for the other providers, so strip the whole object for
/// Mistral. Streaming usage falls back to token estimation (as for any
/// provider that omits streaming usage).
fn strip_stream_options(body: &mut Value) {
if let Some(obj) = body.as_object_mut() {
obj.remove("stream_options");
}
}
@@ -393,6 +409,38 @@ mod tests {
assert_eq!(body["messages"][0].get("model_id"), None);
}
#[test]
fn mistral_dialect_strips_stream_options_and_private_fields() {
use kigi_sampling_types::ChatCompat;
// Mistral 422s on stream_options (extra_forbidden) and doesn't know
// kigi's private message fields; OpenAI-style reasoning_effort stays.
let mut body = json!({
"model": "mistral-medium-latest",
"reasoning_effort": "high",
"stream": true,
"stream_options": { "include_usage": true },
"messages": [
{ "role": "assistant", "content": "hi",
"reasoning_content": "internal", "model_id": "kigi/x" }
]
});
adapt_chat_completions_body_for(ChatCompat::Mistral, &mut body);
assert_eq!(
body.get("stream_options"),
None,
"stream_options must be stripped"
);
assert_eq!(body["stream"], json!(true), "stream flag stays");
assert_eq!(
body["reasoning_effort"],
json!("high"),
"OpenAI-style effort passes through (Mistral accepts it natively)"
);
assert_eq!(body["messages"][0].get("reasoning_content"), None);
assert_eq!(body["messages"][0].get("model_id"), None);
assert_eq!(body["messages"][0]["content"], json!("hi"));
}
#[test]
fn passthrough_dialect_leaves_openai_body_verbatim() {
use kigi_sampling_types::ChatCompat;