blob: 71641c543664c89a8b9289f6cbdfd9782a80883c [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::Wire;
use super::{Selection, route::Route};
use crate::facts::{ProviderFacts, ThinkingOffBehavior};
use maka_agent_facts::execution::ThinkingLevel;
use maka_agent_ports::model::ProviderKind;
use maka_agent_ports::provider::Error;
use serde_json::{Value, json};
use std::borrow::Cow;
mod compatible;
mod relay;
/// Compose protocol options from the same public request as routing.
/// SDKs encode the wire; this is Maka's explicit execution policy.
pub(super) fn resolve(
row: &Selection<'_>,
facts: &ProviderFacts,
thinking_levels: &[ThinkingLevel],
thinking_level: Option<ThinkingLevel>,
route: &Route,
) -> Result<Value, Error> {
let wire = route.wire;
let parallel = row
.model
.capabilities
.and_then(|caps| caps.parallel_tool_calls);
match &route.kind {
ProviderKind::Anthropic => Ok(anthropic(
facts,
row.model,
thinking_levels,
thinking_level,
parallel,
)),
ProviderKind::OpenaiResponses if row.provider == "openai-responses-compatible" => {
relay::responses(row, thinking_level, parallel.unwrap_or(true))
}
ProviderKind::OpenaiChatCompletions | ProviderKind::OpenaiResponses => {
// An explicit account capability can opt out; it never gates rendering.
let summary = row.model.supports_reasoning_summary.unwrap_or(false);
Ok(openai(
wire,
thinking_level,
summary,
parallel.unwrap_or(true),
))
}
ProviderKind::OpenaiCompatible { name } => {
Ok(compatible::chat_completions(name, thinking_level, parallel))
}
ProviderKind::OpenResponses(contract) => {
if parallel == Some(false) {
return Err(Error::UnsupportedParallelPolicy);
}
let mut options = json!({});
if let Some(level) = thinking_level {
options["reasoningEffort"] = if level == ThinkingLevel::Off {
json!("none")
} else {
json!(level)
};
}
if contract.reasoning_replay
== maka_agent_facts::model::PlaintextReasoningReplay::PlaintextSummary
&& thinking_level != Some(ThinkingLevel::Off)
{
options["reasoningSummary"] = json!("auto");
}
Ok(json!({"openResponses":options}))
}
}
}
fn openai(wire: Wire, level: Option<ThinkingLevel>, summary: bool, parallel: bool) -> Value {
let mut options = json!({"store": false, "parallelToolCalls": parallel});
if let Some(level) = level {
options["reasoningEffort"] = match level {
ThinkingLevel::Off => json!("none"),
level => json!(level),
};
}
if wire == Wire::OpenaiResponses && summary && level != Some(ThinkingLevel::Off) {
options["reasoningSummary"] = json!("auto");
}
json!({"openai": options})
}
fn anthropic(
facts: &ProviderFacts,
model: &maka_agent_facts::configuration::ModelInfo,
levels: &[ThinkingLevel],
level: Option<ThinkingLevel>,
parallel: Option<bool>,
) -> Value {
let family = claude_family(&model.id);
let known = facts.models.get(&model.id);
let family_facts = facts.models.get(family.as_ref());
let metadata = known.map(|item| &item.metadata);
let family_metadata = family_facts.map(|item| &item.metadata);
// The SDK's off encoding and adaptive wire support remain compiled policy.
let off_behavior = metadata
.and_then(|metadata| metadata.thinking_options.as_ref())
.and_then(|thinking| thinking.off_behavior)
.or_else(|| {
family_metadata
.and_then(|metadata| metadata.thinking_options.as_ref())
.and_then(|thinking| thinking.off_behavior)
});
let bare_legacy = matches!(family.as_ref(), "claude-opus-4" | "claude-sonnet-4");
let visible = family.starts_with("claude-")
&& (model.capabilities.and_then(|caps| caps.reasoning) == Some(true)
|| levels.iter().any(|level| *level != ThinkingLevel::Off));
let mut options = json!({"cacheControl":{"type":"ephemeral"}});
if parallel == Some(false) {
options["disableParallelToolUse"] = json!(true);
}
if level == Some(ThinkingLevel::Off)
&& off_behavior == Some(ThinkingOffBehavior::AnthropicThinkingDisabled)
{
options["thinking"] = json!({"type":"disabled"});
} else if visible {
let adaptive = !bare_legacy
&& known
.or(family_facts)
.is_some_and(|item| item.anthropic_adaptive_thinking == Some(true));
options["thinking"] = if adaptive {
json!({"type":"adaptive", "display":"summarized"})
} else {
json!({"type":"enabled", "budgetTokens":1024})
};
if let Some(level) = level {
options["effort"] = json!(level);
}
} else if let Some(level) = level.filter(|level| *level != ThinkingLevel::Off) {
options["effort"] = json!(level);
}
json!({"anthropic": options})
}
fn claude_family(model: &str) -> Cow<'_, str> {
let family = model.rsplit('/').next().unwrap_or(model);
for prefix in ["claude-haiku-", "claude-opus-", "claude-sonnet-"] {
let Some((major, rest)) = family
.strip_prefix(prefix)
.and_then(|tail| tail.split_once('.'))
else {
continue;
};
let end = rest
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(rest.len());
if !major.is_empty()
&& major.bytes().all(|c| c.is_ascii_digit())
&& end > 0
&& (end == rest.len() || rest[end..].starts_with('-'))
{
return Cow::Owned(format!("{prefix}{major}-{rest}"));
}
}
Cow::Borrowed(family)
}