mirror of
https://github.com/instructkr/claude-code.git
synced 2026-06-05 03:56:45 +00:00
In the provider compatibility layer for Gemini (and other providers requiring ), fully support the flow, round-trip, and placeholder fallback of thought_signature.
This commit is contained in:
@@ -38,6 +38,7 @@ fn create_sample_request(message_count: usize) -> MessageRequest {
|
|||||||
id: format!("call_{}", i),
|
id: format!("call_{}", i),
|
||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
input: json!({"path": format!("/tmp/file{}", i)}),
|
input: json!({"path": format!("/tmp/file{}", i)}),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -57,6 +58,7 @@ fn create_sample_request(message_count: usize) -> MessageRequest {
|
|||||||
id: format!("call_{}", i),
|
id: format!("call_{}", i),
|
||||||
name: "write_file".to_string(),
|
name: "write_file".to_string(),
|
||||||
input: json!({"path": format!("/tmp/out{}", i), "content": "data"}),
|
input: json!({"path": format!("/tmp/out{}", i), "content": "data"}),
|
||||||
|
thought_signature: None,
|
||||||
}],
|
}],
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@@ -105,11 +107,13 @@ fn bench_translate_message(c: &mut Criterion) {
|
|||||||
id: "call_1".to_string(),
|
id: "call_1".to_string(),
|
||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
input: json!({"path": "/tmp/test"}),
|
input: json!({"path": "/tmp/test"}),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
InputContentBlock::ToolUse {
|
InputContentBlock::ToolUse {
|
||||||
id: "call_2".to_string(),
|
id: "call_2".to_string(),
|
||||||
name: "write_file".to_string(),
|
name: "write_file".to_string(),
|
||||||
input: json!({"path": "/tmp/out", "content": "data"}),
|
input: json!({"path": "/tmp/out", "content": "data"}),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -586,6 +586,21 @@ impl StreamState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(delta_extra) = &choice.delta.extra_content {
|
||||||
|
if let Some(delta_sig) = delta_extra
|
||||||
|
.get("google")
|
||||||
|
.and_then(|g| g.get("thought_signature"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
{
|
||||||
|
for state in self.tool_calls.values_mut() {
|
||||||
|
if state.thought_signature.is_none() {
|
||||||
|
state.thought_signature.get_or_insert(delta_sig.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(finish_reason) = choice.finish_reason {
|
if let Some(finish_reason) = choice.finish_reason {
|
||||||
self.stop_reason = Some(normalize_finish_reason(&finish_reason));
|
self.stop_reason = Some(normalize_finish_reason(&finish_reason));
|
||||||
if finish_reason == "tool_calls" {
|
if finish_reason == "tool_calls" {
|
||||||
@@ -693,6 +708,7 @@ struct ToolCallState {
|
|||||||
id: Option<String>,
|
id: Option<String>,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
arguments: String,
|
arguments: String,
|
||||||
|
thought_signature: Option<String>,
|
||||||
emitted_len: usize,
|
emitted_len: usize,
|
||||||
started: bool,
|
started: bool,
|
||||||
stopped: bool,
|
stopped: bool,
|
||||||
@@ -710,6 +726,24 @@ impl ToolCallState {
|
|||||||
if let Some(arguments) = tool_call.function.arguments {
|
if let Some(arguments) = tool_call.function.arguments {
|
||||||
self.arguments.push_str(&arguments);
|
self.arguments.push_str(&arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(sig) = tool_call.thought_signature.filter(|s| !s.is_empty()) {
|
||||||
|
self.thought_signature.get_or_insert(sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://ai.google.dev/gemini-api/docs/thought-signatures
|
||||||
|
if self.thought_signature.is_none() {
|
||||||
|
if let Some(sig) = tool_call
|
||||||
|
.extra_content
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|ec| ec.get("google"))
|
||||||
|
.and_then(|g| g.get("thought_signature"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
{
|
||||||
|
self.thought_signature.get_or_insert(sig.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn block_index(&self, offset: u32) -> u32 {
|
const fn block_index(&self, offset: u32) -> u32 {
|
||||||
@@ -731,6 +765,7 @@ impl ToolCallState {
|
|||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
input: json!({}),
|
input: json!({}),
|
||||||
|
thought_signature: self.thought_signature.clone(),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -782,6 +817,10 @@ struct ChatMessage {
|
|||||||
struct ResponseToolCall {
|
struct ResponseToolCall {
|
||||||
id: String,
|
id: String,
|
||||||
function: ResponseToolFunction,
|
function: ResponseToolFunction,
|
||||||
|
#[serde(default)]
|
||||||
|
thought_signature: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
extra_content: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -852,6 +891,8 @@ struct ChunkDelta {
|
|||||||
thinking: Option<ThinkingDelta>,
|
thinking: Option<ThinkingDelta>,
|
||||||
#[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
|
#[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
|
||||||
tool_calls: Vec<DeltaToolCall>,
|
tool_calls: Vec<DeltaToolCall>,
|
||||||
|
#[serde(default)]
|
||||||
|
extra_content: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
@@ -860,7 +901,7 @@ struct ThinkingDelta {
|
|||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
struct DeltaToolCall {
|
struct DeltaToolCall {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
index: u32,
|
index: u32,
|
||||||
@@ -868,6 +909,10 @@ struct DeltaToolCall {
|
|||||||
id: Option<String>,
|
id: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
function: DeltaFunction,
|
function: DeltaFunction,
|
||||||
|
#[serde(default)]
|
||||||
|
thought_signature: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
extra_content: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
@@ -923,6 +968,22 @@ pub fn model_requires_reasoning_content_in_history(model: &str) -> bool {
|
|||||||
canonical.starts_with("deepseek-v4")
|
canonical.starts_with("deepseek-v4")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dummy thought signature accepted by Gemini as a validation bypass for
|
||||||
|
/// conversation history that lacks a real signature. Source:
|
||||||
|
/// - LiteLLM: https://github.com/BerriAI/litellm/pull/16812
|
||||||
|
/// - Google: https://ai.google.dev/gemini-api/docs/thought-signatures#faqs
|
||||||
|
const GEMINI_DUMMY_THOUGHT_SIGNATURE: &str = "c2tpcF90aG91Z2h0X3NpZ25hdHVyZV92YWxpZGF0b3I=";
|
||||||
|
|
||||||
|
/// Returns true if the model is a Gemini model (Gemini 2.5+, 3+ etc) that
|
||||||
|
/// requires `thought_signature` on function calls in conversation history.
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_gemini_model(model: &str) -> bool {
|
||||||
|
let lowered = model.to_ascii_lowercase();
|
||||||
|
let canonical = lowered.rsplit('/').next().unwrap_or(lowered.as_str());
|
||||||
|
canonical.starts_with("gemini")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Strip routing prefix (e.g., "openai/gpt-4" → "gpt-4") for the wire.
|
/// Strip routing prefix (e.g., "openai/gpt-4" → "gpt-4") for the wire.
|
||||||
/// The prefix is used only to select transport; the backend expects the
|
/// The prefix is used only to select transport; the backend expects the
|
||||||
/// bare model id. Use `local/` to force OpenAI-compatible routing while
|
/// bare model id. Use `local/` to force OpenAI-compatible routing while
|
||||||
@@ -1216,14 +1277,32 @@ pub fn translate_message(message: &InputMessage, model: &str) -> Vec<Value> {
|
|||||||
InputContentBlock::Thinking {
|
InputContentBlock::Thinking {
|
||||||
thinking: value, ..
|
thinking: value, ..
|
||||||
} => reasoning.push_str(value),
|
} => reasoning.push_str(value),
|
||||||
InputContentBlock::ToolUse { id, name, input } => tool_calls.push(json!({
|
InputContentBlock::ToolUse { id, name, input, thought_signature } => {
|
||||||
"id": id,
|
let mut tc = json!({
|
||||||
"type": "function",
|
"id": id,
|
||||||
"function": {
|
"type": "function",
|
||||||
"name": name,
|
"function": {
|
||||||
"arguments": input.to_string(),
|
"name": name,
|
||||||
|
"arguments": input.to_string(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let sig_for_gemini = thought_signature.clone().or_else(|| {
|
||||||
|
if is_gemini_model(model) {
|
||||||
|
Some(GEMINI_DUMMY_THOUGHT_SIGNATURE.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Some(sig) = sig_for_gemini {
|
||||||
|
tc["extra_content"] = json!({
|
||||||
|
"google": {
|
||||||
|
"thought_signature": sig
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})),
|
tool_calls.push(tc);
|
||||||
|
}
|
||||||
InputContentBlock::ToolResult { .. } => {}
|
InputContentBlock::ToolResult { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1468,10 +1547,22 @@ fn normalize_response(
|
|||||||
content.push(OutputContentBlock::Text { text });
|
content.push(OutputContentBlock::Text { text });
|
||||||
}
|
}
|
||||||
for tool_call in choice.message.tool_calls {
|
for tool_call in choice.message.tool_calls {
|
||||||
|
let thought_signature = tool_call.thought_signature.or_else(|| {
|
||||||
|
tool_call
|
||||||
|
.extra_content
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|ec| ec.get("google"))
|
||||||
|
.and_then(|g| g.get("thought_signature"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(String::from)
|
||||||
|
});
|
||||||
|
|
||||||
content.push(OutputContentBlock::ToolUse {
|
content.push(OutputContentBlock::ToolUse {
|
||||||
id: tool_call.id,
|
id: tool_call.id,
|
||||||
name: tool_call.function.name,
|
name: tool_call.function.name,
|
||||||
input: parse_tool_arguments(&tool_call.function.arguments),
|
input: parse_tool_arguments(&tool_call.function.arguments),
|
||||||
|
thought_signature,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1866,6 +1957,7 @@ mod tests {
|
|||||||
id: "call_1".to_string(),
|
id: "call_1".to_string(),
|
||||||
name: "get_weather".to_string(),
|
name: "get_weather".to_string(),
|
||||||
input: json!({"city": "Paris"}),
|
input: json!({"city": "Paris"}),
|
||||||
|
thought_signature: None,
|
||||||
}],
|
}],
|
||||||
}],
|
}],
|
||||||
stream: false,
|
stream: false,
|
||||||
@@ -1943,6 +2035,7 @@ mod tests {
|
|||||||
reasoning_content: Some("think".to_string()),
|
reasoning_content: Some("think".to_string()),
|
||||||
thinking: None,
|
thinking: None,
|
||||||
tool_calls: Vec::new(),
|
tool_calls: Vec::new(),
|
||||||
|
extra_content: None,
|
||||||
},
|
},
|
||||||
finish_reason: None,
|
finish_reason: None,
|
||||||
}],
|
}],
|
||||||
@@ -1960,6 +2053,7 @@ mod tests {
|
|||||||
reasoning_content: None,
|
reasoning_content: None,
|
||||||
thinking: None,
|
thinking: None,
|
||||||
tool_calls: Vec::new(),
|
tool_calls: Vec::new(),
|
||||||
|
extra_content: None,
|
||||||
},
|
},
|
||||||
finish_reason: Some("stop".to_string()),
|
finish_reason: Some("stop".to_string()),
|
||||||
}],
|
}],
|
||||||
@@ -2504,6 +2598,7 @@ mod tests {
|
|||||||
id: "call_1".to_string(),
|
id: "call_1".to_string(),
|
||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
input: serde_json::json!({"path": "/tmp/test"}),
|
input: serde_json::json!({"path": "/tmp/test"}),
|
||||||
|
thought_signature: None,
|
||||||
}],
|
}],
|
||||||
}],
|
}],
|
||||||
stream: false,
|
stream: false,
|
||||||
@@ -2722,6 +2817,7 @@ mod tests {
|
|||||||
id: "call_1".to_string(),
|
id: "call_1".to_string(),
|
||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
input: serde_json::json!({"path": "/tmp/test"}),
|
input: serde_json::json!({"path": "/tmp/test"}),
|
||||||
|
thought_signature: None,
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
InputMessage {
|
InputMessage {
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ pub enum InputContentBlock {
|
|||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
input: Value,
|
input: Value,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
thought_signature: Option<String>,
|
||||||
},
|
},
|
||||||
ToolResult {
|
ToolResult {
|
||||||
tool_use_id: String,
|
tool_use_id: String,
|
||||||
@@ -167,6 +169,8 @@ pub enum OutputContentBlock {
|
|||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
input: Value,
|
input: Value,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
thought_signature: Option<String>,
|
||||||
},
|
},
|
||||||
Thinking {
|
Thinking {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -768,7 +768,7 @@ fn tool_calls_for_json(content: &[OutputContentBlock]) -> Vec<Value> {
|
|||||||
content
|
content
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|b| {
|
.filter_map(|b| {
|
||||||
if let OutputContentBlock::ToolUse { id, name, input } = b {
|
if let OutputContentBlock::ToolUse { id, name, input, .. } = b {
|
||||||
Some(json!({
|
Some(json!({
|
||||||
"id": id,
|
"id": id,
|
||||||
"name": name,
|
"name": name,
|
||||||
@@ -1474,7 +1474,7 @@ async fn stream_to_message_response(
|
|||||||
block_kind.insert(index, BlockKind::Text);
|
block_kind.insert(index, BlockKind::Text);
|
||||||
text_buf.insert(index, text);
|
text_buf.insert(index, text);
|
||||||
}
|
}
|
||||||
OutputContentBlock::ToolUse { id, name, input } => {
|
OutputContentBlock::ToolUse { id, name, input, .. } => {
|
||||||
let json = if input.as_object().is_some_and(|m| m.is_empty()) {
|
let json = if input.as_object().is_some_and(|m| m.is_empty()) {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
@@ -1523,7 +1523,7 @@ async fn stream_to_message_response(
|
|||||||
Some(BlockKind::Tool { id, name, json }) => {
|
Some(BlockKind::Tool { id, name, json }) => {
|
||||||
let input = serde_json::from_str::<Value>(&json)
|
let input = serde_json::from_str::<Value>(&json)
|
||||||
.unwrap_or_else(|_| json!({ "raw": json }));
|
.unwrap_or_else(|_| json!({ "raw": json }));
|
||||||
finished.insert(idx, OutputContentBlock::ToolUse { id, name, input });
|
finished.insert(idx, OutputContentBlock::ToolUse { id, name, input, thought_signature: None });
|
||||||
}
|
}
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
@@ -1581,7 +1581,7 @@ fn collect_tool_uses(content: &[OutputContentBlock]) -> Vec<ToolUse<'_>> {
|
|||||||
content
|
content
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|b| {
|
.filter_map(|b| {
|
||||||
if let OutputContentBlock::ToolUse { id, name, input } = b {
|
if let OutputContentBlock::ToolUse { id, name, input, .. } = b {
|
||||||
Some(ToolUse {
|
Some(ToolUse {
|
||||||
id: id.as_str(),
|
id: id.as_str(),
|
||||||
name: name.as_str(),
|
name: name.as_str(),
|
||||||
@@ -1601,10 +1601,11 @@ fn output_to_input_blocks(blocks: &[OutputContentBlock]) -> Vec<InputContentBloc
|
|||||||
OutputContentBlock::Text { text } => {
|
OutputContentBlock::Text { text } => {
|
||||||
Some(InputContentBlock::Text { text: text.clone() })
|
Some(InputContentBlock::Text { text: text.clone() })
|
||||||
}
|
}
|
||||||
OutputContentBlock::ToolUse { id, name, input } => Some(InputContentBlock::ToolUse {
|
OutputContentBlock::ToolUse { id, name, input, .. } => Some(InputContentBlock::ToolUse {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
input: input.clone(),
|
input: input.clone(),
|
||||||
|
thought_signature: None,
|
||||||
}),
|
}),
|
||||||
OutputContentBlock::Thinking { .. } | OutputContentBlock::RedactedThinking { .. } => {
|
OutputContentBlock::Thinking { .. } | OutputContentBlock::RedactedThinking { .. } => {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -746,6 +746,7 @@ fn tool_message_response_many(id: &str, tool_uses: &[ToolUseMessage<'_>]) -> Mes
|
|||||||
id: tool_use.tool_id.to_string(),
|
id: tool_use.tool_id.to_string(),
|
||||||
name: tool_use.tool_name.to_string(),
|
name: tool_use.tool_name.to_string(),
|
||||||
input: tool_use.input.clone(),
|
input: tool_use.input.clone(),
|
||||||
|
thought_signature: None,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
model: DEFAULT_MODEL.to_string(),
|
model: DEFAULT_MODEL.to_string(),
|
||||||
|
|||||||
@@ -780,6 +780,7 @@ mod tests {
|
|||||||
id: tool_id.to_string(),
|
id: tool_id.to_string(),
|
||||||
name: "search".to_string(),
|
name: "search".to_string(),
|
||||||
input: "{\"q\":\"*.rs\"}".to_string(),
|
input: "{\"q\":\"*.rs\"}".to_string(),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
]))
|
]))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ pub enum AssistantEvent {
|
|||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
input: String,
|
input: String,
|
||||||
|
thought_signature: Option<String>,
|
||||||
},
|
},
|
||||||
Usage(TokenUsage),
|
Usage(TokenUsage),
|
||||||
PromptCache(PromptCacheEvent),
|
PromptCache(PromptCacheEvent),
|
||||||
@@ -381,7 +382,7 @@ where
|
|||||||
.blocks
|
.blocks
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|block| match block {
|
.filter_map(|block| match block {
|
||||||
ContentBlock::ToolUse { id, name, input } => {
|
ContentBlock::ToolUse { id, name, input, .. } => {
|
||||||
Some((id.clone(), name.clone(), input.clone()))
|
Some((id.clone(), name.clone(), input.clone()))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -741,9 +742,9 @@ fn build_assistant_message(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
AssistantEvent::TextDelta(delta) => text.push_str(&delta),
|
AssistantEvent::TextDelta(delta) => text.push_str(&delta),
|
||||||
AssistantEvent::ToolUse { id, name, input } => {
|
AssistantEvent::ToolUse { id, name, input, thought_signature } => {
|
||||||
flush_text_block(&mut text, &mut blocks);
|
flush_text_block(&mut text, &mut blocks);
|
||||||
blocks.push(ContentBlock::ToolUse { id, name, input });
|
blocks.push(ContentBlock::ToolUse { id, name, input, thought_signature });
|
||||||
}
|
}
|
||||||
AssistantEvent::Usage(value) => usage = Some(value),
|
AssistantEvent::Usage(value) => usage = Some(value),
|
||||||
AssistantEvent::PromptCache(event) => prompt_cache_events.push(event),
|
AssistantEvent::PromptCache(event) => prompt_cache_events.push(event),
|
||||||
@@ -880,6 +881,7 @@ mod tests {
|
|||||||
id: "tool-1".to_string(),
|
id: "tool-1".to_string(),
|
||||||
name: "add".to_string(),
|
name: "add".to_string(),
|
||||||
input: "2,2".to_string(),
|
input: "2,2".to_string(),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
AssistantEvent::Usage(TokenUsage {
|
AssistantEvent::Usage(TokenUsage {
|
||||||
input_tokens: 20,
|
input_tokens: 20,
|
||||||
@@ -1046,6 +1048,7 @@ mod tests {
|
|||||||
id: "tool-1".to_string(),
|
id: "tool-1".to_string(),
|
||||||
name: "blocked".to_string(),
|
name: "blocked".to_string(),
|
||||||
input: "secret".to_string(),
|
input: "secret".to_string(),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
AssistantEvent::MessageStop,
|
AssistantEvent::MessageStop,
|
||||||
])
|
])
|
||||||
@@ -1091,6 +1094,7 @@ mod tests {
|
|||||||
id: "tool-1".to_string(),
|
id: "tool-1".to_string(),
|
||||||
name: "blocked".to_string(),
|
name: "blocked".to_string(),
|
||||||
input: r#"{"path":"secret.txt"}"#.to_string(),
|
input: r#"{"path":"secret.txt"}"#.to_string(),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
AssistantEvent::MessageStop,
|
AssistantEvent::MessageStop,
|
||||||
])
|
])
|
||||||
@@ -1153,6 +1157,7 @@ mod tests {
|
|||||||
id: "tool-1".to_string(),
|
id: "tool-1".to_string(),
|
||||||
name: "blocked".to_string(),
|
name: "blocked".to_string(),
|
||||||
input: r#"{"path":"secret.txt"}"#.to_string(),
|
input: r#"{"path":"secret.txt"}"#.to_string(),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
AssistantEvent::MessageStop,
|
AssistantEvent::MessageStop,
|
||||||
])
|
])
|
||||||
@@ -1213,6 +1218,7 @@ mod tests {
|
|||||||
id: "tool-1".to_string(),
|
id: "tool-1".to_string(),
|
||||||
name: "add".to_string(),
|
name: "add".to_string(),
|
||||||
input: r#"{"lhs":2,"rhs":2}"#.to_string(),
|
input: r#"{"lhs":2,"rhs":2}"#.to_string(),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
AssistantEvent::MessageStop,
|
AssistantEvent::MessageStop,
|
||||||
]),
|
]),
|
||||||
@@ -1288,6 +1294,7 @@ mod tests {
|
|||||||
id: "tool-1".to_string(),
|
id: "tool-1".to_string(),
|
||||||
name: "fail".to_string(),
|
name: "fail".to_string(),
|
||||||
input: r#"{"path":"README.md"}"#.to_string(),
|
input: r#"{"path":"README.md"}"#.to_string(),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
AssistantEvent::MessageStop,
|
AssistantEvent::MessageStop,
|
||||||
]),
|
]),
|
||||||
@@ -1755,6 +1762,7 @@ mod tests {
|
|||||||
id: "tool-1".to_string(),
|
id: "tool-1".to_string(),
|
||||||
name: "echo".to_string(),
|
name: "echo".to_string(),
|
||||||
input: "payload".to_string(),
|
input: "payload".to_string(),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
AssistantEvent::MessageStop,
|
AssistantEvent::MessageStop,
|
||||||
];
|
];
|
||||||
@@ -1778,6 +1786,7 @@ mod tests {
|
|||||||
id: "tool-1".to_string(),
|
id: "tool-1".to_string(),
|
||||||
name: "echo".to_string(),
|
name: "echo".to_string(),
|
||||||
input: "payload".to_string(),
|
input: "payload".to_string(),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -1811,6 +1820,7 @@ mod tests {
|
|||||||
id: "tool-1".to_string(),
|
id: "tool-1".to_string(),
|
||||||
name: "echo".to_string(),
|
name: "echo".to_string(),
|
||||||
input: "payload".to_string(),
|
input: "payload".to_string(),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
AssistantEvent::MessageStop,
|
AssistantEvent::MessageStop,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ pub enum ContentBlock {
|
|||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
input: String,
|
input: String,
|
||||||
|
thought_signature: Option<String>,
|
||||||
},
|
},
|
||||||
ToolResult {
|
ToolResult {
|
||||||
tool_use_id: String,
|
tool_use_id: String,
|
||||||
@@ -817,7 +818,7 @@ impl ContentBlock {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::ToolUse { id, name, input } => {
|
Self::ToolUse { id, name, input, thought_signature } => {
|
||||||
object.insert(
|
object.insert(
|
||||||
"type".to_string(),
|
"type".to_string(),
|
||||||
JsonValue::String("tool_use".to_string()),
|
JsonValue::String("tool_use".to_string()),
|
||||||
@@ -825,6 +826,12 @@ impl ContentBlock {
|
|||||||
object.insert("id".to_string(), JsonValue::String(id.clone()));
|
object.insert("id".to_string(), JsonValue::String(id.clone()));
|
||||||
object.insert("name".to_string(), JsonValue::String(name.clone()));
|
object.insert("name".to_string(), JsonValue::String(name.clone()));
|
||||||
object.insert("input".to_string(), JsonValue::String(input.clone()));
|
object.insert("input".to_string(), JsonValue::String(input.clone()));
|
||||||
|
if let Some(sig) = thought_signature {
|
||||||
|
object.insert(
|
||||||
|
"thought_signature".to_string(),
|
||||||
|
JsonValue::String(sig.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Self::ToolResult {
|
Self::ToolResult {
|
||||||
tool_use_id,
|
tool_use_id,
|
||||||
@@ -874,6 +881,7 @@ impl ContentBlock {
|
|||||||
id: required_string(object, "id")?,
|
id: required_string(object, "id")?,
|
||||||
name: required_string(object, "name")?,
|
name: required_string(object, "name")?,
|
||||||
input: required_string(object, "input")?,
|
input: required_string(object, "input")?,
|
||||||
|
thought_signature: object.get("thought_signature").and_then(JsonValue::as_str).map(String::from)
|
||||||
}),
|
}),
|
||||||
"tool_result" => Ok(Self::ToolResult {
|
"tool_result" => Ok(Self::ToolResult {
|
||||||
tool_use_id: required_string(object, "tool_use_id")?,
|
tool_use_id: required_string(object, "tool_use_id")?,
|
||||||
@@ -1069,7 +1077,7 @@ fn persisted_block_json(block: &ContentBlock) -> JsonValue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContentBlock::ToolUse { id, name, input } => {
|
ContentBlock::ToolUse { id, name, input, thought_signature } => {
|
||||||
object.insert(
|
object.insert(
|
||||||
"type".to_string(),
|
"type".to_string(),
|
||||||
JsonValue::String("tool_use".to_string()),
|
JsonValue::String("tool_use".to_string()),
|
||||||
@@ -1083,6 +1091,12 @@ fn persisted_block_json(block: &ContentBlock) -> JsonValue {
|
|||||||
"input".to_string(),
|
"input".to_string(),
|
||||||
JsonValue::String(sanitize_jsonl_field(input)),
|
JsonValue::String(sanitize_jsonl_field(input)),
|
||||||
);
|
);
|
||||||
|
if let Some(sig) = thought_signature {
|
||||||
|
object.insert(
|
||||||
|
"thought_signature".to_string(),
|
||||||
|
JsonValue::String(sanitize_jsonl_field(sig)),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ContentBlock::ToolResult {
|
ContentBlock::ToolResult {
|
||||||
tool_use_id,
|
tool_use_id,
|
||||||
@@ -1433,6 +1447,7 @@ mod tests {
|
|||||||
id: "tool-1".to_string(),
|
id: "tool-1".to_string(),
|
||||||
name: "bash".to_string(),
|
name: "bash".to_string(),
|
||||||
input: "echo hi".to_string(),
|
input: "echo hi".to_string(),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
Some(TokenUsage {
|
Some(TokenUsage {
|
||||||
@@ -1596,6 +1611,7 @@ mod tests {
|
|||||||
id: "tool-1".to_string(),
|
id: "tool-1".to_string(),
|
||||||
name: "bash".to_string(),
|
name: "bash".to_string(),
|
||||||
input: format!("Authorization: Bearer {secret}"),
|
input: format!("Authorization: Bearer {secret}"),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
]))
|
]))
|
||||||
.expect("tool use should append");
|
.expect("tool use should append");
|
||||||
|
|||||||
@@ -686,6 +686,7 @@ mod tests {
|
|||||||
id: "1".to_string(),
|
id: "1".to_string(),
|
||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
input: r#"{"path":"src/main.rs"}"#.to_string(),
|
input: r#"{"path":"src/main.rs"}"#.to_string(),
|
||||||
|
thought_signature: None,
|
||||||
}]),
|
}]),
|
||||||
ConversationMessage::tool_result(
|
ConversationMessage::tool_result(
|
||||||
"1",
|
"1",
|
||||||
@@ -697,6 +698,7 @@ mod tests {
|
|||||||
id: "2".to_string(),
|
id: "2".to_string(),
|
||||||
name: "edit_file".to_string(),
|
name: "edit_file".to_string(),
|
||||||
input: r#"{"path":"src/main.rs","old":"old","new":"new"}"#.to_string(),
|
input: r#"{"path":"src/main.rs","old":"old","new":"new"}"#.to_string(),
|
||||||
|
thought_signature: None,
|
||||||
}]),
|
}]),
|
||||||
ConversationMessage::tool_result(
|
ConversationMessage::tool_result(
|
||||||
"2",
|
"2",
|
||||||
@@ -718,6 +720,7 @@ mod tests {
|
|||||||
id: "1".to_string(),
|
id: "1".to_string(),
|
||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
input: r#"{"path":"src/main.rs"}"#.to_string(),
|
input: r#"{"path":"src/main.rs"}"#.to_string(),
|
||||||
|
thought_signature: None,
|
||||||
}]),
|
}]),
|
||||||
ConversationMessage::tool_result(
|
ConversationMessage::tool_result(
|
||||||
"1",
|
"1",
|
||||||
@@ -746,6 +749,7 @@ mod tests {
|
|||||||
id: "t".to_string(),
|
id: "t".to_string(),
|
||||||
name: "bash".to_string(),
|
name: "bash".to_string(),
|
||||||
input: r#"{"command":"ls"}"#.to_string(),
|
input: r#"{"command":"ls"}"#.to_string(),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
]));
|
]));
|
||||||
|
|
||||||
@@ -764,6 +768,7 @@ mod tests {
|
|||||||
id: format!("read_{i}"),
|
id: format!("read_{i}"),
|
||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
input: format!(r#"{{"path":"src/{i}.rs"}}"#),
|
input: format!(r#"{{"path":"src/{i}.rs"}}"#),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
]));
|
]));
|
||||||
messages.push(ConversationMessage::tool_result(
|
messages.push(ConversationMessage::tool_result(
|
||||||
@@ -789,6 +794,7 @@ mod tests {
|
|||||||
id: "1".to_string(),
|
id: "1".to_string(),
|
||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
input: r#"{"path":"src/main.rs"}"#.to_string(),
|
input: r#"{"path":"src/main.rs"}"#.to_string(),
|
||||||
|
thought_signature: None,
|
||||||
}]),
|
}]),
|
||||||
ConversationMessage::tool_result(
|
ConversationMessage::tool_result(
|
||||||
"1",
|
"1",
|
||||||
@@ -800,6 +806,7 @@ mod tests {
|
|||||||
id: "2".to_string(),
|
id: "2".to_string(),
|
||||||
name: "edit_file".to_string(),
|
name: "edit_file".to_string(),
|
||||||
input: r#"{"path":"src/main.rs","old":"buggy","new":"fixed"}"#.to_string(),
|
input: r#"{"path":"src/main.rs","old":"buggy","new":"fixed"}"#.to_string(),
|
||||||
|
thought_signature: None,
|
||||||
}]),
|
}]),
|
||||||
ConversationMessage::tool_result(
|
ConversationMessage::tool_result(
|
||||||
"2",
|
"2",
|
||||||
|
|||||||
@@ -10414,7 +10414,7 @@ fn render_last_tool_debug_report(session: &Session) -> Result<String, Box<dyn st
|
|||||||
.rev()
|
.rev()
|
||||||
.find_map(|message| {
|
.find_map(|message| {
|
||||||
message.blocks.iter().rev().find_map(|block| match block {
|
message.blocks.iter().rev().find_map(|block| match block {
|
||||||
ContentBlock::ToolUse { id, name, input } => {
|
ContentBlock::ToolUse { id, name, input, .. } => {
|
||||||
Some((id.clone(), name.clone(), input.clone()))
|
Some((id.clone(), name.clone(), input.clone()))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -10751,7 +10751,7 @@ fn render_export_text(session: &Session) -> String {
|
|||||||
match block {
|
match block {
|
||||||
ContentBlock::Text { text } => lines.push(text.clone()),
|
ContentBlock::Text { text } => lines.push(text.clone()),
|
||||||
ContentBlock::Thinking { .. } => {}
|
ContentBlock::Thinking { .. } => {}
|
||||||
ContentBlock::ToolUse { id, name, input } => {
|
ContentBlock::ToolUse { id, name, input, .. } => {
|
||||||
lines.push(format!("[tool_use id={id} name={name}] {input}"));
|
lines.push(format!("[tool_use id={id} name={name}] {input}"));
|
||||||
}
|
}
|
||||||
ContentBlock::ToolResult {
|
ContentBlock::ToolResult {
|
||||||
@@ -10989,7 +10989,7 @@ fn render_session_markdown(session: &Session, session_id: &str, session_path: &P
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContentBlock::Thinking { .. } => {}
|
ContentBlock::Thinking { .. } => {}
|
||||||
ContentBlock::ToolUse { id, name, input } => {
|
ContentBlock::ToolUse { id, name, input, .. } => {
|
||||||
lines.push(format!(
|
lines.push(format!(
|
||||||
"**Tool call** `{name}` _(id `{}`)_",
|
"**Tool call** `{name}` _(id `{}`)_",
|
||||||
short_tool_id(id)
|
short_tool_id(id)
|
||||||
@@ -11872,7 +11872,7 @@ impl AnthropicRuntimeClient {
|
|||||||
let renderer = TerminalRenderer::new();
|
let renderer = TerminalRenderer::new();
|
||||||
let mut markdown_stream = MarkdownStreamState::default();
|
let mut markdown_stream = MarkdownStreamState::default();
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
let mut pending_tool: Option<(String, String, String)> = None;
|
let mut pending_tool: Option<(String, String, String, Option<String>)> = None;
|
||||||
// 累积 reasoning_content 到 Thinking 块(修复 DeepSeek V4 reasoning_content 协议 bug)
|
// 累积 reasoning_content 到 Thinking 块(修复 DeepSeek V4 reasoning_content 协议 bug)
|
||||||
let mut pending_thinking: Option<(String, Option<String>)> = None;
|
let mut pending_thinking: Option<(String, Option<String>)> = None;
|
||||||
let mut block_has_thinking_summary = false;
|
let mut block_has_thinking_summary = false;
|
||||||
@@ -11948,7 +11948,7 @@ impl AnthropicRuntimeClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContentBlockDelta::InputJsonDelta { partial_json } => {
|
ContentBlockDelta::InputJsonDelta { partial_json } => {
|
||||||
if let Some((_, _, input)) = &mut pending_tool {
|
if let Some((_, _, input, _)) = &mut pending_tool {
|
||||||
input.push_str(&partial_json);
|
input.push_str(&partial_json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11983,7 +11983,7 @@ impl AnthropicRuntimeClient {
|
|||||||
signature,
|
signature,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if let Some((id, name, input)) = pending_tool.take() {
|
if let Some((id, name, input, thought_signature)) = pending_tool.take() {
|
||||||
if let Some(progress_reporter) = &self.progress_reporter {
|
if let Some(progress_reporter) = &self.progress_reporter {
|
||||||
progress_reporter.mark_tool_phase(&name, &input);
|
progress_reporter.mark_tool_phase(&name, &input);
|
||||||
}
|
}
|
||||||
@@ -11991,7 +11991,7 @@ impl AnthropicRuntimeClient {
|
|||||||
writeln!(out, "\n{}", format_tool_call_start(&name, &input))
|
writeln!(out, "\n{}", format_tool_call_start(&name, &input))
|
||||||
.and_then(|()| out.flush())
|
.and_then(|()| out.flush())
|
||||||
.map_err(|error| RuntimeError::new(error.to_string()))?;
|
.map_err(|error| RuntimeError::new(error.to_string()))?;
|
||||||
events.push(AssistantEvent::ToolUse { id, name, input });
|
events.push(AssistantEvent::ToolUse { id, name, input, thought_signature });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ApiStreamEvent::MessageDelta(delta) => {
|
ApiStreamEvent::MessageDelta(delta) => {
|
||||||
@@ -12167,7 +12167,7 @@ fn collect_tool_uses(summary: &runtime::TurnSummary) -> Vec<serde_json::Value> {
|
|||||||
.iter()
|
.iter()
|
||||||
.flat_map(|message| message.blocks.iter())
|
.flat_map(|message| message.blocks.iter())
|
||||||
.filter_map(|block| match block {
|
.filter_map(|block| match block {
|
||||||
ContentBlock::ToolUse { id, name, input } => Some(json!({
|
ContentBlock::ToolUse { id, name, input, .. } => Some(json!({
|
||||||
"id": id,
|
"id": id,
|
||||||
"name": name,
|
"name": name,
|
||||||
"input": input,
|
"input": input,
|
||||||
@@ -12868,7 +12868,7 @@ fn push_output_block(
|
|||||||
block: OutputContentBlock,
|
block: OutputContentBlock,
|
||||||
out: &mut (impl Write + ?Sized),
|
out: &mut (impl Write + ?Sized),
|
||||||
events: &mut Vec<AssistantEvent>,
|
events: &mut Vec<AssistantEvent>,
|
||||||
pending_tool: &mut Option<(String, String, String)>,
|
pending_tool: &mut Option<(String, String, String, Option<String>)>,
|
||||||
streaming_tool_input: bool,
|
streaming_tool_input: bool,
|
||||||
block_has_thinking_summary: &mut bool,
|
block_has_thinking_summary: &mut bool,
|
||||||
) -> Result<(), RuntimeError> {
|
) -> Result<(), RuntimeError> {
|
||||||
@@ -12882,7 +12882,7 @@ fn push_output_block(
|
|||||||
events.push(AssistantEvent::TextDelta(text));
|
events.push(AssistantEvent::TextDelta(text));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OutputContentBlock::ToolUse { id, name, input } => {
|
OutputContentBlock::ToolUse { id, name, input, thought_signature } => {
|
||||||
// During streaming, the initial content_block_start has an empty input ({}).
|
// During streaming, the initial content_block_start has an empty input ({}).
|
||||||
// The real input arrives via input_json_delta events. In
|
// The real input arrives via input_json_delta events. In
|
||||||
// non-streaming responses, preserve a legitimate empty object.
|
// non-streaming responses, preserve a legitimate empty object.
|
||||||
@@ -12894,7 +12894,7 @@ fn push_output_block(
|
|||||||
} else {
|
} else {
|
||||||
input.to_string()
|
input.to_string()
|
||||||
};
|
};
|
||||||
*pending_tool = Some((id, name, initial_input));
|
*pending_tool = Some((id, name, initial_input, thought_signature));
|
||||||
}
|
}
|
||||||
OutputContentBlock::Thinking { thinking, .. } => {
|
OutputContentBlock::Thinking { thinking, .. } => {
|
||||||
render_thinking_block_summary(out, Some(thinking.chars().count()), false)?;
|
render_thinking_block_summary(out, Some(thinking.chars().count()), false)?;
|
||||||
@@ -12925,8 +12925,8 @@ fn response_to_events(
|
|||||||
false,
|
false,
|
||||||
&mut block_has_thinking_summary,
|
&mut block_has_thinking_summary,
|
||||||
)?;
|
)?;
|
||||||
if let Some((id, name, input)) = pending_tool.take() {
|
if let Some((id, name, input, thought_signature)) = pending_tool.take() {
|
||||||
events.push(AssistantEvent::ToolUse { id, name, input });
|
events.push(AssistantEvent::ToolUse { id, name, input, thought_signature });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13130,11 +13130,12 @@ fn convert_messages(messages: &[ConversationMessage]) -> Vec<InputMessage> {
|
|||||||
signature: signature.clone(),
|
signature: signature.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ContentBlock::ToolUse { id, name, input } => Some(InputContentBlock::ToolUse {
|
ContentBlock::ToolUse { id, name, input, thought_signature, .. } => Some(InputContentBlock::ToolUse {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
input: serde_json::from_str(input)
|
input: serde_json::from_str(input)
|
||||||
.unwrap_or_else(|_| serde_json::json!({ "raw": input })),
|
.unwrap_or_else(|_| serde_json::json!({ "raw": input })),
|
||||||
|
thought_signature: thought_signature.clone(),
|
||||||
}),
|
}),
|
||||||
ContentBlock::ToolResult {
|
ContentBlock::ToolResult {
|
||||||
tool_use_id,
|
tool_use_id,
|
||||||
@@ -15728,6 +15729,7 @@ mod tests {
|
|||||||
id: "toolu_abcdefghijklmnop".to_string(),
|
id: "toolu_abcdefghijklmnop".to_string(),
|
||||||
name: "bash".to_string(),
|
name: "bash".to_string(),
|
||||||
input: r#"{"command":"ls -la"}"#.to_string(),
|
input: r#"{"command":"ls -la"}"#.to_string(),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
ConversationMessage {
|
ConversationMessage {
|
||||||
@@ -17669,6 +17671,7 @@ UU conflicted.rs",
|
|||||||
id: "tool-1".to_string(),
|
id: "tool-1".to_string(),
|
||||||
name: "bash".to_string(),
|
name: "bash".to_string(),
|
||||||
input: "{\"command\":\"pwd\"}".to_string(),
|
input: "{\"command\":\"pwd\"}".to_string(),
|
||||||
|
thought_signature: None,
|
||||||
}]),
|
}]),
|
||||||
ConversationMessage {
|
ConversationMessage {
|
||||||
role: MessageRole::Tool,
|
role: MessageRole::Tool,
|
||||||
@@ -18059,6 +18062,7 @@ UU conflicted.rs",
|
|||||||
id: "tool-1".to_string(),
|
id: "tool-1".to_string(),
|
||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
input: json!({}),
|
input: json!({}),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
&mut out,
|
&mut out,
|
||||||
&mut events,
|
&mut events,
|
||||||
@@ -18071,7 +18075,7 @@ UU conflicted.rs",
|
|||||||
assert!(events.is_empty());
|
assert!(events.is_empty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
pending_tool,
|
pending_tool,
|
||||||
Some(("tool-1".to_string(), "read_file".to_string(), String::new(),))
|
Some(("tool-1".to_string(), "read_file".to_string(), String::new(), None))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18088,6 +18092,7 @@ UU conflicted.rs",
|
|||||||
id: "tool-1".to_string(),
|
id: "tool-1".to_string(),
|
||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
input: json!({}),
|
input: json!({}),
|
||||||
|
thought_signature: None,
|
||||||
}],
|
}],
|
||||||
stop_reason: Some("tool_use".to_string()),
|
stop_reason: Some("tool_use".to_string()),
|
||||||
stop_sequence: None,
|
stop_sequence: None,
|
||||||
@@ -18123,6 +18128,7 @@ UU conflicted.rs",
|
|||||||
id: "tool-2".to_string(),
|
id: "tool-2".to_string(),
|
||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
input: json!({ "path": "rust/Cargo.toml" }),
|
input: json!({ "path": "rust/Cargo.toml" }),
|
||||||
|
thought_signature: None,
|
||||||
}],
|
}],
|
||||||
stop_reason: Some("tool_use".to_string()),
|
stop_reason: Some("tool_use".to_string()),
|
||||||
stop_sequence: None,
|
stop_sequence: None,
|
||||||
|
|||||||
@@ -5203,7 +5203,7 @@ async fn stream_with_provider(
|
|||||||
) -> Result<Vec<AssistantEvent>, ApiError> {
|
) -> Result<Vec<AssistantEvent>, ApiError> {
|
||||||
let mut stream = client.stream_message(message_request).await?;
|
let mut stream = client.stream_message(message_request).await?;
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
let mut pending_tools: BTreeMap<u32, (String, String, String)> = BTreeMap::new();
|
let mut pending_tools: BTreeMap<u32, (String, String, String, Option<String>)> = BTreeMap::new();
|
||||||
let mut pending_thinking: BTreeMap<u32, (String, Option<String>)> = BTreeMap::new();
|
let mut pending_thinking: BTreeMap<u32, (String, Option<String>)> = BTreeMap::new();
|
||||||
let mut saw_stop = false;
|
let mut saw_stop = false;
|
||||||
|
|
||||||
@@ -5238,7 +5238,7 @@ async fn stream_with_provider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContentBlockDelta::InputJsonDelta { partial_json } => {
|
ContentBlockDelta::InputJsonDelta { partial_json } => {
|
||||||
if let Some((_, _, input)) = pending_tools.get_mut(&delta.index) {
|
if let Some((_, _, input, _)) = pending_tools.get_mut(&delta.index) {
|
||||||
input.push_str(&partial_json);
|
input.push_str(&partial_json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5262,8 +5262,8 @@ async fn stream_with_provider(
|
|||||||
signature,
|
signature,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if let Some((id, name, input)) = pending_tools.remove(&stop.index) {
|
if let Some((id, name, input, thought_signature)) = pending_tools.remove(&stop.index) {
|
||||||
events.push(AssistantEvent::ToolUse { id, name, input });
|
events.push(AssistantEvent::ToolUse { id, name, input, thought_signature });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ApiStreamEvent::MessageDelta(delta) => {
|
ApiStreamEvent::MessageDelta(delta) => {
|
||||||
@@ -5371,11 +5371,12 @@ fn convert_messages(messages: &[ConversationMessage]) -> Vec<InputMessage> {
|
|||||||
thinking: thinking.clone(),
|
thinking: thinking.clone(),
|
||||||
signature: signature.clone(),
|
signature: signature.clone(),
|
||||||
},
|
},
|
||||||
ContentBlock::ToolUse { id, name, input } => InputContentBlock::ToolUse {
|
ContentBlock::ToolUse { id, name, input, thought_signature } => InputContentBlock::ToolUse {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
input: serde_json::from_str(input)
|
input: serde_json::from_str(input)
|
||||||
.unwrap_or_else(|_| serde_json::json!({ "raw": input })),
|
.unwrap_or_else(|_| serde_json::json!({ "raw": input })),
|
||||||
|
thought_signature: thought_signature.clone(),
|
||||||
},
|
},
|
||||||
ContentBlock::ToolResult {
|
ContentBlock::ToolResult {
|
||||||
tool_use_id,
|
tool_use_id,
|
||||||
@@ -5406,7 +5407,7 @@ fn push_output_block(
|
|||||||
block: OutputContentBlock,
|
block: OutputContentBlock,
|
||||||
block_index: u32,
|
block_index: u32,
|
||||||
events: &mut Vec<AssistantEvent>,
|
events: &mut Vec<AssistantEvent>,
|
||||||
pending_tools: &mut BTreeMap<u32, (String, String, String)>,
|
pending_tools: &mut BTreeMap<u32, (String, String, String, Option<String>)>,
|
||||||
pending_thinking: &mut BTreeMap<u32, (String, Option<String>)>,
|
pending_thinking: &mut BTreeMap<u32, (String, Option<String>)>,
|
||||||
streaming_tool_input: bool,
|
streaming_tool_input: bool,
|
||||||
) {
|
) {
|
||||||
@@ -5416,7 +5417,7 @@ fn push_output_block(
|
|||||||
events.push(AssistantEvent::TextDelta(text));
|
events.push(AssistantEvent::TextDelta(text));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OutputContentBlock::ToolUse { id, name, input } => {
|
OutputContentBlock::ToolUse { id, name, input, thought_signature } => {
|
||||||
let initial_input = if streaming_tool_input
|
let initial_input = if streaming_tool_input
|
||||||
&& input.is_object()
|
&& input.is_object()
|
||||||
&& input.as_object().is_some_and(serde_json::Map::is_empty)
|
&& input.as_object().is_some_and(serde_json::Map::is_empty)
|
||||||
@@ -5425,7 +5426,7 @@ fn push_output_block(
|
|||||||
} else {
|
} else {
|
||||||
input.to_string()
|
input.to_string()
|
||||||
};
|
};
|
||||||
pending_tools.insert(block_index, (id, name, initial_input));
|
pending_tools.insert(block_index, (id, name, initial_input, thought_signature));
|
||||||
}
|
}
|
||||||
OutputContentBlock::Thinking {
|
OutputContentBlock::Thinking {
|
||||||
thinking,
|
thinking,
|
||||||
@@ -5459,8 +5460,8 @@ fn response_to_events(response: MessageResponse) -> Vec<AssistantEvent> {
|
|||||||
&mut pending_thinking,
|
&mut pending_thinking,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
if let Some((id, name, input)) = pending_tools.remove(&index) {
|
if let Some((id, name, input, thought_signature)) = pending_tools.remove(&index) {
|
||||||
events.push(AssistantEvent::ToolUse { id, name, input });
|
events.push(AssistantEvent::ToolUse { id, name, input, thought_signature });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8066,6 +8067,7 @@ mod tests {
|
|||||||
id: "tool-1".to_string(),
|
id: "tool-1".to_string(),
|
||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
input: json!({}),
|
input: json!({}),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
1,
|
1,
|
||||||
&mut events,
|
&mut events,
|
||||||
@@ -8078,6 +8080,7 @@ mod tests {
|
|||||||
id: "tool-2".to_string(),
|
id: "tool-2".to_string(),
|
||||||
name: "grep_search".to_string(),
|
name: "grep_search".to_string(),
|
||||||
input: json!({}),
|
input: json!({}),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
2,
|
2,
|
||||||
&mut events,
|
&mut events,
|
||||||
@@ -9358,6 +9361,7 @@ mod tests {
|
|||||||
id: "tool-1".to_string(),
|
id: "tool-1".to_string(),
|
||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
input: json!({ "path": self.input_path }).to_string(),
|
input: json!({ "path": self.input_path }).to_string(),
|
||||||
|
thought_signature: None,
|
||||||
},
|
},
|
||||||
AssistantEvent::MessageStop,
|
AssistantEvent::MessageStop,
|
||||||
])
|
])
|
||||||
|
|||||||
Reference in New Issue
Block a user