Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ pub(super) fn extract_system(
}
}
"assistant" => {
let mut content_blocks: Vec<Value> = Vec::new();
let mut content_blocks: Vec<Value> = msg
.get("content")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();

let text = msg.get("content").and_then(|c| c.as_str()).unwrap_or("");
if !text.is_empty() {
Expand Down Expand Up @@ -434,17 +438,24 @@ fn finalize_wire_hygiene(messages: &mut Vec<Value>) {
fn anthropic_thinking_block(tool_call: &Value) -> Option<Value> {
let anthropic = tool_call.get("extra_content")?.get("anthropic")?;
let thinking = anthropic.get("thinking")?.as_str()?;
let signature = anthropic.get("signature")?.as_str()?;
// Fable 5.1 can sign an empty visible summary. The opaque signature,
// not the amount of display text, determines whether to replay it.
if signature.is_empty() {
let mut block = thinking_history(thinking, anthropic.get("signature").and_then(Value::as_str))?
["anthropic"]
.clone();
block["type"] = Value::String("thinking".into());
Some(block)
}

/// MiniMax returns unsigned reasoning; Claude may sign even an empty summary.
/// Preserve whichever fields the provider supplied without fabricating a signature.
pub(super) fn thinking_history(thinking: &str, signature: Option<&str>) -> Option<Value> {
if thinking.is_empty() && signature.is_none_or(str::is_empty) {
return None;
}
Some(serde_json::json!({
"type": "thinking",
"thinking": thinking,
"signature": signature,
}))
let mut block = serde_json::json!({ "thinking": thinking });
if let Some(signature) = signature {
block["signature"] = Value::String(signature.into());
}
Some(serde_json::json!({ "anthropic": block }))
}

/// Convert a single content block from OpenAI format to Anthropic format.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,22 @@ pub(super) fn prepare_request(
max_tokens,
temperature,
);
let thinking = outcome.thinking;
let effort = outcome.effort;
// Flash always thinks; it accepts adaptive thinking and five effort levels.
// Old `-none` selections retain the model's default instead of sending an
// unsupported disabled/none request. M3 keeps its protocol-native default.
let minimax_flash = client.provider_spec.name
== crate::providers::registry::provider_id::MINIMAX
&& resolved_model.eq_ignore_ascii_case("MiniMax-M3.1-Flash");
let thinking = if minimax_flash {
Some(json!({ "type": "adaptive" }))
} else {
outcome.thinking
};
let effort = if minimax_flash {
Some(crate::providers::thinking_mode::minimax_flash_effort(parsed.level).to_string())
} else {
outcome.effort
};
let mut effective_temp = outcome.temperature;
let effective_max_tokens = outcome.max_tokens;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -447,14 +447,8 @@ pub(super) fn finalize_blocks(
}
}
BlockAcc::Thinking { text, signature } => {
if let Some(sig) = signature {
pending_anthropic_thinking = Some(serde_json::json!({
"anthropic": {
"thinking": text,
"signature": sig,
}
}));
}
pending_anthropic_thinking =
super::messages::thinking_history(&text, signature.as_deref());
if !text.is_empty() {
blocks.push(AssistantBlock::Reasoning { text });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -564,14 +564,8 @@ fn build_non_streaming_response(parsed: MessagesResponse) -> LLMResponse {
signature,
} => {
if let Some(ref thought) = thinking {
if let Some(sig) = signature {
pending_anthropic_thinking = Some(serde_json::json!({
"anthropic": {
"thinking": thought,
"signature": sig,
}
}));
}
pending_anthropic_thinking =
super::messages::thinking_history(thought, signature.as_deref());
// Empty summaries still carry signed history (Fable 5.1).
if thought.is_empty() {
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ fn preferences(spec: &ProviderSpec, codex_oauth: bool) -> &'static [&'static str
"glm-4.7-flashx",
],
provider_id::DASHSCOPE => &["qwen-flash", "qwen-turbo", "qwen3.5-flash"],
provider_id::MINIMAX => &["minimax-m2.5", "minimax-m2.1", "minimax-m2"],
provider_id::MINIMAX => &["minimax-m3.1-flash", "minimax-m3"],
provider_id::LONGCAT => &["longcat-flash-chat", "longcat-flash"],
provider_id::MOONSHOT => &["kimi-k2-0905-preview", "kimi-k2.5", "kimi-k2"],
provider_id::OPENROUTER
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,21 @@ fn resolve_with_keyvault_variants(model: &str, variants: &[ModelVariant]) -> Mod

fn resolve_from_family_table(model: &str) -> ModelCapabilities {
let base_model = parse_model_variant_id(model).base_model;
// Direct IDs only: gateway routes (including :free / -free) must use
// their own reported limits instead of inheriting direct API metadata.
if base_model.eq_ignore_ascii_case("MiniMax-M3.1-Flash")
|| base_model.eq_ignore_ascii_case("MiniMax-M3")
{
return ModelCapabilities {
context_window: 1_000_000,
thinking: if base_model.eq_ignore_ascii_case("MiniMax-M3.1-Flash") {
ThinkingSupport::AlwaysOn
} else {
ThinkingSupport::Optional
},
omit_temperature_with_thinking: true,
};
}
let normalized = super::model_hints::normalize_claude_shorthand(&base_model);
let model_lower = normalize_claude_release_separators(&normalized.to_lowercase());
for rule in FAMILY_RULES {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ fn account(model_type: ModelType, model: &str) -> ModelKey {
#[test]
fn auxiliary_model_respects_account_access_and_enabled_models() {
for (family, model_type, parent, fast) in [
(
provider_id::MINIMAX,
ModelType::MinimaxApi,
"MiniMax-M3",
"MiniMax-M3.1-Flash",
),
(
provider_id::OPENAI,
ModelType::OpenaiApi,
Expand Down
Loading
Loading