mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-10 03:30:59 +00:00
fix(llm): omit Anthropic thinking for forced tools
This commit is contained in:
parent
dc5602580b
commit
4661c0fbf4
1 changed files with 142 additions and 1 deletions
|
|
@ -389,6 +389,15 @@ fn translate_tool_choice(choice: &ToolChoice) -> Option<serde_json::Value> {
|
|||
}
|
||||
}
|
||||
|
||||
fn tool_choice_forces_tool_use(tool_choice: Option<&serde_json::Value>) -> bool {
|
||||
matches!(
|
||||
tool_choice
|
||||
.and_then(|value| value.get("type"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("any" | "tool")
|
||||
)
|
||||
}
|
||||
|
||||
// --- Structured output (response_format) helpers ---
|
||||
|
||||
const SYNTHETIC_TOOL_NAME: &str = "json_output";
|
||||
|
|
@ -1148,7 +1157,7 @@ async fn build_api_request(
|
|||
.or_else(|| model_info.and_then(|m| m.limits.max_output))
|
||||
.unwrap_or(65536);
|
||||
|
||||
let (thinking, output_config) = if let Some(effort) = &request.reasoning_effort {
|
||||
let (mut thinking, mut output_config) = if let Some(effort) = &request.reasoning_effort {
|
||||
if supports_effort {
|
||||
(
|
||||
explicit_thinking,
|
||||
|
|
@ -1182,6 +1191,11 @@ async fn build_api_request(
|
|||
(thinking, None)
|
||||
};
|
||||
|
||||
if tool_choice_forces_tool_use(tool_choice_json.as_ref()) {
|
||||
thinking = None;
|
||||
output_config = None;
|
||||
}
|
||||
|
||||
let is_fast = request.speed.as_deref() == Some("fast");
|
||||
|
||||
let api_request = ApiRequest {
|
||||
|
|
@ -1788,6 +1802,24 @@ mod tests {
|
|||
assert!(system.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_choice_forces_tool_use_detects_forced_modes() {
|
||||
assert!(tool_choice_forces_tool_use(Some(
|
||||
&serde_json::json!({"type": "any"})
|
||||
)));
|
||||
assert!(tool_choice_forces_tool_use(Some(
|
||||
&serde_json::json!({"type": "tool", "name": "json_output"})
|
||||
)));
|
||||
|
||||
assert!(!tool_choice_forces_tool_use(Some(
|
||||
&serde_json::json!({"type": "auto"})
|
||||
)));
|
||||
assert!(!tool_choice_forces_tool_use(Some(
|
||||
&serde_json::json!({"type": "none"})
|
||||
)));
|
||||
assert!(!tool_choice_forces_tool_use(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_format_json_schema_appends_to_existing_tools() {
|
||||
let schema = serde_json::json!({"type": "object"});
|
||||
|
|
@ -2193,6 +2225,115 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_api_request_uses_adaptive_thinking_for_opus_4_7_without_forced_tools() {
|
||||
let adapter = Adapter::new("test-key");
|
||||
let request = Request {
|
||||
model: "claude-opus-4-7".to_string(),
|
||||
..make_base_request()
|
||||
};
|
||||
|
||||
let (api_request, _req_builder) = build_api_request(&adapter, &request, false).await;
|
||||
assert_eq!(
|
||||
api_request.thinking,
|
||||
Some(serde_json::json!({"type": "adaptive"}))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_api_request_omits_thinking_for_opus_4_7_json_schema() {
|
||||
let adapter = Adapter::new("test-key");
|
||||
let request = Request {
|
||||
model: "claude-opus-4-7".to_string(),
|
||||
response_format: Some(ResponseFormat {
|
||||
kind: ResponseFormatType::JsonSchema,
|
||||
json_schema: Some(serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {"title": {"type": "string"}},
|
||||
"required": ["title"]
|
||||
})),
|
||||
strict: true,
|
||||
}),
|
||||
..make_base_request()
|
||||
};
|
||||
|
||||
let (api_request, _req_builder) = build_api_request(&adapter, &request, false).await;
|
||||
let tool_choice = api_request
|
||||
.tool_choice
|
||||
.as_ref()
|
||||
.expect("json schema response format should force synthetic tool");
|
||||
assert_eq!(tool_choice["type"], "tool");
|
||||
assert_eq!(tool_choice["name"], SYNTHETIC_TOOL_NAME);
|
||||
assert!(
|
||||
api_request.thinking.is_none(),
|
||||
"forced tool calls must omit thinking"
|
||||
);
|
||||
assert!(
|
||||
api_request.output_config.is_none(),
|
||||
"forced tool calls must omit output_config effort"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_api_request_omits_thinking_for_explicit_named_tool_choice() {
|
||||
let adapter = Adapter::new("test-key");
|
||||
let request = Request {
|
||||
tools: Some(vec![ToolDefinition {
|
||||
name: "json_output".to_string(),
|
||||
description: "Output JSON".to_string(),
|
||||
parameters: serde_json::json!({"type": "object"}),
|
||||
}]),
|
||||
tool_choice: Some(ToolChoice::Named {
|
||||
tool_name: "json_output".to_string(),
|
||||
}),
|
||||
provider_options: Some(serde_json::json!({
|
||||
"anthropic": {
|
||||
"thinking": {"type": "adaptive"}
|
||||
}
|
||||
})),
|
||||
..make_base_request()
|
||||
};
|
||||
|
||||
let (api_request, _req_builder) = build_api_request(&adapter, &request, false).await;
|
||||
let tool_choice = api_request
|
||||
.tool_choice
|
||||
.as_ref()
|
||||
.expect("named tool choice should be translated");
|
||||
assert_eq!(tool_choice["type"], "tool");
|
||||
assert_eq!(tool_choice["name"], "json_output");
|
||||
assert!(
|
||||
api_request.thinking.is_none(),
|
||||
"forced named tool choice must omit explicit thinking"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_api_request_omits_effort_for_required_tool_choice() {
|
||||
let adapter = Adapter::new("test-key");
|
||||
let request = Request {
|
||||
model: "claude-opus-4-7".to_string(),
|
||||
tools: Some(vec![ToolDefinition {
|
||||
name: "json_output".to_string(),
|
||||
description: "Output JSON".to_string(),
|
||||
parameters: serde_json::json!({"type": "object"}),
|
||||
}]),
|
||||
tool_choice: Some(ToolChoice::Required),
|
||||
reasoning_effort: Some(ReasoningEffort::Medium),
|
||||
..make_base_request()
|
||||
};
|
||||
|
||||
let (api_request, _req_builder) = build_api_request(&adapter, &request, false).await;
|
||||
let tool_choice = api_request
|
||||
.tool_choice
|
||||
.as_ref()
|
||||
.expect("required tool choice should be translated");
|
||||
assert_eq!(tool_choice["type"], "any");
|
||||
assert!(
|
||||
api_request.output_config.is_none(),
|
||||
"required tool choice must omit output_config effort"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn build_api_request_omits_output_config_when_no_reasoning_effort() {
|
||||
let adapter = Adapter::new("test-key");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue