language_model: Add OpenAI Responses API custom tool support (#60638)
# Objective
Allow zed's language model stack to express OpenAI Responses API custom
tools (freeform text-input tools with an optional lark/regex grammar),
so downstream consumers can offer tools like a freeform `apply_patch` to
GPT models.
## Solution
- `LanguageModelRequestTool` now carries a Function-vs-Custom input
variant; `LanguageModelCustomToolFormat` models text/grammar formats.
- `LanguageModelToolUse.input` becomes a typed
`LanguageModelToolUseInput::{Json, Text}`. Serialization is tagged so
persisted Text inputs round-trip losslessly; legacy plain JSON values
still deserialize as `Json`.
- `open_ai` gains the custom tool wire types (tool definition,
`custom_tool_call`/`custom_tool_call_output` input items with
string-or-content-part outputs, output item, and
`custom_tool_call_input` delta/done stream events). The Responses event
mapper accumulates raw text deltas into `ToolUse` events, and history
replay derives custom-vs-function tool results from the matching
`ToolUse` by id.
- All non-OpenAI providers and the Chat Completions path error
explicitly when a request contains custom tools — no silent drops or
empty-schema coercions.
Release Notes:
- N/A
This commit is contained in:
1 parent
55b776183d
commit
3e976e89bc
31 files changed
+1652
-501
No files matched your search
@@ -281,7 +281,9 @@ impl DbThread {
|
||||
name: tool_use.name.into(),
|
||||
raw_input: serde_json::to_string(&tool_use.input)
|
||||
.unwrap_or_default(),
|
||||
input: tool_use.input,
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
tool_use.input,
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
|
||||
@@ -689,7 +689,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
|
||||
id: "tool_1".into(),
|
||||
name: EchoTool::NAME.into(),
|
||||
raw_input: json!({"text": "test"}).to_string(),
|
||||
input: json!({"text": "test"}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "test"})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -840,17 +840,25 @@ async fn test_streaming_tool_calls(cx: &mut TestAppContext) {
|
||||
assert_eq!(last_tool_use.name.as_ref(), "word_list");
|
||||
if tool_call.status == acp::ToolCallStatus::Pending {
|
||||
if !last_tool_use.is_input_complete
|
||||
&& last_tool_use.input.get("g").is_none()
|
||||
&& last_tool_use
|
||||
.input
|
||||
.as_json()
|
||||
.and_then(|input| input.get("g"))
|
||||
.is_none()
|
||||
{
|
||||
saw_partial_tool_use = true;
|
||||
}
|
||||
} else {
|
||||
last_tool_use
|
||||
.input
|
||||
.as_json()
|
||||
.expect("tool input should be JSON")
|
||||
.get("a")
|
||||
.expect("'a' has streamed because input is now complete");
|
||||
last_tool_use
|
||||
.input
|
||||
.as_json()
|
||||
.expect("tool input should be JSON")
|
||||
.get("g")
|
||||
.expect("'g' has streamed because input is now complete");
|
||||
}
|
||||
@@ -884,7 +892,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||
id: "tool_id_1".into(),
|
||||
name: ToolRequiringPermission::NAME.into(),
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -894,7 +902,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||
id: "tool_id_2".into(),
|
||||
name: ToolRequiringPermission::NAME.into(),
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -951,7 +959,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||
id: "tool_id_3".into(),
|
||||
name: ToolRequiringPermission::NAME.into(),
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -990,7 +998,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
|
||||
id: "tool_id_4".into(),
|
||||
name: ToolRequiringPermission::NAME.into(),
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -1029,7 +1037,7 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) {
|
||||
id: "tool_id_1".into(),
|
||||
name: "nonexistent_tool".into(),
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -1533,7 +1541,7 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
|
||||
id: "tool_1".into(),
|
||||
name: "echo".into(),
|
||||
raw_input: json!({"text": "test"}).to_string(),
|
||||
input: json!({"text": "test"}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "test"})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -1577,7 +1585,7 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
|
||||
id: "tool_2".into(),
|
||||
name: "test_server_echo".into(),
|
||||
raw_input: json!({"text": "mcp"}).to_string(),
|
||||
input: json!({"text": "mcp"}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "mcp"})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -1587,7 +1595,7 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
|
||||
id: "tool_3".into(),
|
||||
name: "echo".into(),
|
||||
raw_input: json!({"text": "native"}).to_string(),
|
||||
input: json!({"text": "native"}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "native"})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -1697,7 +1705,7 @@ async fn test_mcp_tool_names_are_sanitized_for_providers(cx: &mut TestAppContext
|
||||
id: "tool_1".into(),
|
||||
name: "snake_case_PascalCase".into(),
|
||||
raw_input: json!({}).to_string(),
|
||||
input: json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -1786,7 +1794,7 @@ async fn test_mcp_tool_multi_content_response(cx: &mut TestAppContext) {
|
||||
id: "tool_1".into(),
|
||||
name: "screenshot".into(),
|
||||
raw_input: json!({}).to_string(),
|
||||
input: json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -1936,7 +1944,9 @@ async fn test_mcp_tool_result_displayed_when_server_disconnected(cx: &mut TestAp
|
||||
name: "issue_read".into(),
|
||||
raw_input: json!({"issue_url": "https://github.com/zed-industries/zed/issues/47404"})
|
||||
.to_string(),
|
||||
input: json!({"issue_url": "https://github.com/zed-industries/zed/issues/47404"}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
json!({"issue_url": "https://github.com/zed-industries/zed/issues/47404"}),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -2351,7 +2361,9 @@ async fn test_terminal_tool_cancellation_captures_output(cx: &mut TestAppContext
|
||||
id: "terminal_tool_1".into(),
|
||||
name: TerminalTool::NAME.into(),
|
||||
raw_input: r#"{"command": "sleep 1000", "cd": "."}"#.into(),
|
||||
input: json!({"command": "sleep 1000", "cd": "."}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
json!({"command": "sleep 1000", "cd": "."}),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -2446,7 +2458,7 @@ async fn test_cancellation_aware_tool_responds_to_cancellation(cx: &mut TestAppC
|
||||
id: "cancellation_aware_1".into(),
|
||||
name: "cancellation_aware".into(),
|
||||
raw_input: r#"{}"#.into(),
|
||||
input: json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -2632,7 +2644,9 @@ async fn test_truncate_while_terminal_tool_running(cx: &mut TestAppContext) {
|
||||
id: "terminal_tool_1".into(),
|
||||
name: TerminalTool::NAME.into(),
|
||||
raw_input: r#"{"command": "sleep 1000", "cd": "."}"#.into(),
|
||||
input: json!({"command": "sleep 1000", "cd": "."}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
json!({"command": "sleep 1000", "cd": "."}),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -2697,7 +2711,9 @@ async fn test_cancel_multiple_concurrent_terminal_tools(cx: &mut TestAppContext)
|
||||
id: "terminal_tool_1".into(),
|
||||
name: TerminalTool::NAME.into(),
|
||||
raw_input: r#"{"command": "sleep 1000", "cd": "."}"#.into(),
|
||||
input: json!({"command": "sleep 1000", "cd": "."}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
json!({"command": "sleep 1000", "cd": "."}),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -2707,7 +2723,9 @@ async fn test_cancel_multiple_concurrent_terminal_tools(cx: &mut TestAppContext)
|
||||
id: "terminal_tool_2".into(),
|
||||
name: TerminalTool::NAME.into(),
|
||||
raw_input: r#"{"command": "sleep 2000", "cd": "."}"#.into(),
|
||||
input: json!({"command": "sleep 2000", "cd": "."}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
json!({"command": "sleep 2000", "cd": "."}),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -2811,7 +2829,9 @@ async fn test_terminal_tool_stopped_via_terminal_card_button(cx: &mut TestAppCon
|
||||
id: "terminal_tool_1".into(),
|
||||
name: TerminalTool::NAME.into(),
|
||||
raw_input: r#"{"command": "sleep 1000", "cd": "."}"#.into(),
|
||||
input: json!({"command": "sleep 1000", "cd": "."}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
json!({"command": "sleep 1000", "cd": "."}),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -2907,7 +2927,9 @@ async fn test_terminal_tool_timeout_expires(cx: &mut TestAppContext) {
|
||||
id: "terminal_tool_1".into(),
|
||||
name: TerminalTool::NAME.into(),
|
||||
raw_input: r#"{"command": "sleep 1000", "cd": ".", "timeout_ms": 100}"#.into(),
|
||||
input: json!({"command": "sleep 1000", "cd": ".", "timeout_ms": 100}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
json!({"command": "sleep 1000", "cd": ".", "timeout_ms": 100}),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -3376,7 +3398,7 @@ async fn test_cumulative_token_usage(cx: &mut TestAppContext) {
|
||||
id: "tool_1".into(),
|
||||
name: EchoTool::NAME.into(),
|
||||
raw_input: json!({"text": "hello"}).to_string(),
|
||||
input: json!({"text": "hello"}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "hello"})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -3786,7 +3808,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
|
||||
id: "tool_id_1".into(),
|
||||
name: ToolRequiringPermission::NAME.into(),
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -3794,7 +3816,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
|
||||
id: "tool_id_2".into(),
|
||||
name: EchoTool::NAME.into(),
|
||||
raw_input: json!({"text": "test"}).to_string(),
|
||||
input: json!({"text": "test"}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "test"})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -3992,7 +4014,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
id: "1".into(),
|
||||
name: EchoTool::NAME.into(),
|
||||
raw_input: input.to_string(),
|
||||
input,
|
||||
input: language_model::LanguageModelToolUseInput::Json(input),
|
||||
is_input_complete: false,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -4005,7 +4027,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
id: "1".into(),
|
||||
name: "echo".into(),
|
||||
raw_input: input.to_string(),
|
||||
input,
|
||||
input: language_model::LanguageModelToolUseInput::Json(input),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -4175,7 +4197,7 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
|
||||
id: "tool_1".into(),
|
||||
name: EchoTool::NAME.into(),
|
||||
raw_input: json!({"text": "test"}).to_string(),
|
||||
input: json!({"text": "test"}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "test"})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -4323,7 +4345,7 @@ async fn test_streaming_tool_completes_when_llm_stream_ends_without_final_input(
|
||||
id: "tool_1".into(),
|
||||
name: "streaming_echo".into(),
|
||||
raw_input: r#"{"text": "partial"}"#.into(),
|
||||
input: json!({"text": "partial"}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "partial"})),
|
||||
is_input_complete: false,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -4428,7 +4450,7 @@ async fn test_streaming_tool_json_parse_error_is_forwarded_to_running_tool(
|
||||
id: "tool_1".into(),
|
||||
name: StreamingJsonErrorContextTool::NAME.into(),
|
||||
raw_input: r#"{"text": "partial"#.into(),
|
||||
input: json!({"text": "partial"}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "partial"})),
|
||||
is_input_complete: false,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -5228,7 +5250,9 @@ async fn test_subagent_tool_call_end_to_end(cx: &mut TestAppContext) {
|
||||
id: "subagent_1".into(),
|
||||
name: SpawnAgentTool::NAME.into(),
|
||||
raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
|
||||
input: serde_json::to_value(&subagent_tool_input).unwrap(),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
serde_json::to_value(&subagent_tool_input).unwrap(),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -5362,7 +5386,9 @@ async fn test_subagent_tool_output_does_not_include_thinking(cx: &mut TestAppCon
|
||||
id: "subagent_1".into(),
|
||||
name: SpawnAgentTool::NAME.into(),
|
||||
raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
|
||||
input: serde_json::to_value(&subagent_tool_input).unwrap(),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
serde_json::to_value(&subagent_tool_input).unwrap(),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -5509,7 +5535,9 @@ async fn test_subagent_tool_call_cancellation_during_task_prompt(cx: &mut TestAp
|
||||
id: "subagent_1".into(),
|
||||
name: SpawnAgentTool::NAME.into(),
|
||||
raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
|
||||
input: serde_json::to_value(&subagent_tool_input).unwrap(),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
serde_json::to_value(&subagent_tool_input).unwrap(),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -5638,7 +5666,9 @@ async fn test_subagent_tool_resume_session(cx: &mut TestAppContext) {
|
||||
id: "subagent_1".into(),
|
||||
name: SpawnAgentTool::NAME.into(),
|
||||
raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
|
||||
input: serde_json::to_value(&subagent_tool_input).unwrap(),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
serde_json::to_value(&subagent_tool_input).unwrap(),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -5699,7 +5729,9 @@ async fn test_subagent_tool_resume_session(cx: &mut TestAppContext) {
|
||||
id: "subagent_2".into(),
|
||||
name: SpawnAgentTool::NAME.into(),
|
||||
raw_input: serde_json::to_string(&resume_tool_input).unwrap(),
|
||||
input: serde_json::to_value(&resume_tool_input).unwrap(),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
serde_json::to_value(&resume_tool_input).unwrap(),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -6285,7 +6317,9 @@ async fn test_subagent_context_window_warning(cx: &mut TestAppContext) {
|
||||
id: "subagent_1".into(),
|
||||
name: SpawnAgentTool::NAME.into(),
|
||||
raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
|
||||
input: serde_json::to_value(&subagent_tool_input).unwrap(),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
serde_json::to_value(&subagent_tool_input).unwrap(),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -6410,7 +6444,9 @@ async fn test_subagent_no_context_window_warning_when_already_at_warning(cx: &mu
|
||||
id: "subagent_1".into(),
|
||||
name: SpawnAgentTool::NAME.into(),
|
||||
raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
|
||||
input: serde_json::to_value(&subagent_tool_input).unwrap(),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
serde_json::to_value(&subagent_tool_input).unwrap(),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -6476,7 +6512,9 @@ async fn test_subagent_no_context_window_warning_when_already_at_warning(cx: &mu
|
||||
id: "subagent_2".into(),
|
||||
name: SpawnAgentTool::NAME.into(),
|
||||
raw_input: serde_json::to_string(&resume_tool_input).unwrap(),
|
||||
input: serde_json::to_value(&resume_tool_input).unwrap(),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
serde_json::to_value(&resume_tool_input).unwrap(),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -6583,7 +6621,9 @@ async fn test_subagent_error_propagation(cx: &mut TestAppContext) {
|
||||
id: "subagent_1".into(),
|
||||
name: SpawnAgentTool::NAME.into(),
|
||||
raw_input: serde_json::to_string(&subagent_tool_input).unwrap(),
|
||||
input: serde_json::to_value(&subagent_tool_input).unwrap(),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
serde_json::to_value(&subagent_tool_input).unwrap(),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -7372,7 +7412,7 @@ async fn test_always_allow_resolves_pending_authorizations(cx: &mut TestAppConte
|
||||
id: id.into(),
|
||||
name: ToolRequiringPermission::NAME.into(),
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -7451,7 +7491,7 @@ async fn test_external_settings_edit_resolves_pending_authorization(cx: &mut Tes
|
||||
id: "tool_id_1".into(),
|
||||
name: ToolRequiringPermission::NAME.into(),
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -7522,7 +7562,7 @@ async fn test_external_deny_rule_resolves_pending_authorization(cx: &mut TestApp
|
||||
id: "tool_id_1".into(),
|
||||
name: ToolRequiringPermission::NAME.into(),
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -7598,7 +7638,7 @@ async fn test_unrelated_settings_change_does_not_resolve_pending_authorization(
|
||||
id: "tool_id_1".into(),
|
||||
name: ToolRequiringPermission::NAME.into(),
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -7668,7 +7708,7 @@ async fn test_always_allow_does_not_resolve_unrelated_tool_authorization(cx: &mu
|
||||
id: id.into(),
|
||||
name: name.into(),
|
||||
raw_input: "{}".into(),
|
||||
input: json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -7765,7 +7805,7 @@ async fn test_queued_message_ends_turn_at_boundary(cx: &mut TestAppContext) {
|
||||
id: "tool_1".into(),
|
||||
name: "echo".into(),
|
||||
raw_input: r#"{"text": "hello"}"#.into(),
|
||||
input: json!({"text": "hello"}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "hello"})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -7847,7 +7887,7 @@ async fn test_queued_message_does_not_end_turn_without_boundary_flag(cx: &mut Te
|
||||
id: "tool_1".into(),
|
||||
name: "echo".into(),
|
||||
raw_input: r#"{"text": "hello"}"#.into(),
|
||||
input: json!({"text": "hello"}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "hello"})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -7914,7 +7954,7 @@ async fn test_streaming_tool_error_breaks_stream_loop_immediately(cx: &mut TestA
|
||||
id: "call_1".into(),
|
||||
name: StreamingFailingEchoTool::NAME.into(),
|
||||
raw_input: "hello".into(),
|
||||
input: json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({})),
|
||||
is_input_complete: false,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -7996,7 +8036,7 @@ async fn test_streaming_tool_error_waits_for_prior_tools_to_complete(cx: &mut Te
|
||||
id: "call_1".into(),
|
||||
name: StreamingEchoTool::NAME.into(),
|
||||
raw_input: "hello".into(),
|
||||
input: json!({ "text": "hello" }),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({ "text": "hello" })),
|
||||
is_input_complete: false,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -8005,7 +8045,7 @@ async fn test_streaming_tool_error_waits_for_prior_tools_to_complete(cx: &mut Te
|
||||
id: "call_1".into(),
|
||||
name: StreamingEchoTool::NAME.into(),
|
||||
raw_input: "hello world".into(),
|
||||
input: json!({ "text": "hello world" }),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({ "text": "hello world" })),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -8015,7 +8055,7 @@ async fn test_streaming_tool_error_waits_for_prior_tools_to_complete(cx: &mut Te
|
||||
let second_tool_use = LanguageModelToolUse {
|
||||
name: StreamingFailingEchoTool::NAME.into(),
|
||||
raw_input: "hello".into(),
|
||||
input: json!({ "text": "hello" }),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({ "text": "hello" })),
|
||||
is_input_complete: false,
|
||||
thought_signature: None,
|
||||
id: "call_2".into(),
|
||||
@@ -8144,7 +8184,7 @@ async fn test_mid_turn_model_and_settings_refresh(cx: &mut TestAppContext) {
|
||||
id: "tool_1".into(),
|
||||
name: "echo".into(),
|
||||
raw_input: r#"{"text":"hello"}"#.into(),
|
||||
input: json!({"text": "hello"}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({"text": "hello"})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
},
|
||||
|
||||
+47
-29
@@ -607,7 +607,7 @@ impl AgentMessage {
|
||||
"{}\n",
|
||||
MarkdownCodeBlock {
|
||||
tag: "json",
|
||||
text: &format!("{:#}", tool_use.input)
|
||||
text: &format!("{:#}", tool_use.input.to_display_json())
|
||||
}
|
||||
));
|
||||
}
|
||||
@@ -1593,7 +1593,7 @@ impl Thread {
|
||||
.unbounded_send(Ok(ThreadEvent::ToolCall(
|
||||
acp::ToolCall::new(tool_use.id.to_string(), tool_use.name.to_string())
|
||||
.status(status)
|
||||
.raw_input(tool_use.input.clone()),
|
||||
.raw_input(tool_use.input.to_display_json()),
|
||||
)))
|
||||
.ok();
|
||||
let mut fields = acp::ToolCallUpdateFields::new()
|
||||
@@ -1606,15 +1606,12 @@ impl Thread {
|
||||
return;
|
||||
};
|
||||
|
||||
let title = tool.initial_title(tool_use.input.clone(), cx);
|
||||
let Ok(input) = tool_use.input.clone().into_json() else {
|
||||
return;
|
||||
};
|
||||
let title = tool.initial_title(input.clone(), cx);
|
||||
let kind = tool.kind();
|
||||
stream.send_tool_call(
|
||||
&tool_use.id,
|
||||
&tool_use.name,
|
||||
title,
|
||||
kind,
|
||||
tool_use.input.clone(),
|
||||
);
|
||||
stream.send_tool_call(&tool_use.id, &tool_use.name, title, kind, input.clone());
|
||||
|
||||
if let Some(content) = replay_content {
|
||||
stream.update_tool_call_fields(
|
||||
@@ -1635,8 +1632,7 @@ impl Thread {
|
||||
self.sandbox_grants.clone(),
|
||||
Some(cx.weak_entity()),
|
||||
);
|
||||
tool.replay(tool_use.input.clone(), output, tool_event_stream, cx)
|
||||
.log_err();
|
||||
tool.replay(input, output, tool_event_stream, cx).log_err();
|
||||
}
|
||||
|
||||
stream.update_tool_call_fields(
|
||||
@@ -3418,7 +3414,9 @@ impl Thread {
|
||||
let mut title = SharedString::from(&tool_use.name);
|
||||
let mut kind = acp::ToolKind::Other;
|
||||
if let Some(tool) = tool.as_ref() {
|
||||
title = tool.initial_title(tool_use.input.clone(), cx);
|
||||
if let Ok(input) = tool_use.input.clone().into_json() {
|
||||
title = tool.initial_title(input, cx);
|
||||
}
|
||||
kind = tool.kind();
|
||||
}
|
||||
|
||||
@@ -3435,16 +3433,33 @@ impl Thread {
|
||||
}));
|
||||
};
|
||||
|
||||
// Agent tools are JSON-schema tools. Custom text-tool deltas are rejected
|
||||
// before considering partial-vs-complete input for these local tools.
|
||||
let input = match tool_use.input.clone().into_json() {
|
||||
Ok(input) => input,
|
||||
Err(error) => {
|
||||
return Some(Task::ready(LanguageModelToolResult {
|
||||
content: vec![LanguageModelToolResultContent::Text(Arc::from(
|
||||
error.to_string(),
|
||||
))],
|
||||
tool_use_id: tool_use.id,
|
||||
tool_name: tool_use.name,
|
||||
is_error: true,
|
||||
output: None,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
if !tool_use.is_input_complete {
|
||||
if tool.supports_input_streaming() {
|
||||
let running_turn = self.running_turn.as_mut()?;
|
||||
if let Some(sender) = running_turn.streaming_tool_inputs.get_mut(&tool_use.id) {
|
||||
sender.send_partial(tool_use.input);
|
||||
sender.send_partial(input);
|
||||
return None;
|
||||
}
|
||||
|
||||
let (mut sender, tool_input) = ToolInputSender::channel();
|
||||
sender.send_partial(tool_use.input);
|
||||
sender.send_partial(input);
|
||||
running_turn
|
||||
.streaming_tool_inputs
|
||||
.insert(tool_use.id.clone(), sender);
|
||||
@@ -3471,12 +3486,12 @@ impl Thread {
|
||||
.streaming_tool_inputs
|
||||
.remove(&tool_use.id)
|
||||
{
|
||||
sender.send_full(tool_use.input);
|
||||
sender.send_full(input);
|
||||
return None;
|
||||
}
|
||||
|
||||
log::debug!("Running tool {}", tool_use.name);
|
||||
let tool_input = ToolInput::ready(tool_use.input);
|
||||
let tool_input = ToolInput::ready(input);
|
||||
Some(self.run_tool(
|
||||
tool,
|
||||
tool_input,
|
||||
@@ -3600,7 +3615,7 @@ impl Thread {
|
||||
id: tool_use_id,
|
||||
name: tool_name,
|
||||
raw_input: raw_input.to_string(),
|
||||
input: serde_json::json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(serde_json::json!({})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -3676,7 +3691,7 @@ impl Thread {
|
||||
&tool_use.name,
|
||||
title,
|
||||
kind,
|
||||
tool_use.input.clone(),
|
||||
tool_use.input.to_display_json(),
|
||||
);
|
||||
last_message
|
||||
.content
|
||||
@@ -3687,7 +3702,7 @@ impl Thread {
|
||||
acp::ToolCallUpdateFields::new()
|
||||
.title(title.as_str())
|
||||
.kind(kind)
|
||||
.raw_input(tool_use.input.clone()),
|
||||
.raw_input(tool_use.input.to_display_json()),
|
||||
None,
|
||||
);
|
||||
}
|
||||
@@ -3927,12 +3942,12 @@ impl Thread {
|
||||
.iter()
|
||||
.filter_map(|(tool_name, tool)| {
|
||||
log::trace!("Including tool: {}", tool_name);
|
||||
Some(LanguageModelRequestTool {
|
||||
name: tool_name.to_string(),
|
||||
description: tool.description().to_string(),
|
||||
input_schema: tool.input_schema(model.tool_input_format()).log_err()?,
|
||||
use_input_streaming: tool.supports_input_streaming(),
|
||||
})
|
||||
Some(LanguageModelRequestTool::function(
|
||||
tool_name.to_string(),
|
||||
tool.description().to_string(),
|
||||
tool.input_schema(model.tool_input_format()).log_err()?,
|
||||
tool.supports_input_streaming(),
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
@@ -7870,7 +7885,7 @@ mod tests {
|
||||
id: registered_tool_use_id.clone(),
|
||||
name: ReplayImageTool::NAME.into(),
|
||||
raw_input: "null".to_string(),
|
||||
input: json!(null),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!(null)),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -7878,7 +7893,7 @@ mod tests {
|
||||
id: missing_tool_use_id.clone(),
|
||||
name: "missing_image_tool".into(),
|
||||
raw_input: "{}".to_string(),
|
||||
input: json!({}),
|
||||
input: language_model::LanguageModelToolUseInput::Json(json!({})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -8185,7 +8200,10 @@ mod tests {
|
||||
assert_eq!(tool_use.raw_input, raw_input.to_string());
|
||||
assert!(tool_use.is_input_complete);
|
||||
// Should fall back to empty object for invalid JSON
|
||||
assert_eq!(tool_use.input, json!({}));
|
||||
assert_eq!(
|
||||
tool_use.input,
|
||||
language_model::LanguageModelToolUseInput::Json(json!({}))
|
||||
);
|
||||
}
|
||||
_ => panic!("Expected ToolUse content"),
|
||||
}
|
||||
|
||||
@@ -153,12 +153,12 @@ macro_rules! tools {
|
||||
/// A list of all built-in tools
|
||||
pub fn built_in_tools() -> impl Iterator<Item = LanguageModelRequestTool> {
|
||||
fn language_model_tool<T: AgentTool>() -> LanguageModelRequestTool {
|
||||
LanguageModelRequestTool {
|
||||
name: T::NAME.to_string(),
|
||||
description: T::description().to_string(),
|
||||
input_schema: T::input_schema(LanguageModelToolSchemaFormat::JsonSchema).to_value(),
|
||||
use_input_streaming: T::supports_input_streaming(),
|
||||
}
|
||||
LanguageModelRequestTool::function(
|
||||
T::NAME.to_string(),
|
||||
T::description().to_string(),
|
||||
T::input_schema(LanguageModelToolSchemaFormat::JsonSchema).to_value(),
|
||||
T::supports_input_streaming(),
|
||||
)
|
||||
}
|
||||
[
|
||||
$(
|
||||
|
||||
@@ -503,7 +503,9 @@ impl EditToolTest {
|
||||
if tool_use.is_input_complete
|
||||
&& tool_use.name.as_ref() == EditFileTool::NAME =>
|
||||
{
|
||||
let input: EditFileToolInput = serde_json::from_value(tool_use.input)
|
||||
let input: EditFileToolInput = tool_use
|
||||
.input
|
||||
.parse()
|
||||
.context("Failed to parse tool input as EditFileToolInput")?;
|
||||
return Ok(input);
|
||||
}
|
||||
@@ -607,7 +609,9 @@ fn tool_use(
|
||||
id: LanguageModelToolUseId::from(id.into()),
|
||||
name: name.into(),
|
||||
raw_input: serde_json::to_string_pretty(&input).unwrap(),
|
||||
input: serde_json::to_value(input).unwrap(),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
serde_json::to_value(input).unwrap(),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
})
|
||||
|
||||
@@ -327,7 +327,9 @@ async fn extract_tool_use(
|
||||
Ok(LanguageModelCompletionEvent::ToolUse(tool_use))
|
||||
if tool_use.is_input_complete && tool_use.name.as_ref() == TerminalTool::NAME =>
|
||||
{
|
||||
let input: TerminalToolInput = serde_json::from_value(tool_use.input)
|
||||
let input: TerminalToolInput = tool_use
|
||||
.input
|
||||
.parse()
|
||||
.context("Failed to parse tool input as TerminalToolInput")?;
|
||||
return Ok(input);
|
||||
}
|
||||
|
||||
@@ -323,7 +323,9 @@ impl WriteToolTest {
|
||||
if tool_use.is_input_complete
|
||||
&& tool_use.name.as_ref() == WriteFileTool::NAME =>
|
||||
{
|
||||
let input: WriteFileToolInput = serde_json::from_value(tool_use.input)
|
||||
let input: WriteFileToolInput = tool_use
|
||||
.input
|
||||
.parse()
|
||||
.context("Failed to parse tool input as WriteFileToolInput")?;
|
||||
return Ok(input);
|
||||
}
|
||||
@@ -410,7 +412,9 @@ fn tool_use(
|
||||
id: LanguageModelToolUseId::from(id.into()),
|
||||
name: name.into(),
|
||||
raw_input: serde_json::to_string_pretty(&input).unwrap(),
|
||||
input: serde_json::to_value(input).unwrap(),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
serde_json::to_value(input).unwrap(),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
})
|
||||
|
||||
@@ -525,18 +525,18 @@ impl CodegenAlternative {
|
||||
messages.push(user_message);
|
||||
|
||||
let tools = vec![
|
||||
LanguageModelRequestTool {
|
||||
name: REWRITE_SECTION_TOOL_NAME.to_string(),
|
||||
description: "Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.".to_string(),
|
||||
input_schema: language_model::tool_schema::root_schema_for::<RewriteSectionInput>(tool_input_format).to_value(),
|
||||
use_input_streaming: false,
|
||||
},
|
||||
LanguageModelRequestTool {
|
||||
name: FAILURE_MESSAGE_TOOL_NAME.to_string(),
|
||||
description: "Use this tool to provide a message to the user when you're unable to complete a task.".to_string(),
|
||||
input_schema: language_model::tool_schema::root_schema_for::<FailureMessageInput>(tool_input_format).to_value(),
|
||||
use_input_streaming: false,
|
||||
},
|
||||
LanguageModelRequestTool::function(
|
||||
REWRITE_SECTION_TOOL_NAME.to_string(),
|
||||
"Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.".to_string(),
|
||||
language_model::tool_schema::root_schema_for::<RewriteSectionInput>(tool_input_format).to_value(),
|
||||
false,
|
||||
),
|
||||
LanguageModelRequestTool::function(
|
||||
FAILURE_MESSAGE_TOOL_NAME.to_string(),
|
||||
"Use this tool to provide a message to the user when you're unable to complete a task.".to_string(),
|
||||
language_model::tool_schema::root_schema_for::<FailureMessageInput>(tool_input_format).to_value(),
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
LanguageModelRequest {
|
||||
@@ -1179,9 +1179,7 @@ impl CodegenAlternative {
|
||||
let mut chars_read_by_tool_id = chars_read_by_tool_id.lock();
|
||||
match tool_use.name.as_ref() {
|
||||
REWRITE_SECTION_TOOL_NAME => {
|
||||
let Ok(input) =
|
||||
serde_json::from_value::<RewriteSectionInput>(tool_use.input)
|
||||
else {
|
||||
let Ok(input) = tool_use.input.parse::<RewriteSectionInput>() else {
|
||||
return None;
|
||||
};
|
||||
let chars_read_so_far =
|
||||
@@ -1198,9 +1196,7 @@ impl CodegenAlternative {
|
||||
})
|
||||
}
|
||||
FAILURE_MESSAGE_TOOL_NAME => {
|
||||
let Ok(mut input) =
|
||||
serde_json::from_value::<FailureMessageInput>(tool_use.input)
|
||||
else {
|
||||
let Ok(mut input) = tool_use.input.parse::<FailureMessageInput>() else {
|
||||
return None;
|
||||
};
|
||||
Some(ToolUseOutput::Failure(std::mem::take(&mut input.message)))
|
||||
@@ -2011,7 +2007,9 @@ mod tests {
|
||||
id: id.into(),
|
||||
name: REWRITE_SECTION_TOOL_NAME.into(),
|
||||
raw_input: serde_json::to_string(&input).unwrap(),
|
||||
input: serde_json::to_value(&input).unwrap(),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
serde_json::to_value(&input).unwrap(),
|
||||
),
|
||||
is_input_complete: is_complete,
|
||||
thought_signature: None,
|
||||
})
|
||||
|
||||
@@ -3,9 +3,9 @@ use collections::HashMap;
|
||||
use futures::{Stream, StreamExt};
|
||||
use language_model_core::{
|
||||
CompactionContent, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelProviderName, LanguageModelRequest, LanguageModelToolChoice,
|
||||
LanguageModelToolResultContent, LanguageModelToolUse, MessageContent, Role, StopReason,
|
||||
TokenUsage,
|
||||
LanguageModelProviderName, LanguageModelRequest, LanguageModelRequestToolInput,
|
||||
LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse,
|
||||
LanguageModelToolUseInput, MessageContent, Role, StopReason, TokenUsage,
|
||||
util::{fix_streamed_json, parse_tool_arguments},
|
||||
};
|
||||
use std::pin::Pin;
|
||||
@@ -68,7 +68,7 @@ fn mark_last_cacheable_content(content: &mut [RequestContent], cache_control: Ca
|
||||
}
|
||||
}
|
||||
|
||||
fn to_anthropic_content(content: MessageContent) -> Option<RequestContent> {
|
||||
fn to_anthropic_content(content: MessageContent) -> Result<Option<RequestContent>> {
|
||||
match content {
|
||||
MessageContent::Text(text) => {
|
||||
let text = if text.chars().last().is_some_and(|c| c.is_whitespace()) {
|
||||
@@ -77,12 +77,12 @@ fn to_anthropic_content(content: MessageContent) -> Option<RequestContent> {
|
||||
text
|
||||
};
|
||||
if !text.is_empty() {
|
||||
Some(RequestContent::Text {
|
||||
Ok(Some(RequestContent::Text {
|
||||
text,
|
||||
cache_control: None,
|
||||
})
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
MessageContent::Thinking {
|
||||
@@ -92,36 +92,41 @@ fn to_anthropic_content(content: MessageContent) -> Option<RequestContent> {
|
||||
if let Some(signature) = signature
|
||||
&& !thinking.is_empty()
|
||||
{
|
||||
Some(RequestContent::Thinking {
|
||||
Ok(Some(RequestContent::Thinking {
|
||||
thinking,
|
||||
signature,
|
||||
cache_control: None,
|
||||
})
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
MessageContent::RedactedThinking(data) => {
|
||||
if !data.is_empty() {
|
||||
Some(RequestContent::RedactedThinking { data })
|
||||
Ok(Some(RequestContent::RedactedThinking { data }))
|
||||
} else {
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
MessageContent::Image(image) => Some(RequestContent::Image {
|
||||
MessageContent::Image(image) => Ok(Some(RequestContent::Image {
|
||||
source: ImageSource {
|
||||
source_type: "base64".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
data: image.source.to_string(),
|
||||
},
|
||||
cache_control: None,
|
||||
}),
|
||||
MessageContent::ToolUse(tool_use) => Some(RequestContent::ToolUse {
|
||||
id: tool_use.id.to_string(),
|
||||
name: tool_use.name.to_string(),
|
||||
input: tool_use.input,
|
||||
cache_control: None,
|
||||
}),
|
||||
})),
|
||||
MessageContent::ToolUse(tool_use) => match tool_use.input {
|
||||
LanguageModelToolUseInput::Json(input) => Ok(Some(RequestContent::ToolUse {
|
||||
id: tool_use.id.to_string(),
|
||||
name: tool_use.name.to_string(),
|
||||
input,
|
||||
cache_control: None,
|
||||
})),
|
||||
LanguageModelToolUseInput::Text(_) => Err(anyhow::anyhow!(
|
||||
"Anthropic does not support custom tool calls"
|
||||
)),
|
||||
},
|
||||
MessageContent::ToolResult(tool_result) => {
|
||||
let content = match tool_result.content.as_slice() {
|
||||
[LanguageModelToolResultContent::Text(text)] => {
|
||||
@@ -147,24 +152,24 @@ fn to_anthropic_content(content: MessageContent) -> Option<RequestContent> {
|
||||
ToolResultContent::Multipart(parts)
|
||||
}
|
||||
};
|
||||
Some(RequestContent::ToolResult {
|
||||
Ok(Some(RequestContent::ToolResult {
|
||||
tool_use_id: tool_result.tool_use_id.to_string(),
|
||||
is_error: tool_result.is_error,
|
||||
content,
|
||||
cache_control: None,
|
||||
})
|
||||
}))
|
||||
}
|
||||
MessageContent::Compaction(CompactionContent::Summary { content }) => {
|
||||
Some(RequestContent::Compaction {
|
||||
Ok(Some(RequestContent::Compaction {
|
||||
content,
|
||||
cache_control: None,
|
||||
})
|
||||
}))
|
||||
}
|
||||
// Encrypted compaction blocks come from other providers, and a
|
||||
// Pending block is a streaming-only UI signal; neither is replayed.
|
||||
MessageContent::Compaction(
|
||||
CompactionContent::Encrypted { .. } | CompactionContent::Pending,
|
||||
) => None,
|
||||
) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +180,7 @@ pub fn into_anthropic(
|
||||
max_output_tokens: u64,
|
||||
mode: AnthropicModelMode,
|
||||
cache_mode: AnthropicPromptCacheMode,
|
||||
) -> crate::Request {
|
||||
) -> Result<crate::Request> {
|
||||
let mut new_messages: Vec<Message> = Vec::new();
|
||||
let mut system_message = String::new();
|
||||
let mut any_message_wants_cache = false;
|
||||
@@ -189,11 +194,12 @@ pub fn into_anthropic(
|
||||
|
||||
match message.role {
|
||||
Role::User | Role::Assistant => {
|
||||
let mut anthropic_message_content: Vec<RequestContent> = message
|
||||
.content
|
||||
.into_iter()
|
||||
.filter_map(to_anthropic_content)
|
||||
.collect();
|
||||
let mut anthropic_message_content = Vec::new();
|
||||
for content in message.content {
|
||||
if let Some(content) = to_anthropic_content(content)? {
|
||||
anthropic_message_content.push(content);
|
||||
}
|
||||
}
|
||||
let anthropic_role = match message.role {
|
||||
Role::User => crate::Role::User,
|
||||
Role::Assistant => crate::Role::Assistant,
|
||||
@@ -261,21 +267,29 @@ pub fn into_anthropic(
|
||||
let mut tools: Vec<Tool> = request
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| Tool {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
input_schema: tool.input_schema,
|
||||
eager_input_streaming: tool.use_input_streaming,
|
||||
cache_control: None,
|
||||
.map(|tool| match tool.input {
|
||||
LanguageModelRequestToolInput::Function {
|
||||
input_schema,
|
||||
use_input_streaming,
|
||||
} => Ok(Tool {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
input_schema,
|
||||
eager_input_streaming: use_input_streaming,
|
||||
cache_control: None,
|
||||
}),
|
||||
LanguageModelRequestToolInput::Custom { .. } => {
|
||||
Err(anyhow::anyhow!("Anthropic does not support custom tools"))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
.collect::<Result<_>>()?;
|
||||
if let Some(cache_control) = long_lived_cache
|
||||
&& let Some(last_tool) = tools.last_mut()
|
||||
{
|
||||
last_tool.cache_control = Some(cache_control);
|
||||
}
|
||||
|
||||
crate::Request {
|
||||
Ok(crate::Request {
|
||||
model,
|
||||
messages: new_messages,
|
||||
max_tokens: max_output_tokens,
|
||||
@@ -339,7 +353,7 @@ pub fn into_anthropic(
|
||||
trigger: Some(CompactionTrigger::InputTokens { value }),
|
||||
}],
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub struct AnthropicEventMapper {
|
||||
@@ -451,7 +465,7 @@ impl AnthropicEventMapper {
|
||||
name: tool_use.name.clone().into(),
|
||||
is_input_complete: false,
|
||||
raw_input: tool_use.input_json.clone(),
|
||||
input,
|
||||
input: LanguageModelToolUseInput::Json(input),
|
||||
thought_signature: None,
|
||||
},
|
||||
))];
|
||||
@@ -469,7 +483,7 @@ impl AnthropicEventMapper {
|
||||
id: tool_use.id.into(),
|
||||
name: tool_use.name.into(),
|
||||
is_input_complete: true,
|
||||
input,
|
||||
input: LanguageModelToolUseInput::Json(input),
|
||||
raw_input: tool_use.input_json.clone(),
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -597,12 +611,12 @@ mod tests {
|
||||
intent: None,
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
tools: vec![language_model_core::LanguageModelRequestTool {
|
||||
name: "do_thing".into(),
|
||||
description: "Does a thing.".into(),
|
||||
input_schema: serde_json::json!({"type": "object"}),
|
||||
use_input_streaming: false,
|
||||
}],
|
||||
tools: vec![language_model_core::LanguageModelRequestTool::function(
|
||||
"do_thing".into(),
|
||||
"Does a thing.".into(),
|
||||
serde_json::json!({"type": "object"}),
|
||||
false,
|
||||
)],
|
||||
tool_choice: None,
|
||||
thinking_allowed: true,
|
||||
thinking_effort: None,
|
||||
@@ -617,7 +631,8 @@ mod tests {
|
||||
4096,
|
||||
AnthropicModelMode::Default,
|
||||
AnthropicPromptCacheMode::Automatic,
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// No message content block should carry cache_control anymore; the
|
||||
// conversation breakpoint is set via top-level automatic caching.
|
||||
@@ -703,12 +718,12 @@ mod tests {
|
||||
intent: None,
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
tools: vec![language_model_core::LanguageModelRequestTool {
|
||||
name: "do_thing".into(),
|
||||
description: "Does a thing.".into(),
|
||||
input_schema: serde_json::json!({"type": "object"}),
|
||||
use_input_streaming: false,
|
||||
}],
|
||||
tools: vec![language_model_core::LanguageModelRequestTool::function(
|
||||
"do_thing".into(),
|
||||
"Does a thing.".into(),
|
||||
serde_json::json!({"type": "object"}),
|
||||
false,
|
||||
)],
|
||||
tool_choice: None,
|
||||
thinking_allowed: true,
|
||||
thinking_effort: None,
|
||||
@@ -723,7 +738,8 @@ mod tests {
|
||||
4096,
|
||||
AnthropicModelMode::Default,
|
||||
AnthropicPromptCacheMode::Legacy,
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(anthropic_request.cache_control.is_none());
|
||||
assert!(matches!(
|
||||
@@ -781,7 +797,8 @@ mod tests {
|
||||
128_000,
|
||||
AnthropicModelMode::AdaptiveThinking,
|
||||
AnthropicPromptCacheMode::Automatic,
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
anthropic_request
|
||||
@@ -813,12 +830,12 @@ mod tests {
|
||||
intent: None,
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
tools: vec![language_model_core::LanguageModelRequestTool {
|
||||
name: "do_thing".into(),
|
||||
description: "Does a thing.".into(),
|
||||
input_schema: serde_json::json!({"type": "object"}),
|
||||
use_input_streaming: false,
|
||||
}],
|
||||
tools: vec![language_model_core::LanguageModelRequestTool::function(
|
||||
"do_thing".into(),
|
||||
"Does a thing.".into(),
|
||||
serde_json::json!({"type": "object"}),
|
||||
false,
|
||||
)],
|
||||
tool_choice: None,
|
||||
thinking_allowed: true,
|
||||
thinking_effort: None,
|
||||
@@ -833,7 +850,8 @@ mod tests {
|
||||
4096,
|
||||
AnthropicModelMode::Default,
|
||||
AnthropicPromptCacheMode::Automatic,
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(anthropic_request.cache_control.is_none());
|
||||
assert!(matches!(
|
||||
@@ -879,6 +897,7 @@ mod tests {
|
||||
},
|
||||
AnthropicPromptCacheMode::Automatic,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -977,7 +996,8 @@ mod tests {
|
||||
4096,
|
||||
AnthropicModelMode::Default,
|
||||
AnthropicPromptCacheMode::Disabled,
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(&anthropic_request.context_management).unwrap(),
|
||||
|
||||
@@ -2,8 +2,9 @@ use anyhow::Result;
|
||||
use futures::{Stream, StreamExt};
|
||||
use language_model_core::{
|
||||
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelRequest,
|
||||
LanguageModelToolChoice, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
|
||||
StopReason, TokenUsage,
|
||||
LanguageModelRequestToolInput, LanguageModelToolChoice, LanguageModelToolUse,
|
||||
LanguageModelToolUseId, LanguageModelToolUseInput, MessageContent, Role, StopReason,
|
||||
TokenUsage,
|
||||
};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
@@ -20,20 +21,18 @@ pub fn into_google(
|
||||
mut request: LanguageModelRequest,
|
||||
model_id: String,
|
||||
mode: GoogleModelMode,
|
||||
) -> crate::GenerateContentRequest {
|
||||
fn map_content(content: Vec<MessageContent>) -> Vec<Part> {
|
||||
content
|
||||
.into_iter()
|
||||
.flat_map(|content| match content {
|
||||
) -> Result<crate::GenerateContentRequest> {
|
||||
fn map_content(content: Vec<MessageContent>) -> Result<Vec<Part>> {
|
||||
let mut mapped_parts = Vec::new();
|
||||
for content in content {
|
||||
match content {
|
||||
MessageContent::Text(text) => {
|
||||
if !text.is_empty() {
|
||||
vec![Part::TextPart(TextPart {
|
||||
mapped_parts.push(Part::TextPart(TextPart {
|
||||
text,
|
||||
thought: false,
|
||||
thought_signature: None,
|
||||
})]
|
||||
} else {
|
||||
vec![]
|
||||
}));
|
||||
}
|
||||
}
|
||||
MessageContent::Thinking {
|
||||
@@ -41,39 +40,37 @@ pub fn into_google(
|
||||
signature: Some(signature),
|
||||
} => {
|
||||
if !signature.is_empty() {
|
||||
vec![Part::TextPart(TextPart {
|
||||
mapped_parts.push(Part::TextPart(TextPart {
|
||||
text,
|
||||
thought: true,
|
||||
thought_signature: Some(signature),
|
||||
})]
|
||||
} else {
|
||||
vec![]
|
||||
}));
|
||||
}
|
||||
}
|
||||
MessageContent::Thinking { .. } => {
|
||||
vec![]
|
||||
}
|
||||
MessageContent::RedactedThinking(_) | MessageContent::Compaction(_) => vec![],
|
||||
MessageContent::Thinking { .. } => {}
|
||||
MessageContent::RedactedThinking(_) | MessageContent::Compaction(_) => {}
|
||||
MessageContent::Image(image) => {
|
||||
vec![Part::InlineDataPart(InlineDataPart {
|
||||
mapped_parts.push(Part::InlineDataPart(InlineDataPart {
|
||||
inline_data: GenerativeContentBlob {
|
||||
mime_type: "image/png".to_string(),
|
||||
data: image.source.to_string(),
|
||||
},
|
||||
})]
|
||||
}));
|
||||
}
|
||||
MessageContent::ToolUse(tool_use) => {
|
||||
// Normalize empty string signatures to None
|
||||
let thought_signature = tool_use.thought_signature.filter(|s| !s.is_empty());
|
||||
let LanguageModelToolUseInput::Json(input) = tool_use.input else {
|
||||
anyhow::bail!("Google AI does not support custom tool calls");
|
||||
};
|
||||
|
||||
vec![Part::FunctionCallPart(crate::FunctionCallPart {
|
||||
mapped_parts.push(Part::FunctionCallPart(crate::FunctionCallPart {
|
||||
function_call: crate::FunctionCall {
|
||||
name: tool_use.name.to_string(),
|
||||
args: tool_use.input,
|
||||
args: input,
|
||||
id: Some(tool_use.id.to_string()),
|
||||
},
|
||||
thought_signature,
|
||||
})]
|
||||
}));
|
||||
}
|
||||
MessageContent::ToolResult(tool_result) => {
|
||||
let mut text_output = String::new();
|
||||
@@ -98,7 +95,7 @@ pub fn into_google(
|
||||
} else {
|
||||
text_output
|
||||
};
|
||||
let mut parts = vec![Part::FunctionResponsePart(crate::FunctionResponsePart {
|
||||
mapped_parts.push(Part::FunctionResponsePart(crate::FunctionResponsePart {
|
||||
function_response: crate::FunctionResponse {
|
||||
name: tool_result.tool_name.to_string(),
|
||||
// The API expects a valid JSON object
|
||||
@@ -107,12 +104,12 @@ pub fn into_google(
|
||||
}),
|
||||
id: Some(tool_result.tool_use_id.to_string()),
|
||||
},
|
||||
})];
|
||||
parts.extend(images.into_iter().map(Part::InlineDataPart));
|
||||
parts
|
||||
}));
|
||||
mapped_parts.extend(images.into_iter().map(Part::InlineDataPart));
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
Ok(mapped_parts)
|
||||
}
|
||||
|
||||
let thinking_config = thinking_config_for_request(&request, &model_id, mode);
|
||||
@@ -124,33 +121,59 @@ pub fn into_google(
|
||||
{
|
||||
let message = request.messages.remove(0);
|
||||
Some(SystemInstruction {
|
||||
parts: map_content(message.content),
|
||||
parts: map_content(message.content)?,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
crate::GenerateContentRequest {
|
||||
let tools = if request.tools.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(vec![crate::Tool {
|
||||
function_declarations: request
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| match tool.input {
|
||||
LanguageModelRequestToolInput::Function { input_schema, .. } => {
|
||||
Ok(FunctionDeclaration {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: input_schema,
|
||||
})
|
||||
}
|
||||
LanguageModelRequestToolInput::Custom { .. } => {
|
||||
Err(anyhow::anyhow!("Google AI does not support custom tools"))
|
||||
}
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
}])
|
||||
};
|
||||
|
||||
Ok(crate::GenerateContentRequest {
|
||||
model: ModelName { model_id },
|
||||
system_instruction: system_instructions,
|
||||
contents: request
|
||||
.messages
|
||||
.into_iter()
|
||||
.filter_map(|message| {
|
||||
let parts = map_content(message.content);
|
||||
.map(|message| {
|
||||
let parts = map_content(message.content)?;
|
||||
if parts.is_empty() {
|
||||
None
|
||||
Ok(None)
|
||||
} else {
|
||||
Some(Content {
|
||||
Ok(Some(Content {
|
||||
parts,
|
||||
role: match message.role {
|
||||
Role::User => crate::Role::User,
|
||||
Role::Assistant => crate::Role::Model,
|
||||
Role::System => crate::Role::User, // Google AI doesn't have a system role
|
||||
},
|
||||
})
|
||||
}))
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
generation_config: Some(GenerationConfig {
|
||||
candidate_count: Some(1),
|
||||
@@ -162,19 +185,7 @@ pub fn into_google(
|
||||
top_k: None,
|
||||
}),
|
||||
safety_settings: None,
|
||||
tools: (!request.tools.is_empty()).then(|| {
|
||||
vec![crate::Tool {
|
||||
function_declarations: request
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| FunctionDeclaration {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: tool.input_schema,
|
||||
})
|
||||
.collect(),
|
||||
}]
|
||||
}),
|
||||
tools,
|
||||
tool_config: request.tool_choice.map(|choice| ToolConfig {
|
||||
function_calling_config: FunctionCallingConfig {
|
||||
mode: match choice {
|
||||
@@ -185,7 +196,7 @@ pub fn into_google(
|
||||
allowed_function_names: None,
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn thinking_config_for_request(
|
||||
@@ -404,7 +415,9 @@ impl GoogleEventMapper {
|
||||
name,
|
||||
is_input_complete: true,
|
||||
raw_input: function_call_part.function_call.args.to_string(),
|
||||
input: function_call_part.function_call.args,
|
||||
input: LanguageModelToolUseInput::Json(
|
||||
function_call_part.function_call.args,
|
||||
),
|
||||
thought_signature,
|
||||
},
|
||||
)));
|
||||
@@ -493,7 +506,8 @@ mod tests {
|
||||
GoogleModelMode::Thinking {
|
||||
budget_tokens: None,
|
||||
},
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let thinking_config = request.generation_config.unwrap().thinking_config.unwrap();
|
||||
assert_eq!(thinking_config.include_thoughts, Some(true));
|
||||
@@ -515,7 +529,8 @@ mod tests {
|
||||
GoogleModelMode::Thinking {
|
||||
budget_tokens: None,
|
||||
},
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let thinking_config = request.generation_config.unwrap().thinking_config.unwrap();
|
||||
assert_eq!(thinking_config.thinking_budget, Some(0));
|
||||
@@ -533,7 +548,8 @@ mod tests {
|
||||
GoogleModelMode::Thinking {
|
||||
budget_tokens: None,
|
||||
},
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let thinking_config = request.generation_config.unwrap().thinking_config.unwrap();
|
||||
assert_eq!(thinking_config.thinking_level, Some(ThinkingLevel::Minimal));
|
||||
@@ -561,7 +577,8 @@ mod tests {
|
||||
GoogleModelMode::Thinking {
|
||||
budget_tokens: None,
|
||||
},
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let Part::TextPart(text_part) = &request.contents[0].parts[0] else {
|
||||
panic!("expected text part");
|
||||
|
||||
@@ -5,7 +5,7 @@ mod role;
|
||||
pub mod tool_schema;
|
||||
pub mod util;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use cloud_llm_client::CompletionRequestStatus;
|
||||
use http_client::{StatusCode, http};
|
||||
use schemars::JsonSchema;
|
||||
@@ -351,13 +351,101 @@ pub struct LanguageModelToolUse {
|
||||
pub id: LanguageModelToolUseId,
|
||||
pub name: Arc<str>,
|
||||
pub raw_input: String,
|
||||
pub input: serde_json::Value,
|
||||
pub input: LanguageModelToolUseInput,
|
||||
pub is_input_complete: bool,
|
||||
/// Thought signature the model sent us. Some models require that this
|
||||
/// signature be preserved and sent back in conversation history for validation.
|
||||
pub thought_signature: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
pub enum LanguageModelToolUseInput {
|
||||
Json(serde_json::Value),
|
||||
Text(String),
|
||||
}
|
||||
|
||||
impl Serialize for LanguageModelToolUseInput {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
|
||||
let mut state = serializer.serialize_struct("LanguageModelToolUseInput", 2)?;
|
||||
match self {
|
||||
Self::Json(input) => {
|
||||
state.serialize_field("type", "json")?;
|
||||
state.serialize_field("value", input)?;
|
||||
}
|
||||
Self::Text(input) => {
|
||||
state.serialize_field("type", "text")?;
|
||||
state.serialize_field("value", input)?;
|
||||
}
|
||||
}
|
||||
state.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for LanguageModelToolUseInput {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = serde_json::Value::deserialize(deserializer)?;
|
||||
if let Some(object) = value.as_object()
|
||||
&& object.len() == 2
|
||||
&& let Some(input_type) = object.get("type").and_then(|value| value.as_str())
|
||||
&& let Some(input) = object.get("value")
|
||||
{
|
||||
return match input_type {
|
||||
"json" => Ok(Self::Json(input.clone())),
|
||||
"text" => input
|
||||
.as_str()
|
||||
.map(|input| Self::Text(input.to_string()))
|
||||
.ok_or_else(|| serde::de::Error::custom("text tool input must be a string")),
|
||||
_ => Ok(Self::Json(value)),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(Self::Json(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModelToolUseInput {
|
||||
pub fn as_json(&self) -> Option<&serde_json::Value> {
|
||||
match self {
|
||||
Self::Json(input) => Some(input),
|
||||
Self::Text(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed parsing for JSON tool inputs; freeform (Text) inputs always error.
|
||||
///
|
||||
/// Callers wanting the raw value should use [`Self::as_json`] or [`Self::into_json`].
|
||||
pub fn parse<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
|
||||
match self {
|
||||
Self::Json(input) => {
|
||||
serde_json::from_value(input.clone()).context("failed to parse JSON tool input")
|
||||
}
|
||||
Self::Text(_) => Err(anyhow!("custom tool text input cannot be parsed as JSON")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_json(self) -> Result<serde_json::Value> {
|
||||
match self {
|
||||
Self::Json(input) => Ok(input),
|
||||
Self::Text(_) => Err(anyhow!("custom tool text input cannot be used as JSON")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_display_json(&self) -> serde_json::Value {
|
||||
match self {
|
||||
Self::Json(input) => input.clone(),
|
||||
Self::Text(input) => serde_json::Value::String(input.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LanguageModelEffortLevel {
|
||||
pub name: SharedString,
|
||||
@@ -624,7 +712,7 @@ mod tests {
|
||||
id: LanguageModelToolUseId::from("test_id"),
|
||||
name: "test_tool".into(),
|
||||
raw_input: json!({"arg": "value"}).to_string(),
|
||||
input: json!({"arg": "value"}),
|
||||
input: LanguageModelToolUseInput::Json(json!({"arg": "value"})),
|
||||
is_input_complete: true,
|
||||
thought_signature: Some("test_signature".to_string()),
|
||||
};
|
||||
@@ -652,9 +740,97 @@ mod tests {
|
||||
|
||||
assert_eq!(tool_use.id, LanguageModelToolUseId::from("test_id"));
|
||||
assert_eq!(tool_use.name.as_ref(), "test_tool");
|
||||
assert_eq!(
|
||||
tool_use.input,
|
||||
LanguageModelToolUseInput::Json(json!({"arg": "value"}))
|
||||
);
|
||||
assert_eq!(tool_use.thought_signature, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_language_model_tool_use_input_round_trips_json() {
|
||||
use serde_json::json;
|
||||
|
||||
let input = LanguageModelToolUseInput::Json(json!({"arg": "value"}));
|
||||
let serialized = serde_json::to_value(&input).unwrap();
|
||||
assert_eq!(
|
||||
serialized,
|
||||
json!({
|
||||
"type": "json",
|
||||
"value": {"arg": "value"}
|
||||
})
|
||||
);
|
||||
|
||||
let deserialized: LanguageModelToolUseInput = serde_json::from_value(serialized).unwrap();
|
||||
assert_eq!(deserialized, input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_language_model_tool_use_input_round_trips_text() {
|
||||
use serde_json::json;
|
||||
|
||||
let input = LanguageModelToolUseInput::Text("raw custom input".to_string());
|
||||
let serialized = serde_json::to_value(&input).unwrap();
|
||||
assert_eq!(
|
||||
serialized,
|
||||
json!({
|
||||
"type": "text",
|
||||
"value": "raw custom input"
|
||||
})
|
||||
);
|
||||
|
||||
let deserialized: LanguageModelToolUseInput = serde_json::from_value(serialized).unwrap();
|
||||
assert_eq!(deserialized, input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_language_model_tool_use_input_parse() {
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
struct TestInput {
|
||||
arg: String,
|
||||
}
|
||||
|
||||
let parsed: TestInput = LanguageModelToolUseInput::Json(json!({"arg": "value"}))
|
||||
.parse()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
parsed,
|
||||
TestInput {
|
||||
arg: "value".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
let error = LanguageModelToolUseInput::Text("raw custom input".to_string())
|
||||
.parse::<TestInput>()
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("custom tool text input cannot be parsed as JSON")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_language_model_tool_use_input_deserializes_legacy_plain_json_as_json() {
|
||||
use serde_json::json;
|
||||
|
||||
let deserialized: LanguageModelToolUseInput =
|
||||
serde_json::from_value(json!({"arg": "value"})).unwrap();
|
||||
assert_eq!(
|
||||
deserialized,
|
||||
LanguageModelToolUseInput::Json(json!({"arg": "value"}))
|
||||
);
|
||||
|
||||
let deserialized: LanguageModelToolUseInput =
|
||||
serde_json::from_value(json!("legacy string argument")).unwrap();
|
||||
assert_eq!(
|
||||
deserialized,
|
||||
LanguageModelToolUseInput::Json(json!("legacy string argument"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_language_model_tool_use_round_trip_with_signature() {
|
||||
use serde_json::json;
|
||||
@@ -663,7 +839,7 @@ mod tests {
|
||||
id: LanguageModelToolUseId::from("round_trip_id"),
|
||||
name: "round_trip_tool".into(),
|
||||
raw_input: json!({"key": "value"}).to_string(),
|
||||
input: json!({"key": "value"}),
|
||||
input: LanguageModelToolUseInput::Json(json!({"key": "value"})),
|
||||
is_input_complete: true,
|
||||
thought_signature: Some("round_trip_sig".to_string()),
|
||||
};
|
||||
@@ -684,7 +860,7 @@ mod tests {
|
||||
id: LanguageModelToolUseId::from("no_sig_id"),
|
||||
name: "no_sig_tool".into(),
|
||||
raw_input: json!({"arg": "value"}).to_string(),
|
||||
input: json!({"arg": "value"}),
|
||||
input: LanguageModelToolUseInput::Json(json!({"arg": "value"})),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,9 @@ use std::sync::Arc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::role::Role;
|
||||
use crate::{LanguageModelToolUse, LanguageModelToolUseId, SharedString};
|
||||
use crate::{
|
||||
LanguageModelToolUse, LanguageModelToolUseId, LanguageModelToolUseInput, SharedString,
|
||||
};
|
||||
|
||||
/// Dimensions of a `LanguageModelImage`
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
@@ -346,8 +348,52 @@ impl LanguageModelRequestMessage {
|
||||
pub struct LanguageModelRequestTool {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub input_schema: serde_json::Value,
|
||||
pub use_input_streaming: bool,
|
||||
pub input: LanguageModelRequestToolInput,
|
||||
}
|
||||
|
||||
impl LanguageModelRequestTool {
|
||||
pub fn function(
|
||||
name: String,
|
||||
description: String,
|
||||
input_schema: serde_json::Value,
|
||||
use_input_streaming: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
description,
|
||||
input: LanguageModelRequestToolInput::Function {
|
||||
input_schema,
|
||||
use_input_streaming,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Hash, Clone, Serialize, Deserialize)]
|
||||
pub enum LanguageModelRequestToolInput {
|
||||
Function {
|
||||
input_schema: serde_json::Value,
|
||||
use_input_streaming: bool,
|
||||
},
|
||||
Custom {
|
||||
format: Option<LanguageModelCustomToolFormat>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
|
||||
pub enum LanguageModelCustomToolFormat {
|
||||
Text,
|
||||
Grammar {
|
||||
syntax: LanguageModelCustomToolGrammarSyntax,
|
||||
definition: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum LanguageModelCustomToolGrammarSyntax {
|
||||
Lark,
|
||||
Regex,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Hash, Clone, Serialize, Deserialize)]
|
||||
@@ -389,6 +435,25 @@ pub struct LanguageModelRequest {
|
||||
pub compact_at_tokens: Option<u64>,
|
||||
}
|
||||
|
||||
impl LanguageModelRequest {
|
||||
pub fn contains_custom_tool_input(&self) -> bool {
|
||||
self.tools
|
||||
.iter()
|
||||
.any(|tool| matches!(tool.input, LanguageModelRequestToolInput::Custom { .. }))
|
||||
|| self.messages.iter().any(|message| {
|
||||
message.content.iter().any(|content| {
|
||||
matches!(
|
||||
content,
|
||||
MessageContent::ToolUse(LanguageModelToolUse {
|
||||
input: LanguageModelToolUseInput::Text(_),
|
||||
..
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Default, Debug, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema,
|
||||
)]
|
||||
|
||||
@@ -528,14 +528,17 @@ impl LanguageModel for AnthropicModel {
|
||||
> {
|
||||
let has_tools = !request.tools.is_empty();
|
||||
let request_id = self.model.request_id(has_tools).to_string();
|
||||
let mut request = into_anthropic(
|
||||
let mut request = match into_anthropic(
|
||||
request,
|
||||
request_id,
|
||||
self.model.default_temperature,
|
||||
self.model.max_output_tokens,
|
||||
self.model.mode.clone(),
|
||||
AnthropicPromptCacheMode::Automatic,
|
||||
);
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
if !self.model.supports_speed {
|
||||
request.speed = None;
|
||||
}
|
||||
|
||||
@@ -333,14 +333,17 @@ impl LanguageModel for AnthropicCompatibleLanguageModel {
|
||||
> {
|
||||
let has_tools = !request.tools.is_empty();
|
||||
let request_id = self.model.request_id(has_tools).to_string();
|
||||
let mut request = into_anthropic(
|
||||
let mut request = match into_anthropic(
|
||||
request,
|
||||
request_id,
|
||||
self.model.default_temperature,
|
||||
self.model.max_output_tokens,
|
||||
self.model.mode.clone(),
|
||||
self.cache_mode,
|
||||
);
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
if !self.model.supports_speed {
|
||||
request.speed = None;
|
||||
}
|
||||
|
||||
@@ -937,6 +937,13 @@ impl LanguageModel for BedrockModel {
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
if request.contains_custom_tool_input() {
|
||||
return async move {
|
||||
Err(anyhow::anyhow!("Bedrock does not support custom tools").into())
|
||||
}
|
||||
.boxed();
|
||||
}
|
||||
|
||||
let (region, allow_global, guardrail_identifier, guardrail_version) =
|
||||
cx.read_entity(&self.state, |state, _cx| {
|
||||
let (gid, gv) = state.get_guardrail_config();
|
||||
@@ -1477,7 +1484,7 @@ impl LanguageModel for BedrockMantleModel {
|
||||
}
|
||||
MantleProtocol::ChatCompletions => {
|
||||
let reasoning_effort = mantle_selected_reasoning_effort(&request, &self.model);
|
||||
let request = into_open_ai(
|
||||
let request = match into_open_ai(
|
||||
request,
|
||||
&model_id,
|
||||
self.model.supports_tools(),
|
||||
@@ -1486,7 +1493,10 @@ impl LanguageModel for BedrockMantleModel {
|
||||
ChatCompletionMaxTokensParameter::MaxCompletionTokens,
|
||||
reasoning_effort,
|
||||
false,
|
||||
);
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let completions = self.stream_completion(request, cx);
|
||||
async move {
|
||||
let mapper = OpenAiEventMapper::new();
|
||||
@@ -1526,6 +1536,10 @@ pub fn into_bedrock(
|
||||
guardrail_identifier: Option<String>,
|
||||
guardrail_version: Option<String>,
|
||||
) -> Result<bedrock::Request> {
|
||||
if request.contains_custom_tool_input() {
|
||||
anyhow::bail!("Bedrock does not support custom tools");
|
||||
}
|
||||
|
||||
let mut new_messages: Vec<BedrockMessage> = Vec::new();
|
||||
let mut system_message = String::new();
|
||||
|
||||
@@ -1588,12 +1602,19 @@ pub fn into_bedrock(
|
||||
}
|
||||
MessageContent::ToolUse(tool_use) => {
|
||||
messages_contain_tool_content = true;
|
||||
let input = if tool_use.input.is_null() {
|
||||
// Bedrock API requires valid JsonValue, not null, for tool use input
|
||||
value_to_aws_document(&serde_json::json!({}))
|
||||
} else {
|
||||
value_to_aws_document(&tool_use.input)
|
||||
};
|
||||
let input =
|
||||
if let language_model::LanguageModelToolUseInput::Json(input) =
|
||||
&tool_use.input
|
||||
{
|
||||
if input.is_null() {
|
||||
// Bedrock API requires valid JsonValue, not null, for tool use input
|
||||
value_to_aws_document(&serde_json::json!({}))
|
||||
} else {
|
||||
value_to_aws_document(input)
|
||||
}
|
||||
} else {
|
||||
value_to_aws_document(&serde_json::json!({}))
|
||||
};
|
||||
BedrockToolUseBlock::builder()
|
||||
.name(tool_use.name.to_string())
|
||||
.tool_use_id(tool_use.id.to_string())
|
||||
@@ -1727,19 +1748,25 @@ pub fn into_bedrock(
|
||||
request
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|tool| {
|
||||
Some(BedrockTool::ToolSpec(
|
||||
.map(|tool| {
|
||||
let language_model::LanguageModelRequestToolInput::Function {
|
||||
input_schema, ..
|
||||
} = &tool.input
|
||||
else {
|
||||
anyhow::bail!("Bedrock does not support custom tools");
|
||||
};
|
||||
Ok(BedrockTool::ToolSpec(
|
||||
BedrockToolSpec::builder()
|
||||
.name(tool.name.clone())
|
||||
.description(tool.description.clone())
|
||||
.input_schema(BedrockToolInputSchema::Json(value_to_aws_document(
|
||||
&tool.input_schema,
|
||||
input_schema,
|
||||
)))
|
||||
.build()
|
||||
.log_err()?,
|
||||
.context("failed to build Bedrock tool spec")?,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
.collect::<Result<_>>()?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
@@ -1894,7 +1921,10 @@ pub fn map_to_language_model_completion_events(
|
||||
name: tool_use.name.clone().into(),
|
||||
is_input_complete: false,
|
||||
raw_input: tool_use.input_json.clone(),
|
||||
input,
|
||||
input:
|
||||
language_model::LanguageModelToolUseInput::Json(
|
||||
input,
|
||||
),
|
||||
thought_signature: None,
|
||||
},
|
||||
)))
|
||||
@@ -1959,7 +1989,9 @@ pub fn map_to_language_model_completion_events(
|
||||
name: tool_use.name.into(),
|
||||
is_input_complete: true,
|
||||
raw_input: tool_use.input_json,
|
||||
input,
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
input,
|
||||
),
|
||||
thought_signature: None,
|
||||
},
|
||||
))
|
||||
|
||||
@@ -368,7 +368,7 @@ impl LanguageModel for CopilotChatLanguageModel {
|
||||
AnthropicModelMode::Default
|
||||
},
|
||||
AnthropicPromptCacheMode::Legacy,
|
||||
);
|
||||
)?;
|
||||
|
||||
anthropic_request.temperature = None;
|
||||
|
||||
@@ -423,7 +423,10 @@ impl LanguageModel for CopilotChatLanguageModel {
|
||||
|
||||
if self.model.supports_response() {
|
||||
let location = intent_to_chat_location(request.intent);
|
||||
let responses_request = into_copilot_responses(&self.model, request);
|
||||
let responses_request = match into_copilot_responses(&self.model, request) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let request_limiter = self.request_limiter.clone();
|
||||
let future = cx.spawn(async move |cx| {
|
||||
let request = CopilotChat::stream_response(
|
||||
@@ -567,7 +570,9 @@ pub fn map_to_language_model_completion_events(
|
||||
id: entry.id.clone().into(),
|
||||
name: entry.name.as_str().into(),
|
||||
is_input_complete: false,
|
||||
input,
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
input,
|
||||
),
|
||||
raw_input: entry.arguments.clone(),
|
||||
thought_signature: entry.thought_signature.clone(),
|
||||
},
|
||||
@@ -629,7 +634,10 @@ pub fn map_to_language_model_completion_events(
|
||||
id: tool_call.id.into(),
|
||||
name: tool_call.name.as_str().into(),
|
||||
is_input_complete: true,
|
||||
input,
|
||||
input:
|
||||
language_model::LanguageModelToolUseInput::Json(
|
||||
input,
|
||||
),
|
||||
raw_input: tool_call.arguments,
|
||||
thought_signature: tool_call.thought_signature,
|
||||
},
|
||||
@@ -734,7 +742,7 @@ impl CopilotResponsesEventMapper {
|
||||
id: call_id.into(),
|
||||
name: name.as_str().into(),
|
||||
is_input_complete: true,
|
||||
input,
|
||||
input: language_model::LanguageModelToolUseInput::Json(input),
|
||||
raw_input: arguments.clone(),
|
||||
thought_signature,
|
||||
},
|
||||
@@ -1054,12 +1062,15 @@ fn into_copilot_chat(
|
||||
let mut tool_calls = Vec::new();
|
||||
for content in &message.content {
|
||||
if let MessageContent::ToolUse(tool_use) = content {
|
||||
let input = tool_use.input.as_json().ok_or_else(|| {
|
||||
anyhow!("Copilot Chat does not support custom tool calls")
|
||||
})?;
|
||||
tool_calls.push(ToolCall {
|
||||
id: tool_use.id.to_string(),
|
||||
content: ToolCallContent::Function {
|
||||
function: FunctionContent {
|
||||
name: tool_use.name.to_string(),
|
||||
arguments: serde_json::to_string(&tool_use.input)?,
|
||||
arguments: serde_json::to_string(input)?,
|
||||
thought_signature: tool_use.thought_signature.clone(),
|
||||
},
|
||||
},
|
||||
@@ -1120,14 +1131,21 @@ fn into_copilot_chat(
|
||||
let tools = request
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| Tool::Function {
|
||||
function: Function {
|
||||
name: tool.name.clone(),
|
||||
description: tool.description.clone(),
|
||||
parameters: tool.input_schema.clone(),
|
||||
},
|
||||
.map(|tool| match &tool.input {
|
||||
language_model::LanguageModelRequestToolInput::Function { input_schema, .. } => {
|
||||
Ok(Tool::Function {
|
||||
function: Function {
|
||||
name: tool.name.clone(),
|
||||
description: tool.description.clone(),
|
||||
parameters: input_schema.clone(),
|
||||
},
|
||||
})
|
||||
}
|
||||
language_model::LanguageModelRequestToolInput::Custom { .. } => Err(anyhow::anyhow!(
|
||||
"Copilot Chat does not support custom tools"
|
||||
)),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
Ok(CopilotChatRequest {
|
||||
n: 1,
|
||||
@@ -1188,7 +1206,7 @@ fn intent_to_chat_location(intent: Option<CompletionIntent>) -> ChatLocation {
|
||||
fn into_copilot_responses(
|
||||
model: &CopilotChatModel,
|
||||
request: LanguageModelRequest,
|
||||
) -> copilot_responses::Request {
|
||||
) -> Result<copilot_responses::Request> {
|
||||
use copilot_responses as responses;
|
||||
|
||||
let LanguageModelRequest {
|
||||
@@ -1356,13 +1374,20 @@ fn into_copilot_responses(
|
||||
|
||||
let converted_tools: Vec<responses::ToolDefinition> = tools
|
||||
.into_iter()
|
||||
.map(|tool| responses::ToolDefinition::Function {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(tool.input_schema),
|
||||
strict: None,
|
||||
.map(|tool| match tool.input {
|
||||
language_model::LanguageModelRequestToolInput::Function { input_schema, .. } => {
|
||||
Ok(responses::ToolDefinition::Function {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(input_schema),
|
||||
strict: None,
|
||||
})
|
||||
}
|
||||
language_model::LanguageModelRequestToolInput::Custom { .. } => Err(anyhow::anyhow!(
|
||||
"Copilot Chat does not support custom tools"
|
||||
)),
|
||||
})
|
||||
.collect();
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
let mapped_tool_choice = tool_choice.map(|choice| match choice {
|
||||
LanguageModelToolChoice::Auto => responses::ToolChoice::Auto,
|
||||
@@ -1370,7 +1395,7 @@ fn into_copilot_responses(
|
||||
LanguageModelToolChoice::None => responses::ToolChoice::None,
|
||||
});
|
||||
|
||||
responses::Request {
|
||||
Ok(responses::Request {
|
||||
model: model.id().to_string(),
|
||||
input: input_items,
|
||||
stream: model.uses_streaming(),
|
||||
@@ -1393,7 +1418,7 @@ fn into_copilot_responses(
|
||||
copilot_responses::ResponseIncludable::ReasoningEncryptedContent,
|
||||
]),
|
||||
store: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1671,7 +1696,7 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let serialized = serde_json::to_value(into_copilot_responses(&model, request))
|
||||
let serialized = serde_json::to_value(into_copilot_responses(&model, request).unwrap())
|
||||
.expect("serialized request");
|
||||
let input = serialized["input"].as_array().expect("input items");
|
||||
|
||||
|
||||
@@ -338,7 +338,10 @@ impl LanguageModel for DeepSeekLanguageModel {
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let request = into_deepseek(request, &self.model, self.max_output_tokens());
|
||||
let request = match into_deepseek(request, &self.model, self.max_output_tokens()) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let stream = self.stream_completion(request, cx);
|
||||
|
||||
async move {
|
||||
@@ -353,7 +356,11 @@ pub fn into_deepseek(
|
||||
request: LanguageModelRequest,
|
||||
model: &deepseek::Model,
|
||||
max_output_tokens: Option<u64>,
|
||||
) -> deepseek::Request {
|
||||
) -> Result<deepseek::Request> {
|
||||
if request.contains_custom_tool_input() {
|
||||
anyhow::bail!("DeepSeek does not support custom tools");
|
||||
}
|
||||
|
||||
let thinking = deepseek_thinking(model, request.thinking_allowed);
|
||||
let thinking_enabled = thinking
|
||||
.as_ref()
|
||||
@@ -392,13 +399,16 @@ pub fn into_deepseek(
|
||||
MessageContent::Image(_) => {}
|
||||
MessageContent::Compaction(_) => {}
|
||||
MessageContent::ToolUse(tool_use) => {
|
||||
let input = tool_use
|
||||
.input
|
||||
.as_json()
|
||||
.ok_or_else(|| anyhow!("DeepSeek does not support custom tool calls"))?;
|
||||
let tool_call = deepseek::ToolCall {
|
||||
id: tool_use.id.to_string(),
|
||||
content: deepseek::ToolCallContent::Function {
|
||||
function: deepseek::FunctionContent {
|
||||
name: tool_use.name.to_string(),
|
||||
arguments: serde_json::to_string(&tool_use.input)
|
||||
.unwrap_or_default(),
|
||||
arguments: serde_json::to_string(input).unwrap_or_default(),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -441,7 +451,7 @@ pub fn into_deepseek(
|
||||
}
|
||||
}
|
||||
|
||||
deepseek::Request {
|
||||
Ok(deepseek::Request {
|
||||
model: model.id().to_string(),
|
||||
messages,
|
||||
stream: true,
|
||||
@@ -466,15 +476,26 @@ pub fn into_deepseek(
|
||||
tools: request
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| deepseek::ToolDefinition::Function {
|
||||
function: deepseek::FunctionDefinition {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(tool.input_schema),
|
||||
},
|
||||
.map(|tool| {
|
||||
let input_schema = match tool.input {
|
||||
language_model::LanguageModelRequestToolInput::Function {
|
||||
input_schema,
|
||||
..
|
||||
} => input_schema,
|
||||
language_model::LanguageModelRequestToolInput::Custom { .. } => {
|
||||
return Err(anyhow::anyhow!("DeepSeek does not support custom tools"));
|
||||
}
|
||||
};
|
||||
Ok(deepseek::ToolDefinition::Function {
|
||||
function: deepseek::FunctionDefinition {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(input_schema),
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
.collect::<Result<_>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn deepseek_thinking(
|
||||
@@ -578,7 +599,7 @@ impl DeepSeekEventMapper {
|
||||
id: entry.id.clone().into(),
|
||||
name: entry.name.as_str().into(),
|
||||
is_input_complete: false,
|
||||
input,
|
||||
input: language_model::LanguageModelToolUseInput::Json(input),
|
||||
raw_input: entry.arguments.clone(),
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -609,7 +630,7 @@ impl DeepSeekEventMapper {
|
||||
id: tool_call.id.clone().into(),
|
||||
name: tool_call.name.as_str().into(),
|
||||
is_input_complete: true,
|
||||
input,
|
||||
input: language_model::LanguageModelToolUseInput::Json(input),
|
||||
raw_input: tool_call.arguments.clone(),
|
||||
thought_signature: None,
|
||||
},
|
||||
|
||||
@@ -358,11 +358,14 @@ impl LanguageModel for GoogleLanguageModel {
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let request = into_google(
|
||||
let request = match into_google(
|
||||
request,
|
||||
self.model.request_id().to_string(),
|
||||
self.model.mode(),
|
||||
);
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let request = self.stream_completion(request, cx);
|
||||
let future = self.request_limiter.stream(async move {
|
||||
let response = request.await.map_err(LanguageModelCompletionError::from)?;
|
||||
|
||||
@@ -650,7 +650,7 @@ impl LlamaCppLanguageModel {
|
||||
fn to_llama_cpp_request(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> llama_cpp::ChatCompletionRequest {
|
||||
) -> Result<llama_cpp::ChatCompletionRequest> {
|
||||
build_llama_cpp_request(
|
||||
&self.name,
|
||||
self.supports_images,
|
||||
@@ -697,7 +697,11 @@ fn build_llama_cpp_request(
|
||||
supports_images: bool,
|
||||
capabilities: LiveCapabilities,
|
||||
request: LanguageModelRequest,
|
||||
) -> llama_cpp::ChatCompletionRequest {
|
||||
) -> Result<llama_cpp::ChatCompletionRequest> {
|
||||
if request.contains_custom_tool_input() {
|
||||
anyhow::bail!("llama.cpp does not support custom tools");
|
||||
}
|
||||
|
||||
let supports_tools = capabilities.supports_tools;
|
||||
let supports_thinking = capabilities.supports_thinking;
|
||||
let mut messages = Vec::new();
|
||||
@@ -743,13 +747,15 @@ fn build_llama_cpp_request(
|
||||
}
|
||||
}
|
||||
MessageContent::ToolUse(tool_use) => {
|
||||
let input = tool_use.input.as_json().ok_or_else(|| {
|
||||
anyhow::anyhow!("llama.cpp does not support custom tool calls")
|
||||
})?;
|
||||
let tool_call = llama_cpp::ToolCall {
|
||||
id: tool_use.id.to_string(),
|
||||
content: llama_cpp::ToolCallContent::Function {
|
||||
function: llama_cpp::FunctionContent {
|
||||
name: tool_use.name.to_string(),
|
||||
arguments: serde_json::to_string(&tool_use.input)
|
||||
.unwrap_or_default(),
|
||||
arguments: serde_json::to_string(input).unwrap_or_default(),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -811,14 +817,25 @@ fn build_llama_cpp_request(
|
||||
request
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| llama_cpp::ToolDefinition::Function {
|
||||
function: llama_cpp::FunctionDefinition {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(tool.input_schema),
|
||||
},
|
||||
.map(|tool| {
|
||||
let input_schema = match tool.input {
|
||||
language_model::LanguageModelRequestToolInput::Function {
|
||||
input_schema,
|
||||
..
|
||||
} => input_schema,
|
||||
language_model::LanguageModelRequestToolInput::Custom { .. } => {
|
||||
return Err(anyhow::anyhow!("llama.cpp does not support custom tools"));
|
||||
}
|
||||
};
|
||||
Ok(llama_cpp::ToolDefinition::Function {
|
||||
function: llama_cpp::FunctionDefinition {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(input_schema),
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
.collect::<Result<_>>()?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
@@ -834,7 +851,7 @@ fn build_llama_cpp_request(
|
||||
})
|
||||
};
|
||||
|
||||
llama_cpp::ChatCompletionRequest {
|
||||
Ok(llama_cpp::ChatCompletionRequest {
|
||||
model: model_name.to_string(),
|
||||
messages,
|
||||
stream: true,
|
||||
@@ -853,7 +870,7 @@ fn build_llama_cpp_request(
|
||||
stream_options: Some(llama_cpp::StreamOptions {
|
||||
include_usage: true,
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl LanguageModel for LlamaCppLanguageModel {
|
||||
@@ -919,7 +936,10 @@ impl LanguageModel for LlamaCppLanguageModel {
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let request = self.to_llama_cpp_request(request);
|
||||
let request = match self.to_llama_cpp_request(request) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let completions = self.stream_completion(request, cx);
|
||||
async move {
|
||||
let mapper = LlamaCppEventMapper::new();
|
||||
@@ -1019,7 +1039,9 @@ impl LlamaCppEventMapper {
|
||||
id: tool_call.id.into(),
|
||||
name: tool_call.name.into(),
|
||||
is_input_complete: true,
|
||||
input,
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
input,
|
||||
),
|
||||
raw_input: tool_call.arguments,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -1829,7 +1851,8 @@ mod tests {
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(request.messages.len(), 1);
|
||||
match &request.messages[0] {
|
||||
@@ -1872,7 +1895,8 @@ mod tests {
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(request.messages.len(), 1);
|
||||
match &request.messages[0] {
|
||||
@@ -1882,7 +1906,7 @@ mod tests {
|
||||
tool_calls,
|
||||
} => {
|
||||
assert_eq!(content, "answer");
|
||||
assert_eq!(reasoning_content, &None);
|
||||
assert!(reasoning_content.is_none());
|
||||
assert!(tool_calls.is_empty());
|
||||
}
|
||||
message => panic!("unexpected message: {message:?}"),
|
||||
@@ -1911,7 +1935,9 @@ mod tests {
|
||||
id: "call_1".into(),
|
||||
name: "weather".into(),
|
||||
raw_input: r#"{"city":"Oslo"}"#.to_string(),
|
||||
input: serde_json::json!({ "city": "Oslo" }),
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
serde_json::json!({ "city": "Oslo" }),
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
}),
|
||||
@@ -1921,7 +1947,8 @@ mod tests {
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(request.messages.len(), 1);
|
||||
match &request.messages[0] {
|
||||
|
||||
@@ -338,7 +338,11 @@ impl LmStudioLanguageModel {
|
||||
fn to_lmstudio_request(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
) -> lmstudio::ChatCompletionRequest {
|
||||
) -> Result<lmstudio::ChatCompletionRequest> {
|
||||
if request.contains_custom_tool_input() {
|
||||
anyhow::bail!("LM Studio does not support custom tools");
|
||||
}
|
||||
|
||||
let mut messages = Vec::new();
|
||||
|
||||
for message in request.messages {
|
||||
@@ -365,13 +369,15 @@ impl LmStudioLanguageModel {
|
||||
);
|
||||
}
|
||||
MessageContent::ToolUse(tool_use) => {
|
||||
let input = tool_use.input.as_json().ok_or_else(|| {
|
||||
anyhow!("LM Studio does not support custom tool calls")
|
||||
})?;
|
||||
let tool_call = lmstudio::ToolCall {
|
||||
id: tool_use.id.to_string(),
|
||||
content: lmstudio::ToolCallContent::Function {
|
||||
function: lmstudio::FunctionContent {
|
||||
name: tool_use.name.to_string(),
|
||||
arguments: serde_json::to_string(&tool_use.input)
|
||||
.unwrap_or_default(),
|
||||
arguments: serde_json::to_string(input).unwrap_or_default(),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -417,7 +423,7 @@ impl LmStudioLanguageModel {
|
||||
}
|
||||
}
|
||||
|
||||
lmstudio::ChatCompletionRequest {
|
||||
Ok(lmstudio::ChatCompletionRequest {
|
||||
model: self.model.name.clone(),
|
||||
messages,
|
||||
stream: true,
|
||||
@@ -430,20 +436,31 @@ impl LmStudioLanguageModel {
|
||||
tools: request
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| lmstudio::ToolDefinition::Function {
|
||||
function: lmstudio::FunctionDefinition {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(tool.input_schema),
|
||||
},
|
||||
.map(|tool| {
|
||||
let input_schema = match tool.input {
|
||||
language_model::LanguageModelRequestToolInput::Function {
|
||||
input_schema,
|
||||
..
|
||||
} => input_schema,
|
||||
language_model::LanguageModelRequestToolInput::Custom { .. } => {
|
||||
return Err(anyhow::anyhow!("LM Studio does not support custom tools"));
|
||||
}
|
||||
};
|
||||
Ok(lmstudio::ToolDefinition::Function {
|
||||
function: lmstudio::FunctionDefinition {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(input_schema),
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
.collect::<Result<_>>()?,
|
||||
tool_choice: request.tool_choice.map(|choice| match choice {
|
||||
LanguageModelToolChoice::Auto => lmstudio::ToolChoice::Auto,
|
||||
LanguageModelToolChoice::Any => lmstudio::ToolChoice::Required,
|
||||
LanguageModelToolChoice::None => lmstudio::ToolChoice::None,
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn stream_completion(
|
||||
@@ -533,7 +550,10 @@ impl LanguageModel for LmStudioLanguageModel {
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let request = self.to_lmstudio_request(request);
|
||||
let request = match self.to_lmstudio_request(request) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let completions = self.stream_completion(request, cx);
|
||||
async move {
|
||||
let mapper = LmStudioEventMapper::new();
|
||||
@@ -637,7 +657,7 @@ impl LmStudioEventMapper {
|
||||
id: tool_call.id.into(),
|
||||
name: tool_call.name.into(),
|
||||
is_input_complete: true,
|
||||
input,
|
||||
input: language_model::LanguageModelToolUseInput::Json(input),
|
||||
raw_input: tool_call.arguments,
|
||||
thought_signature: None,
|
||||
},
|
||||
|
||||
@@ -343,7 +343,10 @@ impl LanguageModel for MistralLanguageModel {
|
||||
>,
|
||||
> {
|
||||
let (request, affinity) =
|
||||
into_mistral(request, self.model.clone(), self.max_output_tokens());
|
||||
match into_mistral(request, self.model.clone(), self.max_output_tokens()) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let stream = self.stream_completion(request, affinity, cx);
|
||||
|
||||
async move {
|
||||
@@ -359,7 +362,11 @@ pub fn into_mistral(
|
||||
request: LanguageModelRequest,
|
||||
model: mistral::Model,
|
||||
max_output_tokens: Option<u64>,
|
||||
) -> (mistral::Request, Option<String>) {
|
||||
) -> Result<(mistral::Request, Option<String>)> {
|
||||
if request.contains_custom_tool_input() {
|
||||
anyhow::bail!("Mistral does not support custom tools");
|
||||
}
|
||||
|
||||
let stream = true;
|
||||
|
||||
let mut messages = Vec::new();
|
||||
@@ -452,13 +459,15 @@ pub fn into_mistral(
|
||||
MessageContent::Image(_) => {}
|
||||
MessageContent::Compaction(_) => {}
|
||||
MessageContent::ToolUse(tool_use) => {
|
||||
let input = tool_use.input.as_json().ok_or_else(|| {
|
||||
anyhow!("Mistral does not support custom tool calls")
|
||||
})?;
|
||||
let tool_call = mistral::ToolCall {
|
||||
id: tool_use.id.to_string(),
|
||||
content: mistral::ToolCallContent::Function {
|
||||
function: mistral::FunctionContent {
|
||||
name: tool_use.name.to_string(),
|
||||
arguments: serde_json::to_string(&tool_use.input)
|
||||
.unwrap_or_default(),
|
||||
arguments: serde_json::to_string(input).unwrap_or_default(),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -516,7 +525,7 @@ pub fn into_mistral(
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
Ok((
|
||||
mistral::Request {
|
||||
model: model.id().to_string(),
|
||||
messages,
|
||||
@@ -550,17 +559,28 @@ pub fn into_mistral(
|
||||
tools: request
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| mistral::ToolDefinition::Function {
|
||||
function: mistral::FunctionDefinition {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(tool.input_schema),
|
||||
},
|
||||
.map(|tool| {
|
||||
let input_schema = match tool.input {
|
||||
language_model::LanguageModelRequestToolInput::Function {
|
||||
input_schema,
|
||||
..
|
||||
} => input_schema,
|
||||
language_model::LanguageModelRequestToolInput::Custom { .. } => {
|
||||
return Err(anyhow::anyhow!("Mistral does not support custom tools"));
|
||||
}
|
||||
};
|
||||
Ok(mistral::ToolDefinition::Function {
|
||||
function: mistral::FunctionDefinition {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(input_schema),
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
.collect::<Result<_>>()?,
|
||||
},
|
||||
request.thread_id,
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
pub struct MistralEventMapper {
|
||||
@@ -664,7 +684,7 @@ impl MistralEventMapper {
|
||||
id: entry.id.clone().into(),
|
||||
name: entry.name.as_str().into(),
|
||||
is_input_complete: false,
|
||||
input,
|
||||
input: language_model::LanguageModelToolUseInput::Json(input),
|
||||
raw_input: entry.arguments.clone(),
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -721,7 +741,7 @@ impl MistralEventMapper {
|
||||
id: tool_call.id.into(),
|
||||
name: tool_call.name.into(),
|
||||
is_input_complete: true,
|
||||
input,
|
||||
input: language_model::LanguageModelToolUseInput::Json(input),
|
||||
raw_input: tool_call.arguments,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -813,7 +833,10 @@ mod tests {
|
||||
|
||||
assert_eq!(tool_use.id.to_string(), "real_id_123");
|
||||
assert_eq!(tool_use.name.as_ref(), "read_file");
|
||||
assert_eq!(tool_use.input, serde_json::json!({"path": "a.txt"}));
|
||||
assert_eq!(
|
||||
tool_use.input,
|
||||
language_model::LanguageModelToolUseInput::Json(serde_json::json!({"path": "a.txt"}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -854,7 +877,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (mistral_request, affinity) =
|
||||
into_mistral(request, mistral::Model::MistralSmallLatest, None);
|
||||
into_mistral(request, mistral::Model::MistralSmallLatest, None).unwrap();
|
||||
|
||||
assert_eq!(mistral_request.model, "mistral-small-latest");
|
||||
assert_eq!(mistral_request.temperature, Some(0.5));
|
||||
@@ -890,7 +913,8 @@ mod tests {
|
||||
compact_at_tokens: None,
|
||||
};
|
||||
|
||||
let (mistral_request, _) = into_mistral(request, mistral::Model::MistralSmallLatest, None);
|
||||
let (mistral_request, _) =
|
||||
into_mistral(request, mistral::Model::MistralSmallLatest, None).unwrap();
|
||||
|
||||
assert_eq!(mistral_request.messages.len(), 1);
|
||||
assert!(matches!(
|
||||
|
||||
@@ -360,7 +360,11 @@ pub struct OllamaLanguageModel {
|
||||
}
|
||||
|
||||
impl OllamaLanguageModel {
|
||||
fn to_ollama_request(&self, request: LanguageModelRequest) -> ChatRequest {
|
||||
fn to_ollama_request(&self, request: LanguageModelRequest) -> Result<ChatRequest> {
|
||||
if request.contains_custom_tool_input() {
|
||||
anyhow::bail!("Ollama does not support custom tools");
|
||||
}
|
||||
|
||||
let supports_vision = self.model.supports_vision.unwrap_or(false);
|
||||
|
||||
let mut messages = Vec::with_capacity(request.messages.len());
|
||||
@@ -422,7 +426,16 @@ impl OllamaLanguageModel {
|
||||
id: tool_use.id.to_string(),
|
||||
function: OllamaFunctionCall {
|
||||
name: tool_use.name.to_string(),
|
||||
arguments: tool_use.input,
|
||||
arguments: match tool_use.input {
|
||||
language_model::LanguageModelToolUseInput::Json(
|
||||
input,
|
||||
) => input,
|
||||
language_model::LanguageModelToolUseInput::Text(_) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Ollama does not support custom tool calls"
|
||||
));
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -445,7 +458,7 @@ impl OllamaLanguageModel {
|
||||
}),
|
||||
}
|
||||
}
|
||||
ChatRequest {
|
||||
Ok(ChatRequest {
|
||||
model: self.model.name.clone(),
|
||||
messages,
|
||||
keep_alive: self.model.keep_alive.clone().unwrap_or_default(),
|
||||
@@ -468,11 +481,15 @@ impl OllamaLanguageModel {
|
||||
.supports_thinking
|
||||
.map(|supports_thinking| supports_thinking && request.thinking_allowed),
|
||||
tools: if self.model.supports_tools.unwrap_or(false) {
|
||||
request.tools.into_iter().map(tool_into_ollama).collect()
|
||||
request
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(tool_into_ollama)
|
||||
.collect::<Result<_>>()?
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,7 +553,10 @@ impl LanguageModel for OllamaLanguageModel {
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let request = self.to_ollama_request(request);
|
||||
let request = match self.to_ollama_request(request) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
|
||||
let http_client = self.http_client.clone();
|
||||
let (api_key, api_url, extra_headers) = self.state.read_with(cx, |state, cx| {
|
||||
@@ -621,7 +641,9 @@ fn map_to_language_model_completion_events(
|
||||
id: LanguageModelToolUseId::from(id),
|
||||
name: Arc::from(function.name),
|
||||
raw_input: function.arguments.to_string(),
|
||||
input: function.arguments,
|
||||
input: language_model::LanguageModelToolUseInput::Json(
|
||||
function.arguments,
|
||||
),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
});
|
||||
@@ -1130,14 +1152,22 @@ fn merge_settings_into_models(
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_into_ollama(tool: LanguageModelRequestTool) -> ollama::OllamaTool {
|
||||
ollama::OllamaTool::Function {
|
||||
fn tool_into_ollama(tool: LanguageModelRequestTool) -> Result<ollama::OllamaTool> {
|
||||
let input_schema = match tool.input {
|
||||
language_model::LanguageModelRequestToolInput::Function { input_schema, .. } => {
|
||||
input_schema
|
||||
}
|
||||
language_model::LanguageModelRequestToolInput::Custom { .. } => {
|
||||
anyhow::bail!("Ollama does not support custom tools");
|
||||
}
|
||||
};
|
||||
Ok(ollama::OllamaTool::Function {
|
||||
function: OllamaFunctionTool {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(tool.input_schema),
|
||||
parameters: Some(input_schema),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -553,7 +553,7 @@ impl LanguageModel for OpenAiLanguageModel {
|
||||
}
|
||||
.boxed()
|
||||
} else {
|
||||
let request = into_open_ai(
|
||||
let request = match into_open_ai(
|
||||
request,
|
||||
self.model.id(),
|
||||
self.model.supports_parallel_tool_calls(),
|
||||
@@ -562,7 +562,10 @@ impl LanguageModel for OpenAiLanguageModel {
|
||||
ChatCompletionMaxTokensParameter::MaxCompletionTokens,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let completions = self.stream_completion(request, cx);
|
||||
async move {
|
||||
let mapper = OpenAiEventMapper::new();
|
||||
|
||||
@@ -424,7 +424,7 @@ impl LanguageModel for OpenAiCompatibleLanguageModel {
|
||||
|
||||
if self.model.capabilities.chat_completions {
|
||||
let reasoning_effort = chat_completion_reasoning_effort(&request, &self.model);
|
||||
let request = into_open_ai(
|
||||
let request = match into_open_ai(
|
||||
request,
|
||||
&self.model.name,
|
||||
self.model.capabilities.parallel_tool_calls,
|
||||
@@ -433,7 +433,10 @@ impl LanguageModel for OpenAiCompatibleLanguageModel {
|
||||
chat_completion_max_tokens_parameter(&self.model),
|
||||
reasoning_effort,
|
||||
self.model.capabilities.interleaved_reasoning,
|
||||
);
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let completions = self.stream_completion(request, cx);
|
||||
async move {
|
||||
let mapper = OpenAiEventMapper::new();
|
||||
@@ -688,7 +691,8 @@ mod tests {
|
||||
chat_completion_max_tokens_parameter(&model),
|
||||
reasoning_effort,
|
||||
model.capabilities.interleaved_reasoning,
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
let serialized = serde_json::to_value(request).unwrap();
|
||||
|
||||
assert_eq!(serialized["reasoning_effort"], json!("high"));
|
||||
|
||||
@@ -400,7 +400,11 @@ impl LanguageModel for OpenRouterLanguageModel {
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let openrouter_request = into_open_router(request, &self.model, self.max_output_tokens());
|
||||
let openrouter_request =
|
||||
match into_open_router(request, &self.model, self.max_output_tokens()) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let request = self.stream_completion(openrouter_request, cx);
|
||||
let future = self.request_limiter.stream(async move {
|
||||
let response = request.await?;
|
||||
@@ -414,7 +418,11 @@ pub fn into_open_router(
|
||||
request: LanguageModelRequest,
|
||||
model: &Model,
|
||||
max_output_tokens: Option<u64>,
|
||||
) -> open_router::Request {
|
||||
) -> Result<open_router::Request> {
|
||||
if request.contains_custom_tool_input() {
|
||||
anyhow::bail!("OpenRouter does not support custom tools");
|
||||
}
|
||||
|
||||
// Anthropic models via OpenRouter don't accept reasoning_details being echoed back
|
||||
// in requests - it's an output-only field for them. However, Gemini models require
|
||||
// the thought signatures to be echoed back for proper reasoning chain continuity.
|
||||
@@ -472,13 +480,15 @@ pub fn into_open_router(
|
||||
message_added_content = true;
|
||||
}
|
||||
MessageContent::ToolUse(tool_use) => {
|
||||
let input = tool_use.input.as_json().ok_or_else(|| {
|
||||
anyhow::anyhow!("OpenRouter does not support custom tool calls")
|
||||
})?;
|
||||
let tool_call = open_router::ToolCall {
|
||||
id: tool_use.id.to_string(),
|
||||
content: open_router::ToolCallContent::Function {
|
||||
function: open_router::FunctionContent {
|
||||
name: tool_use.name.to_string(),
|
||||
arguments: serde_json::to_string(&tool_use.input)
|
||||
.unwrap_or_default(),
|
||||
arguments: serde_json::to_string(input).unwrap_or_default(),
|
||||
thought_signature: tool_use.thought_signature.clone(),
|
||||
},
|
||||
},
|
||||
@@ -551,7 +561,7 @@ pub fn into_open_router(
|
||||
}
|
||||
}
|
||||
|
||||
open_router::Request {
|
||||
Ok(open_router::Request {
|
||||
model: model.id().into(),
|
||||
messages,
|
||||
stream: true,
|
||||
@@ -580,21 +590,32 @@ pub fn into_open_router(
|
||||
tools: request
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| open_router::ToolDefinition::Function {
|
||||
function: open_router::FunctionDefinition {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(tool.input_schema),
|
||||
},
|
||||
.map(|tool| {
|
||||
let input_schema = match tool.input {
|
||||
language_model::LanguageModelRequestToolInput::Function {
|
||||
input_schema,
|
||||
..
|
||||
} => input_schema,
|
||||
language_model::LanguageModelRequestToolInput::Custom { .. } => {
|
||||
return Err(anyhow::anyhow!("OpenRouter does not support custom tools"));
|
||||
}
|
||||
};
|
||||
Ok(open_router::ToolDefinition::Function {
|
||||
function: open_router::FunctionDefinition {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(input_schema),
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
.collect::<Result<_>>()?,
|
||||
tool_choice: request.tool_choice.map(|choice| match choice {
|
||||
LanguageModelToolChoice::Auto => open_router::ToolChoice::Auto,
|
||||
LanguageModelToolChoice::Any => open_router::ToolChoice::Required,
|
||||
LanguageModelToolChoice::None => open_router::ToolChoice::None,
|
||||
}),
|
||||
provider: model.provider.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn open_router_session_id(thread_id: Option<String>) -> Option<String> {
|
||||
@@ -809,7 +830,7 @@ impl OpenRouterEventMapper {
|
||||
id: entry.id.clone().into(),
|
||||
name: entry.name.as_str().into(),
|
||||
is_input_complete: false,
|
||||
input,
|
||||
input: language_model::LanguageModelToolUseInput::Json(input),
|
||||
raw_input: entry.arguments.clone(),
|
||||
thought_signature: entry.thought_signature.clone(),
|
||||
},
|
||||
@@ -832,7 +853,7 @@ impl OpenRouterEventMapper {
|
||||
id: tool_call.id.clone().into(),
|
||||
name: tool_call.name.as_str().into(),
|
||||
is_input_complete: true,
|
||||
input,
|
||||
input: language_model::LanguageModelToolUseInput::Json(input),
|
||||
raw_input: tool_call.arguments.clone(),
|
||||
thought_signature: tool_call.thought_signature.clone(),
|
||||
},
|
||||
@@ -1114,7 +1135,7 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = into_open_router(request, &model, None);
|
||||
let result = into_open_router(request, &model, None).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result.session_id.as_deref(),
|
||||
@@ -1216,7 +1237,7 @@ mod tests {
|
||||
compact_at_tokens: None,
|
||||
};
|
||||
|
||||
let result = into_open_router(request, &model, None);
|
||||
let result = into_open_router(request, &model, None).unwrap();
|
||||
|
||||
let system_cache = result.messages.iter().find_map(|m| {
|
||||
if let open_router::RequestMessage::System { content } = m {
|
||||
@@ -1355,7 +1376,7 @@ mod tests {
|
||||
compact_at_tokens: None,
|
||||
};
|
||||
|
||||
let result = into_open_router(request, &model, None);
|
||||
let result = into_open_router(request, &model, None).unwrap();
|
||||
|
||||
for message in &result.messages {
|
||||
let content = match message {
|
||||
@@ -1418,7 +1439,7 @@ mod tests {
|
||||
compact_at_tokens: None,
|
||||
};
|
||||
|
||||
let result = into_open_router(request, &model, None);
|
||||
let result = into_open_router(request, &model, None).unwrap();
|
||||
|
||||
for message in &result.messages {
|
||||
let content = match message {
|
||||
|
||||
@@ -669,7 +669,7 @@ impl LanguageModel for OpenCodeLanguageModel {
|
||||
} else {
|
||||
anthropic::AnthropicModelMode::Default
|
||||
};
|
||||
let anthropic_request = into_anthropic(
|
||||
let anthropic_request = match into_anthropic(
|
||||
request,
|
||||
self.model.id().to_string(),
|
||||
1.0,
|
||||
@@ -678,7 +678,10 @@ impl LanguageModel for OpenCodeLanguageModel {
|
||||
.unwrap_or(8192),
|
||||
mode,
|
||||
anthropic::completion::AnthropicPromptCacheMode::Automatic,
|
||||
);
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let stream =
|
||||
self.stream_anthropic(anthropic_request, http_client, extra_headers, cx);
|
||||
async move {
|
||||
@@ -696,7 +699,7 @@ impl LanguageModel for OpenCodeLanguageModel {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let openai_request = into_open_ai(
|
||||
let openai_request = match into_open_ai(
|
||||
request,
|
||||
self.model.id(),
|
||||
true,
|
||||
@@ -705,7 +708,10 @@ impl LanguageModel for OpenCodeLanguageModel {
|
||||
ChatCompletionMaxTokensParameter::MaxCompletionTokens,
|
||||
reasoning_effort,
|
||||
self.model.interleaved_reasoning(),
|
||||
);
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let stream =
|
||||
self.stream_openai_chat(openai_request, http_client, extra_headers, cx);
|
||||
async move {
|
||||
@@ -744,7 +750,10 @@ impl LanguageModel for OpenCodeLanguageModel {
|
||||
} else {
|
||||
google_ai::GoogleModelMode::Default
|
||||
};
|
||||
let google_request = into_google(request, self.model.id().to_string(), mode);
|
||||
let google_request = match into_google(request, self.model.id().to_string(), mode) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let stream = self.stream_google(google_request, http_client, extra_headers, cx);
|
||||
async move {
|
||||
let mapper = GoogleEventMapper::new();
|
||||
|
||||
@@ -451,7 +451,7 @@ impl LanguageModel for VercelAiGatewayLanguageModel {
|
||||
LanguageModelCompletionError,
|
||||
>,
|
||||
> {
|
||||
let request = crate::provider::open_ai::into_open_ai(
|
||||
let request = match crate::provider::open_ai::into_open_ai(
|
||||
request,
|
||||
&self.model.name,
|
||||
self.model.capabilities.parallel_tool_calls,
|
||||
@@ -460,7 +460,10 @@ impl LanguageModel for VercelAiGatewayLanguageModel {
|
||||
crate::provider::open_ai::ChatCompletionMaxTokensParameter::MaxCompletionTokens,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let completions = self.stream_open_ai(request, cx);
|
||||
async move {
|
||||
let mapper = crate::provider::open_ai::OpenAiEventMapper::new();
|
||||
|
||||
@@ -413,7 +413,7 @@ impl LanguageModel for XAiLanguageModel {
|
||||
>,
|
||||
> {
|
||||
let reasoning_effort = reasoning_effort_for_request(&request, &self.model);
|
||||
let request = crate::provider::open_ai::into_open_ai(
|
||||
let request = match crate::provider::open_ai::into_open_ai(
|
||||
request,
|
||||
self.model.id(),
|
||||
self.model.supports_parallel_tool_calls(),
|
||||
@@ -422,7 +422,10 @@ impl LanguageModel for XAiLanguageModel {
|
||||
crate::provider::open_ai::ChatCompletionMaxTokensParameter::MaxCompletionTokens,
|
||||
reasoning_effort,
|
||||
false,
|
||||
);
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let completions = self.stream_completion(request, cx);
|
||||
async move {
|
||||
let mapper = crate::provider::open_ai::OpenAiEventMapper::new();
|
||||
|
||||
@@ -462,7 +462,7 @@ impl<TP: CloudLlmTokenProvider + 'static> LanguageModel for CloudLanguageModel<T
|
||||
.as_ref()
|
||||
.and_then(|effort| anthropic::Effort::from_str(effort).ok());
|
||||
|
||||
let mut request = into_anthropic(
|
||||
let mut request = match into_anthropic(
|
||||
request,
|
||||
self.model.id.to_string(),
|
||||
1.0,
|
||||
@@ -475,7 +475,10 @@ impl<TP: CloudLlmTokenProvider + 'static> LanguageModel for CloudLanguageModel<T
|
||||
AnthropicModelMode::Default
|
||||
},
|
||||
AnthropicPromptCacheMode::Automatic,
|
||||
);
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
|
||||
if enable_thinking && effort.is_some() {
|
||||
request.thinking = Some(anthropic::Thinking::Adaptive {
|
||||
@@ -592,7 +595,7 @@ impl<TP: CloudLlmTokenProvider + 'static> LanguageModel for CloudLanguageModel<T
|
||||
cloud_llm_client::LanguageModelProvider::XAi => {
|
||||
let http_client = self.http_client.clone();
|
||||
let token_provider = self.token_provider.clone();
|
||||
let request = into_open_ai(
|
||||
let request = match into_open_ai(
|
||||
request,
|
||||
&self.model.id.0,
|
||||
self.model.supports_parallel_tool_calls,
|
||||
@@ -601,7 +604,10 @@ impl<TP: CloudLlmTokenProvider + 'static> LanguageModel for CloudLanguageModel<T
|
||||
ChatCompletionMaxTokensParameter::MaxCompletionTokens,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
) {
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let auth_context = token_provider.auth_context(cx);
|
||||
let future = self.request_limiter.stream(async move {
|
||||
let PerformLlmCompletionResponse {
|
||||
@@ -640,7 +646,11 @@ impl<TP: CloudLlmTokenProvider + 'static> LanguageModel for CloudLanguageModel<T
|
||||
let http_client = self.http_client.clone();
|
||||
let token_provider = self.token_provider.clone();
|
||||
let request =
|
||||
into_google(request, self.model.id.to_string(), GoogleModelMode::Default);
|
||||
match into_google(request, self.model.id.to_string(), GoogleModelMode::Default)
|
||||
{
|
||||
Ok(request) => request,
|
||||
Err(error) => return async move { Err(error.into()) }.boxed(),
|
||||
};
|
||||
let auth_context = token_provider.auth_context(cx);
|
||||
let future = self.request_limiter.stream(async move {
|
||||
let PerformLlmCompletionResponse {
|
||||
|
||||
@@ -3,16 +3,19 @@ use collections::HashMap;
|
||||
use futures::{Stream, StreamExt};
|
||||
use language_model_core::{
|
||||
CompactionContent, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelImage, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelToolChoice,
|
||||
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, MessageContent,
|
||||
Role, StopReason, TokenUsage,
|
||||
LanguageModelCustomToolFormat, LanguageModelCustomToolGrammarSyntax, LanguageModelImage,
|
||||
LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestToolInput,
|
||||
LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse,
|
||||
LanguageModelToolUseId, LanguageModelToolUseInput, MessageContent, Role, StopReason,
|
||||
TokenUsage,
|
||||
util::{fix_streamed_json, parse_tool_arguments},
|
||||
};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::responses::{
|
||||
ContextManagement, Request as ResponseRequest, ResponseCompactionItem, ResponseError,
|
||||
ContextManagement, Request as ResponseRequest, ResponseCompactionItem,
|
||||
ResponseCustomToolCallItem, ResponseCustomToolCallOutputItem, ResponseError,
|
||||
ResponseFunctionCallItem, ResponseFunctionCallOutputContent, ResponseFunctionCallOutputItem,
|
||||
ResponseIncludable, ResponseInputContent, ResponseInputItem, ResponseMessageItem,
|
||||
ResponseOutputItem, ResponseOutputMessage, ResponseReasoningInputItem, ResponseReasoningItem,
|
||||
@@ -52,7 +55,17 @@ pub fn into_open_ai(
|
||||
max_tokens_parameter: ChatCompletionMaxTokensParameter,
|
||||
reasoning_effort: Option<ReasoningEffort>,
|
||||
interleaved_reasoning: bool,
|
||||
) -> crate::Request {
|
||||
) -> Result<crate::Request> {
|
||||
if request
|
||||
.tools
|
||||
.iter()
|
||||
.any(|tool| matches!(tool.input, LanguageModelRequestToolInput::Custom { .. }))
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"OpenAI Chat Completions does not support custom tools; use Responses API instead"
|
||||
));
|
||||
}
|
||||
|
||||
let stream = !model_id.starts_with("o1-");
|
||||
let service_tier = service_tier_for(request.speed);
|
||||
|
||||
@@ -103,13 +116,18 @@ pub fn into_open_ai(
|
||||
);
|
||||
}
|
||||
MessageContent::ToolUse(tool_use) => {
|
||||
let LanguageModelToolUseInput::Json(input) = &tool_use.input else {
|
||||
return Err(anyhow!(
|
||||
"OpenAI Chat Completions cannot replay custom tool call `{}`",
|
||||
tool_use.name
|
||||
));
|
||||
};
|
||||
let tool_call = ToolCall {
|
||||
id: tool_use.id.to_string(),
|
||||
content: ToolCallContent::Function {
|
||||
function: FunctionContent {
|
||||
name: tool_use.name.to_string(),
|
||||
arguments: serde_json::to_string(&tool_use.input)
|
||||
.unwrap_or_default(),
|
||||
arguments: serde_json::to_string(input).unwrap_or_default(),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -152,7 +170,7 @@ pub fn into_open_ai(
|
||||
}
|
||||
}
|
||||
|
||||
crate::Request {
|
||||
Ok(crate::Request {
|
||||
model: model_id.into(),
|
||||
messages,
|
||||
stream,
|
||||
@@ -184,14 +202,21 @@ pub fn into_open_ai(
|
||||
tools: request
|
||||
.tools
|
||||
.into_iter()
|
||||
.map(|tool| crate::ToolDefinition::Function {
|
||||
function: FunctionDefinition {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(tool.input_schema),
|
||||
},
|
||||
.map(|tool| match tool.input {
|
||||
LanguageModelRequestToolInput::Function { input_schema, .. } => {
|
||||
Ok(crate::ToolDefinition::Function {
|
||||
function: FunctionDefinition {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(input_schema),
|
||||
},
|
||||
})
|
||||
}
|
||||
LanguageModelRequestToolInput::Custom { .. } => Err(anyhow!(
|
||||
"OpenAI Chat Completions does not support custom tools; use Responses API instead"
|
||||
)),
|
||||
})
|
||||
.collect(),
|
||||
.collect::<Result<_>>()?,
|
||||
tool_choice: request.tool_choice.map(|choice| match choice {
|
||||
LanguageModelToolChoice::Auto => crate::ToolChoice::Auto,
|
||||
LanguageModelToolChoice::Any => crate::ToolChoice::Required,
|
||||
@@ -199,7 +224,7 @@ pub fn into_open_ai(
|
||||
}),
|
||||
reasoning_effort,
|
||||
service_tier,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn into_open_ai_response(
|
||||
@@ -232,22 +257,39 @@ pub fn into_open_ai_response(
|
||||
|
||||
let mut input_items = Vec::new();
|
||||
let mut replayed_reasoning_item_indexes = HashMap::default();
|
||||
let mut tool_use_kinds_by_id = HashMap::default();
|
||||
for (index, message) in messages.into_iter().enumerate() {
|
||||
append_message_to_response_items(
|
||||
message,
|
||||
index,
|
||||
&mut replayed_reasoning_item_indexes,
|
||||
&mut tool_use_kinds_by_id,
|
||||
&mut input_items,
|
||||
);
|
||||
}
|
||||
|
||||
let tools: Vec<_> = tools
|
||||
.into_iter()
|
||||
.map(|tool| crate::responses::ToolDefinition::Function {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(tool.input_schema),
|
||||
strict: None,
|
||||
.map(|tool| match tool.input {
|
||||
LanguageModelRequestToolInput::Function { input_schema, .. } => {
|
||||
crate::responses::ToolDefinition::Function {
|
||||
name: tool.name,
|
||||
description: Some(tool.description),
|
||||
parameters: Some(input_schema),
|
||||
strict: None,
|
||||
}
|
||||
}
|
||||
LanguageModelRequestToolInput::Custom { format } => {
|
||||
crate::responses::ToolDefinition::Custom {
|
||||
name: tool.name,
|
||||
description: if tool.description.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(tool.description)
|
||||
},
|
||||
format: format.map(custom_tool_format_into_open_ai),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -323,6 +365,7 @@ fn append_message_to_response_items(
|
||||
message: LanguageModelRequestMessage,
|
||||
index: usize,
|
||||
replayed_reasoning_item_indexes: &mut HashMap<String, usize>,
|
||||
tool_use_kinds_by_id: &mut HashMap<LanguageModelToolUseId, ReplayToolKind>,
|
||||
input_items: &mut Vec<ResponseInputItem>,
|
||||
) {
|
||||
let mut content_parts: Vec<ResponseInputContent> = Vec::new();
|
||||
@@ -386,11 +429,29 @@ fn append_message_to_response_items(
|
||||
input_items,
|
||||
);
|
||||
let call_id = tool_use.id.to_string();
|
||||
input_items.push(ResponseInputItem::FunctionCall(ResponseFunctionCallItem {
|
||||
call_id,
|
||||
name: tool_use.name.to_string(),
|
||||
arguments: tool_use.raw_input,
|
||||
}));
|
||||
match tool_use.input {
|
||||
LanguageModelToolUseInput::Json(_) => {
|
||||
tool_use_kinds_by_id.insert(tool_use.id, ReplayToolKind::Function);
|
||||
input_items.push(ResponseInputItem::FunctionCall(
|
||||
ResponseFunctionCallItem {
|
||||
call_id,
|
||||
name: tool_use.name.to_string(),
|
||||
arguments: tool_use.raw_input,
|
||||
},
|
||||
));
|
||||
}
|
||||
LanguageModelToolUseInput::Text(_) => {
|
||||
tool_use_kinds_by_id.insert(tool_use.id, ReplayToolKind::Custom);
|
||||
input_items.push(ResponseInputItem::CustomToolCall(
|
||||
ResponseCustomToolCallItem {
|
||||
id: None,
|
||||
call_id,
|
||||
name: tool_use.name.to_string(),
|
||||
input: tool_use.raw_input,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
MessageContent::ToolResult(tool_result) => {
|
||||
flush_response_parts(
|
||||
@@ -424,12 +485,24 @@ fn append_message_to_response_items(
|
||||
ResponseFunctionCallOutputContent::List(parts)
|
||||
}
|
||||
};
|
||||
input_items.push(ResponseInputItem::FunctionCallOutput(
|
||||
ResponseFunctionCallOutputItem {
|
||||
call_id: tool_result.tool_use_id.to_string(),
|
||||
output,
|
||||
},
|
||||
));
|
||||
match tool_use_kinds_by_id.get(&tool_result.tool_use_id) {
|
||||
Some(ReplayToolKind::Custom) => {
|
||||
input_items.push(ResponseInputItem::CustomToolCallOutput(
|
||||
ResponseCustomToolCallOutputItem {
|
||||
call_id: tool_result.tool_use_id.to_string(),
|
||||
output,
|
||||
},
|
||||
));
|
||||
}
|
||||
Some(ReplayToolKind::Function) | None => {
|
||||
input_items.push(ResponseInputItem::FunctionCallOutput(
|
||||
ResponseFunctionCallOutputItem {
|
||||
call_id: tool_result.tool_use_id.to_string(),
|
||||
output,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -443,6 +516,33 @@ fn append_message_to_response_items(
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ReplayToolKind {
|
||||
Function,
|
||||
Custom,
|
||||
}
|
||||
|
||||
fn custom_tool_format_into_open_ai(
|
||||
format: LanguageModelCustomToolFormat,
|
||||
) -> crate::responses::CustomToolFormat {
|
||||
match format {
|
||||
LanguageModelCustomToolFormat::Text => crate::responses::CustomToolFormat::Text,
|
||||
LanguageModelCustomToolFormat::Grammar { syntax, definition } => {
|
||||
crate::responses::CustomToolFormat::Grammar {
|
||||
syntax: match syntax {
|
||||
LanguageModelCustomToolGrammarSyntax::Lark => {
|
||||
crate::responses::CustomToolGrammarSyntax::Lark
|
||||
}
|
||||
LanguageModelCustomToolGrammarSyntax::Regex => {
|
||||
crate::responses::CustomToolGrammarSyntax::Regex
|
||||
}
|
||||
},
|
||||
definition,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn append_reasoning_details_to_response_items(
|
||||
reasoning_details: Option<&serde_json::Value>,
|
||||
replayed_reasoning_item_indexes: &mut HashMap<String, usize>,
|
||||
@@ -665,7 +765,7 @@ impl OpenAiEventMapper {
|
||||
id: entry.id.clone().into(),
|
||||
name: entry.name.as_str().into(),
|
||||
is_input_complete: false,
|
||||
input,
|
||||
input: LanguageModelToolUseInput::Json(input),
|
||||
raw_input: entry.arguments.clone(),
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -688,7 +788,7 @@ impl OpenAiEventMapper {
|
||||
id: tool_call.id.clone().into(),
|
||||
name: tool_call.name.as_str().into(),
|
||||
is_input_complete: true,
|
||||
input,
|
||||
input: LanguageModelToolUseInput::Json(input),
|
||||
raw_input: tool_call.arguments.clone(),
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -736,6 +836,7 @@ struct RawToolCall {
|
||||
|
||||
pub struct OpenAiResponseEventMapper {
|
||||
function_calls_by_item: HashMap<String, PendingResponseFunctionCall>,
|
||||
custom_tool_calls_by_item: HashMap<String, PendingResponseCustomToolCall>,
|
||||
reasoning_items: Vec<ResponseReasoningInputItem>,
|
||||
current_message_phase: Option<String>,
|
||||
pending_stop_reason: Option<StopReason>,
|
||||
@@ -748,10 +849,17 @@ struct PendingResponseFunctionCall {
|
||||
arguments: String,
|
||||
}
|
||||
|
||||
struct PendingResponseCustomToolCall {
|
||||
call_id: String,
|
||||
name: Arc<str>,
|
||||
input: String,
|
||||
}
|
||||
|
||||
impl OpenAiResponseEventMapper {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
function_calls_by_item: HashMap::default(),
|
||||
custom_tool_calls_by_item: HashMap::default(),
|
||||
reasoning_items: Vec::new(),
|
||||
current_message_phase: None,
|
||||
pending_stop_reason: None,
|
||||
@@ -805,6 +913,23 @@ impl OpenAiResponseEventMapper {
|
||||
self.function_calls_by_item.insert(item_id, entry);
|
||||
}
|
||||
}
|
||||
ResponseOutputItem::CustomToolCall(custom_tool_call) => {
|
||||
if let Some(item_id) = custom_tool_call.id.clone() {
|
||||
let call_id = custom_tool_call
|
||||
.call_id
|
||||
.clone()
|
||||
.or_else(|| custom_tool_call.id.clone())
|
||||
.unwrap_or_else(|| item_id.clone());
|
||||
let entry = PendingResponseCustomToolCall {
|
||||
call_id,
|
||||
name: Arc::<str>::from(
|
||||
custom_tool_call.name.clone().unwrap_or_default(),
|
||||
),
|
||||
input: custom_tool_call.input.clone(),
|
||||
};
|
||||
self.custom_tool_calls_by_item.insert(item_id, entry);
|
||||
}
|
||||
}
|
||||
ResponseOutputItem::Compaction(_) => {
|
||||
events.push(Ok(LanguageModelCompletionEvent::Compaction(
|
||||
CompactionContent::Pending,
|
||||
@@ -848,7 +973,7 @@ impl OpenAiResponseEventMapper {
|
||||
id: LanguageModelToolUseId::from(entry.call_id.clone()),
|
||||
name: entry.name.clone(),
|
||||
is_input_complete: false,
|
||||
input,
|
||||
input: LanguageModelToolUseInput::Json(input),
|
||||
raw_input: entry.arguments.clone(),
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -873,7 +998,7 @@ impl OpenAiResponseEventMapper {
|
||||
id: LanguageModelToolUseId::from(entry.call_id.clone()),
|
||||
name: entry.name.clone(),
|
||||
is_input_complete: true,
|
||||
input,
|
||||
input: LanguageModelToolUseInput::Json(input),
|
||||
raw_input,
|
||||
thought_signature: None,
|
||||
},
|
||||
@@ -892,6 +1017,30 @@ impl OpenAiResponseEventMapper {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
ResponsesStreamEvent::CustomToolCallInputDelta { item_id, delta, .. } => {
|
||||
if let Some(entry) = self.custom_tool_calls_by_item.get_mut(&item_id) {
|
||||
entry.input.push_str(&delta);
|
||||
return vec![Ok(LanguageModelCompletionEvent::ToolUse(
|
||||
LanguageModelToolUse {
|
||||
id: LanguageModelToolUseId::from(entry.call_id.clone()),
|
||||
name: entry.name.clone(),
|
||||
is_input_complete: false,
|
||||
input: LanguageModelToolUseInput::Text(entry.input.clone()),
|
||||
raw_input: entry.input.clone(),
|
||||
thought_signature: None,
|
||||
},
|
||||
))];
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
ResponsesStreamEvent::CustomToolCallInputDone { item_id, input, .. } => {
|
||||
if let Some(entry) = self.custom_tool_calls_by_item.get_mut(&item_id)
|
||||
&& !input.is_empty()
|
||||
{
|
||||
entry.input = input;
|
||||
}
|
||||
self.finish_pending_custom_tool_call(&item_id, None)
|
||||
}
|
||||
ResponsesStreamEvent::Completed { response } => {
|
||||
self.handle_completion(response, StopReason::EndTurn)
|
||||
}
|
||||
@@ -959,6 +1108,13 @@ impl OpenAiResponseEventMapper {
|
||||
ResponsesStreamEvent::OutputItemDone { item, .. } => match item {
|
||||
ResponseOutputItem::Reasoning(reasoning) => self.capture_reasoning_item(&reasoning),
|
||||
ResponseOutputItem::Message(message) => self.capture_message_phase(&message),
|
||||
ResponseOutputItem::CustomToolCall(custom_tool_call) => {
|
||||
if let Some(item_id) = custom_tool_call.id.as_ref() {
|
||||
self.finish_pending_custom_tool_call(item_id, Some(&custom_tool_call))
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
ResponseOutputItem::Compaction(compaction) => {
|
||||
vec![Ok(LanguageModelCompletionEvent::Compaction(
|
||||
CompactionContent::Encrypted {
|
||||
@@ -1015,48 +1171,109 @@ impl OpenAiResponseEventMapper {
|
||||
) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
|
||||
let mut events = Vec::new();
|
||||
for item in output {
|
||||
if let ResponseOutputItem::FunctionCall(function_call) = item {
|
||||
let Some(call_id) = function_call
|
||||
.call_id
|
||||
.clone()
|
||||
.or_else(|| function_call.id.clone())
|
||||
else {
|
||||
log::error!(
|
||||
"Function call item missing both call_id and id: {:?}",
|
||||
function_call
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let name: Arc<str> = Arc::from(function_call.name.clone().unwrap_or_default());
|
||||
let arguments = &function_call.arguments;
|
||||
self.pending_stop_reason = Some(StopReason::ToolUse);
|
||||
match parse_tool_arguments(arguments) {
|
||||
Ok(input) => {
|
||||
events.push(Ok(LanguageModelCompletionEvent::ToolUse(
|
||||
LanguageModelToolUse {
|
||||
match item {
|
||||
ResponseOutputItem::FunctionCall(function_call) => {
|
||||
let Some(call_id) = function_call
|
||||
.call_id
|
||||
.clone()
|
||||
.or_else(|| function_call.id.clone())
|
||||
else {
|
||||
log::error!(
|
||||
"Function call item missing both call_id and id: {:?}",
|
||||
function_call
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let name: Arc<str> = Arc::from(function_call.name.clone().unwrap_or_default());
|
||||
let arguments = &function_call.arguments;
|
||||
self.pending_stop_reason = Some(StopReason::ToolUse);
|
||||
match parse_tool_arguments(arguments) {
|
||||
Ok(input) => {
|
||||
events.push(Ok(LanguageModelCompletionEvent::ToolUse(
|
||||
LanguageModelToolUse {
|
||||
id: LanguageModelToolUseId::from(call_id.clone()),
|
||||
name: name.clone(),
|
||||
is_input_complete: true,
|
||||
input: LanguageModelToolUseInput::Json(input),
|
||||
raw_input: arguments.clone(),
|
||||
thought_signature: None,
|
||||
},
|
||||
)));
|
||||
}
|
||||
Err(error) => {
|
||||
events.push(Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
|
||||
id: LanguageModelToolUseId::from(call_id.clone()),
|
||||
name: name.clone(),
|
||||
is_input_complete: true,
|
||||
input,
|
||||
raw_input: arguments.clone(),
|
||||
thought_signature: None,
|
||||
},
|
||||
)));
|
||||
}
|
||||
Err(error) => {
|
||||
events.push(Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
|
||||
id: LanguageModelToolUseId::from(call_id.clone()),
|
||||
tool_name: name.clone(),
|
||||
raw_input: Arc::<str>::from(arguments.clone()),
|
||||
json_parse_error: error.to_string(),
|
||||
}));
|
||||
tool_name: name.clone(),
|
||||
raw_input: Arc::<str>::from(arguments.clone()),
|
||||
json_parse_error: error.to_string(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
ResponseOutputItem::CustomToolCall(custom_tool_call) => {
|
||||
events.extend(self.emit_custom_tool_call(custom_tool_call));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
fn emit_custom_tool_call(
|
||||
&mut self,
|
||||
custom_tool_call: &crate::responses::ResponseCustomToolCall,
|
||||
) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
|
||||
let Some(call_id) = custom_tool_call
|
||||
.call_id
|
||||
.clone()
|
||||
.or_else(|| custom_tool_call.id.clone())
|
||||
else {
|
||||
log::error!(
|
||||
"Custom tool call item missing both call_id and id: {:?}",
|
||||
custom_tool_call
|
||||
);
|
||||
return Vec::new();
|
||||
};
|
||||
self.pending_stop_reason = Some(StopReason::ToolUse);
|
||||
let input = custom_tool_call.input.clone();
|
||||
vec![Ok(LanguageModelCompletionEvent::ToolUse(
|
||||
LanguageModelToolUse {
|
||||
id: LanguageModelToolUseId::from(call_id),
|
||||
name: Arc::from(custom_tool_call.name.clone().unwrap_or_default()),
|
||||
is_input_complete: true,
|
||||
input: LanguageModelToolUseInput::Text(input.clone()),
|
||||
raw_input: input,
|
||||
thought_signature: None,
|
||||
},
|
||||
))]
|
||||
}
|
||||
|
||||
fn finish_pending_custom_tool_call(
|
||||
&mut self,
|
||||
item_id: &str,
|
||||
fallback: Option<&crate::responses::ResponseCustomToolCall>,
|
||||
) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
|
||||
let Some(mut entry) = self.custom_tool_calls_by_item.remove(item_id) else {
|
||||
return Vec::new();
|
||||
};
|
||||
if let Some(fallback) = fallback
|
||||
&& !fallback.input.is_empty()
|
||||
{
|
||||
entry.input = fallback.input.clone();
|
||||
}
|
||||
self.pending_stop_reason = Some(StopReason::ToolUse);
|
||||
vec![Ok(LanguageModelCompletionEvent::ToolUse(
|
||||
LanguageModelToolUse {
|
||||
id: LanguageModelToolUseId::from(entry.call_id),
|
||||
name: entry.name,
|
||||
is_input_complete: true,
|
||||
input: LanguageModelToolUseInput::Text(entry.input.clone()),
|
||||
raw_input: entry.input,
|
||||
thought_signature: None,
|
||||
},
|
||||
))]
|
||||
}
|
||||
|
||||
fn capture_reasoning_items_from_output(
|
||||
&mut self,
|
||||
output: &[ResponseOutputItem],
|
||||
@@ -1245,15 +1462,17 @@ fn response_reasoning_input_item_from_output(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::responses::{
|
||||
ReasoningSummaryPart, ResponseError, ResponseFunctionToolCall, ResponseIncompleteDetails,
|
||||
ResponseInputTokensDetails, ResponseOutputItem, ResponseOutputMessage,
|
||||
ResponseReasoningItem, ResponseSummary, ResponseUsage, StreamEvent as ResponsesStreamEvent,
|
||||
ReasoningSummaryPart, ResponseCustomToolCall, ResponseError, ResponseFunctionToolCall,
|
||||
ResponseIncompleteDetails, ResponseInputItem, ResponseInputTokensDetails,
|
||||
ResponseOutputItem, ResponseOutputMessage, ResponseReasoningItem, ResponseSummary,
|
||||
ResponseUsage, StreamEvent as ResponsesStreamEvent, ToolDefinition,
|
||||
};
|
||||
use futures::{StreamExt, executor::block_on};
|
||||
use language_model_core::{
|
||||
LanguageModelImage, LanguageModelRequestMessage, LanguageModelRequestTool,
|
||||
LanguageModelCustomToolFormat, LanguageModelCustomToolGrammarSyntax, LanguageModelImage,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelRequestToolInput,
|
||||
LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolUse,
|
||||
LanguageModelToolUseId, SharedString, Speed,
|
||||
LanguageModelToolUseId, LanguageModelToolUseInput, SharedString, Speed,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
@@ -1306,6 +1525,16 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
fn response_item_custom_tool_call(id: &str, input: &str) -> ResponseOutputItem {
|
||||
ResponseOutputItem::CustomToolCall(ResponseCustomToolCall {
|
||||
id: Some(id.to_string()),
|
||||
status: Some("in_progress".to_string()),
|
||||
name: Some("apply_patch".to_string()),
|
||||
call_id: Some("call_abc".to_string()),
|
||||
input: input.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn response_reasoning_item(
|
||||
id: &str,
|
||||
summary: Vec<ReasoningSummaryPart>,
|
||||
@@ -1417,6 +1646,97 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_custom_tool_wire_types_round_trip() -> Result<()> {
|
||||
let tool_json = json!({
|
||||
"type": "custom",
|
||||
"name": "apply_patch",
|
||||
"description": "Apply a patch",
|
||||
"format": {
|
||||
"type": "grammar",
|
||||
"syntax": "lark",
|
||||
"definition": "start: /.+/"
|
||||
}
|
||||
});
|
||||
let tool: ToolDefinition = serde_json::from_value(tool_json.clone())?;
|
||||
assert_eq!(serde_json::to_value(tool)?, tool_json);
|
||||
|
||||
let text_tool_json = json!({
|
||||
"type": "custom",
|
||||
"name": "write_text",
|
||||
"format": { "type": "text" }
|
||||
});
|
||||
let text_tool: ToolDefinition = serde_json::from_value(text_tool_json.clone())?;
|
||||
assert_eq!(serde_json::to_value(text_tool)?, text_tool_json);
|
||||
|
||||
let input_json = json!({
|
||||
"type": "custom_tool_call",
|
||||
"id": "ctc_1",
|
||||
"call_id": "call_abc",
|
||||
"name": "apply_patch",
|
||||
"input": "*** Begin Patch\n*** End Patch"
|
||||
});
|
||||
let input: ResponseInputItem = serde_json::from_value(input_json.clone())?;
|
||||
assert_eq!(serde_json::to_value(input)?, input_json);
|
||||
|
||||
let output_json = json!({
|
||||
"type": "custom_tool_call_output",
|
||||
"call_id": "call_abc",
|
||||
"output": "ok"
|
||||
});
|
||||
let output: ResponseInputItem = serde_json::from_value(output_json.clone())?;
|
||||
assert_eq!(serde_json::to_value(output)?, output_json);
|
||||
|
||||
let output_item_json = json!({
|
||||
"id": "ctc_1",
|
||||
"type": "custom_tool_call",
|
||||
"status": "completed",
|
||||
"call_id": "call_abc",
|
||||
"name": "apply_patch",
|
||||
"input": "*** Begin Patch\n*** End Patch"
|
||||
});
|
||||
let output_item: ResponseOutputItem = serde_json::from_value(output_item_json.clone())?;
|
||||
assert_eq!(serde_json::to_value(output_item)?, output_item_json);
|
||||
|
||||
let delta_json = json!({
|
||||
"type": "response.custom_tool_call_input.delta",
|
||||
"output_index": 0,
|
||||
"item_id": "ctc_1",
|
||||
"sequence_number": 5,
|
||||
"delta": "chunk"
|
||||
});
|
||||
let delta: ResponsesStreamEvent = serde_json::from_value(delta_json)?;
|
||||
assert!(matches!(
|
||||
delta,
|
||||
ResponsesStreamEvent::CustomToolCallInputDelta {
|
||||
output_index: 0,
|
||||
sequence_number: Some(5),
|
||||
ref item_id,
|
||||
ref delta,
|
||||
} if item_id == "ctc_1" && delta == "chunk"
|
||||
));
|
||||
|
||||
let done_json = json!({
|
||||
"type": "response.custom_tool_call_input.done",
|
||||
"output_index": 0,
|
||||
"item_id": "ctc_1",
|
||||
"sequence_number": 6,
|
||||
"input": "full text"
|
||||
});
|
||||
let done: ResponsesStreamEvent = serde_json::from_value(done_json)?;
|
||||
assert!(matches!(
|
||||
done,
|
||||
ResponsesStreamEvent::CustomToolCallInputDone {
|
||||
output_index: 0,
|
||||
sequence_number: Some(6),
|
||||
ref item_id,
|
||||
ref input,
|
||||
} if item_id == "ctc_1" && input == "full text"
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_open_ai_response_builds_complete_payload() {
|
||||
let tool_call_id = LanguageModelToolUseId::from("call-42");
|
||||
@@ -1426,7 +1746,7 @@ mod tests {
|
||||
id: tool_call_id.clone(),
|
||||
name: Arc::from("get_weather"),
|
||||
raw_input: tool_arguments.clone(),
|
||||
input: tool_input,
|
||||
input: LanguageModelToolUseInput::Json(tool_input),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -1478,12 +1798,12 @@ mod tests {
|
||||
reasoning_details: None,
|
||||
},
|
||||
],
|
||||
tools: vec![LanguageModelRequestTool {
|
||||
name: "get_weather".into(),
|
||||
description: "Fetches the weather".into(),
|
||||
input_schema: json!({ "type": "object" }),
|
||||
use_input_streaming: false,
|
||||
}],
|
||||
tools: vec![LanguageModelRequestTool::function(
|
||||
"get_weather".into(),
|
||||
"Fetches the weather".into(),
|
||||
json!({ "type": "object" }),
|
||||
false,
|
||||
)],
|
||||
tool_choice: Some(LanguageModelToolChoice::Any),
|
||||
stop: vec!["<STOP>".into()],
|
||||
temperature: None,
|
||||
@@ -1562,6 +1882,166 @@ mod tests {
|
||||
assert_eq!(serialized, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_stream_maps_custom_tool_input() {
|
||||
let events = vec![
|
||||
ResponsesStreamEvent::OutputItemAdded {
|
||||
output_index: 0,
|
||||
sequence_number: None,
|
||||
item: response_item_custom_tool_call("ctc_1", ""),
|
||||
},
|
||||
ResponsesStreamEvent::CustomToolCallInputDelta {
|
||||
item_id: "ctc_1".into(),
|
||||
output_index: 0,
|
||||
delta: "*** Begin".into(),
|
||||
sequence_number: Some(1),
|
||||
},
|
||||
ResponsesStreamEvent::CustomToolCallInputDelta {
|
||||
item_id: "ctc_1".into(),
|
||||
output_index: 0,
|
||||
delta: " Patch".into(),
|
||||
sequence_number: Some(2),
|
||||
},
|
||||
ResponsesStreamEvent::CustomToolCallInputDone {
|
||||
item_id: "ctc_1".into(),
|
||||
output_index: 0,
|
||||
input: "*** Begin Patch".into(),
|
||||
sequence_number: Some(3),
|
||||
},
|
||||
ResponsesStreamEvent::OutputItemDone {
|
||||
output_index: 0,
|
||||
sequence_number: None,
|
||||
item: response_item_custom_tool_call("ctc_1", "*** Begin Patch"),
|
||||
},
|
||||
ResponsesStreamEvent::Completed {
|
||||
response: ResponseSummary::default(),
|
||||
},
|
||||
];
|
||||
|
||||
let mapped = map_response_events(events);
|
||||
assert_eq!(
|
||||
mapped,
|
||||
vec![
|
||||
LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
|
||||
id: LanguageModelToolUseId::from("call_abc"),
|
||||
name: Arc::from("apply_patch"),
|
||||
raw_input: "*** Begin".into(),
|
||||
input: LanguageModelToolUseInput::Text("*** Begin".into()),
|
||||
is_input_complete: false,
|
||||
thought_signature: None,
|
||||
}),
|
||||
LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
|
||||
id: LanguageModelToolUseId::from("call_abc"),
|
||||
name: Arc::from("apply_patch"),
|
||||
raw_input: "*** Begin Patch".into(),
|
||||
input: LanguageModelToolUseInput::Text("*** Begin Patch".into()),
|
||||
is_input_complete: false,
|
||||
thought_signature: None,
|
||||
}),
|
||||
LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
|
||||
id: LanguageModelToolUseId::from("call_abc"),
|
||||
name: Arc::from("apply_patch"),
|
||||
raw_input: "*** Begin Patch".into(),
|
||||
input: LanguageModelToolUseInput::Text("*** Begin Patch".into()),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
}),
|
||||
LanguageModelCompletionEvent::Stop(StopReason::ToolUse),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_open_ai_response_replays_custom_tool_calls() {
|
||||
let tool_call_id = LanguageModelToolUseId::from("call_abc");
|
||||
let raw_input = "*** Begin Patch\n*** End Patch".to_string();
|
||||
let tool_use = LanguageModelToolUse {
|
||||
id: tool_call_id.clone(),
|
||||
name: Arc::from("apply_patch"),
|
||||
raw_input: raw_input.clone(),
|
||||
input: LanguageModelToolUseInput::Text(raw_input.clone()),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
let tool_result = LanguageModelToolResult {
|
||||
tool_use_id: tool_call_id,
|
||||
tool_name: Arc::from("apply_patch"),
|
||||
is_error: false,
|
||||
content: vec![LanguageModelToolResultContent::Text(Arc::from("ok"))],
|
||||
output: None,
|
||||
};
|
||||
|
||||
let request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::Assistant,
|
||||
content: vec![
|
||||
MessageContent::ToolUse(tool_use),
|
||||
MessageContent::ToolResult(tool_result),
|
||||
],
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
}],
|
||||
tools: vec![LanguageModelRequestTool {
|
||||
name: "apply_patch".into(),
|
||||
description: "Apply a patch".into(),
|
||||
input: LanguageModelRequestToolInput::Custom {
|
||||
format: Some(LanguageModelCustomToolFormat::Grammar {
|
||||
syntax: LanguageModelCustomToolGrammarSyntax::Lark,
|
||||
definition: "start: /.+/".into(),
|
||||
}),
|
||||
},
|
||||
}],
|
||||
tool_choice: None,
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
thinking_allowed: false,
|
||||
thinking_effort: None,
|
||||
speed: None,
|
||||
compact_at_tokens: None,
|
||||
};
|
||||
|
||||
let response =
|
||||
into_open_ai_response(request, "custom-model", false, false, None, None, false);
|
||||
let serialized = serde_json::to_value(response).unwrap();
|
||||
assert_eq!(
|
||||
serialized,
|
||||
json!({
|
||||
"model": "custom-model",
|
||||
"input": [
|
||||
{
|
||||
"type": "custom_tool_call",
|
||||
"call_id": "call_abc",
|
||||
"name": "apply_patch",
|
||||
"input": raw_input
|
||||
},
|
||||
{
|
||||
"type": "custom_tool_call_output",
|
||||
"call_id": "call_abc",
|
||||
"output": "ok"
|
||||
}
|
||||
],
|
||||
"store": false,
|
||||
"stream": true,
|
||||
"parallel_tool_calls": false,
|
||||
"tools": [
|
||||
{
|
||||
"type": "custom",
|
||||
"name": "apply_patch",
|
||||
"description": "Apply a patch",
|
||||
"format": {
|
||||
"type": "grammar",
|
||||
"syntax": "lark",
|
||||
"definition": "start: /.+/"
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_open_ai_response_replays_encrypted_reasoning_details() {
|
||||
let tool_call_id = LanguageModelToolUseId::from("call-42");
|
||||
@@ -1570,7 +2050,7 @@ mod tests {
|
||||
id: tool_call_id,
|
||||
name: Arc::from("get_weather"),
|
||||
raw_input: tool_arguments.clone(),
|
||||
input: json!({ "city": "Boston" }),
|
||||
input: LanguageModelToolUseInput::Json(json!({ "city": "Boston" })),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -1850,7 +2330,7 @@ mod tests {
|
||||
ChatCompletionMaxTokensParameter::MaxCompletionTokens,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
)?;
|
||||
|
||||
let serialized = serde_json::to_value(&chat)?;
|
||||
assert_eq!(
|
||||
@@ -1895,7 +2375,7 @@ mod tests {
|
||||
ChatCompletionMaxTokensParameter::MaxTokens,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
)?;
|
||||
|
||||
let serialized = serde_json::to_value(&chat)?;
|
||||
assert_eq!(serialized.get("max_completion_tokens"), None);
|
||||
@@ -2700,8 +3180,7 @@ mod tests {
|
||||
}) if id.to_string() == "call_123"
|
||||
&& name.as_ref() == "get_weather"
|
||||
&& raw_input == ""
|
||||
&& input.is_object()
|
||||
&& input.as_object().unwrap().is_empty()
|
||||
&& matches!(input, LanguageModelToolUseInput::Json(value) if value.as_object().is_some_and(|object| object.is_empty()))
|
||||
));
|
||||
assert!(matches!(
|
||||
mapped[1],
|
||||
@@ -3198,7 +3677,7 @@ mod tests {
|
||||
id: tool_use_id.clone(),
|
||||
name: Arc::from("search"),
|
||||
raw_input: tool_arguments.clone(),
|
||||
input: tool_input,
|
||||
input: LanguageModelToolUseInput::Json(tool_input),
|
||||
is_input_complete: true,
|
||||
thought_signature: None,
|
||||
};
|
||||
@@ -3259,7 +3738,8 @@ mod tests {
|
||||
ChatCompletionMaxTokensParameter::MaxCompletionTokens,
|
||||
None,
|
||||
true,
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_value(&result).unwrap()["messages"],
|
||||
json!([
|
||||
@@ -3283,7 +3763,8 @@ mod tests {
|
||||
ChatCompletionMaxTokensParameter::MaxCompletionTokens,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_value(&result).unwrap()["messages"],
|
||||
json!([
|
||||
|
||||
@@ -66,6 +66,8 @@ pub enum ResponseInputItem {
|
||||
Message(ResponseMessageItem),
|
||||
FunctionCall(ResponseFunctionCallItem),
|
||||
FunctionCallOutput(ResponseFunctionCallOutputItem),
|
||||
CustomToolCall(ResponseCustomToolCallItem),
|
||||
CustomToolCallOutput(ResponseCustomToolCallOutputItem),
|
||||
Reasoning(ResponseReasoningInputItem),
|
||||
Compaction(ResponseCompactionItem),
|
||||
}
|
||||
@@ -98,6 +100,21 @@ pub struct ResponseFunctionCallOutputItem {
|
||||
pub output: ResponseFunctionCallOutputContent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ResponseCustomToolCallItem {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<String>,
|
||||
pub call_id: String,
|
||||
pub name: String,
|
||||
pub input: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ResponseCustomToolCallOutputItem {
|
||||
pub call_id: String,
|
||||
pub output: ResponseFunctionCallOutputContent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct ResponseReasoningInputItem {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -157,7 +174,7 @@ pub enum ReasoningSummaryMode {
|
||||
Detailed,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ToolDefinition {
|
||||
Function {
|
||||
@@ -169,9 +186,33 @@ pub enum ToolDefinition {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
strict: Option<bool>,
|
||||
},
|
||||
Custom {
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
format: Option<CustomToolFormat>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum CustomToolFormat {
|
||||
Text,
|
||||
Grammar {
|
||||
syntax: CustomToolGrammarSyntax,
|
||||
definition: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CustomToolGrammarSyntax {
|
||||
Lark,
|
||||
Regex,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ResponseError {
|
||||
#[serde(default)]
|
||||
pub code: Option<String>,
|
||||
@@ -343,6 +384,22 @@ pub enum StreamEvent {
|
||||
#[serde(default)]
|
||||
sequence_number: Option<u64>,
|
||||
},
|
||||
#[serde(rename = "response.custom_tool_call_input.delta")]
|
||||
CustomToolCallInputDelta {
|
||||
item_id: String,
|
||||
output_index: usize,
|
||||
delta: String,
|
||||
#[serde(default)]
|
||||
sequence_number: Option<u64>,
|
||||
},
|
||||
#[serde(rename = "response.custom_tool_call_input.done")]
|
||||
CustomToolCallInputDone {
|
||||
item_id: String,
|
||||
output_index: usize,
|
||||
input: String,
|
||||
#[serde(default)]
|
||||
sequence_number: Option<u64>,
|
||||
},
|
||||
#[serde(rename = "response.completed")]
|
||||
Completed { response: ResponseSummary },
|
||||
#[serde(rename = "response.incomplete")]
|
||||
@@ -360,7 +417,7 @@ pub enum StreamEvent {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Default, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||
pub struct ResponseSummary {
|
||||
#[serde(default)]
|
||||
pub id: Option<String>,
|
||||
@@ -378,13 +435,13 @@ pub struct ResponseSummary {
|
||||
pub service_tier: Option<crate::ServiceTier>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Default, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||
pub struct ResponseIncompleteDetails {
|
||||
#[serde(default)]
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Default, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||
pub struct ResponseUsage {
|
||||
#[serde(default)]
|
||||
pub input_tokens: Option<u64>,
|
||||
@@ -398,30 +455,32 @@ pub struct ResponseUsage {
|
||||
pub total_tokens: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Default, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||
pub struct ResponseInputTokensDetails {
|
||||
#[serde(default)]
|
||||
pub cached_tokens: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Default, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||
pub struct ResponseOutputTokensDetails {
|
||||
#[serde(default)]
|
||||
pub reasoning_tokens: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ResponseOutputItem {
|
||||
Message(ResponseOutputMessage),
|
||||
FunctionCall(ResponseFunctionToolCall),
|
||||
CustomToolCall(ResponseCustomToolCall),
|
||||
Reasoning(ResponseReasoningItem),
|
||||
Compaction(ResponseCompactionItem),
|
||||
/// Deserialization-only catch-all; must never be serialized back to the API.
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ResponseReasoningItem {
|
||||
#[serde(default)]
|
||||
pub id: Option<String>,
|
||||
@@ -435,7 +494,7 @@ pub struct ResponseReasoningItem {
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ReasoningSummaryPart {
|
||||
SummaryText {
|
||||
@@ -445,7 +504,7 @@ pub enum ReasoningSummaryPart {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ResponseOutputMessage {
|
||||
#[serde(default)]
|
||||
pub id: Option<String>,
|
||||
@@ -459,7 +518,7 @@ pub struct ResponseOutputMessage {
|
||||
pub phase: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ResponseFunctionToolCall {
|
||||
#[serde(default)]
|
||||
pub id: Option<String>,
|
||||
@@ -473,6 +532,20 @@ pub struct ResponseFunctionToolCall {
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ResponseCustomToolCall {
|
||||
#[serde(default)]
|
||||
pub id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub status: Option<String>,
|
||||
#[serde(default)]
|
||||
pub call_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub input: String,
|
||||
}
|
||||
|
||||
pub async fn stream_response(
|
||||
client: &dyn HttpClient,
|
||||
provider_name: &str,
|
||||
@@ -580,6 +653,16 @@ pub async fn stream_response(
|
||||
});
|
||||
}
|
||||
}
|
||||
ResponseOutputItem::CustomToolCall(custom_tool_call) => {
|
||||
if let Some(ref item_id) = custom_tool_call.id {
|
||||
all_events.push(StreamEvent::CustomToolCallInputDone {
|
||||
item_id: item_id.clone(),
|
||||
output_index,
|
||||
input: custom_tool_call.input.clone(),
|
||||
sequence_number: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
ResponseOutputItem::Reasoning(reasoning) => {
|
||||
if let Some(ref item_id) = reasoning.id {
|
||||
for part in &reasoning.summary {
|
||||
|
||||
Reference in new issue
Block a user