mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 09:37:37 +08:00
Merge remote-tracking branch 'origin/main' into feature/managed-oauth-account-selector
# Conflicts: # src/components/providers/forms/ProviderForm.tsx
This commit is contained in:
@@ -1350,7 +1350,7 @@ impl RequestForwarder {
|
||||
.await;
|
||||
if restored > 0 {
|
||||
log::debug!(
|
||||
"[Codex] Restored {restored} cached function call(s) for Chat upstream"
|
||||
"[Codex] Restored or enriched {restored} cached function call item(s) for Chat upstream"
|
||||
);
|
||||
}
|
||||
super::providers::apply_codex_chat_upstream_model(provider, &mut mapped_body);
|
||||
|
||||
@@ -145,6 +145,77 @@ pub fn normalize_anthropic_tool_thinking_history_for_provider(
|
||||
normalize_anthropic_tool_thinking_history(body)
|
||||
}
|
||||
|
||||
/// DeepSeek official Anthropic-compatible endpoint URL
|
||||
const DEEPSEEK_OFFICIAL_ANTHROPIC_URL: &str = "https://api.deepseek.com/anthropic";
|
||||
|
||||
/// Check whether the provider is configured to use DeepSeek's official
|
||||
/// Anthropic-compatible endpoint.
|
||||
fn is_deepseek_official_anthropic_endpoint(provider: &Provider) -> bool {
|
||||
let settings = &provider.settings_config;
|
||||
let base_url = settings
|
||||
.get("env")
|
||||
.and_then(|env| env.get("ANTHROPIC_BASE_URL"))
|
||||
.and_then(|v| v.as_str())
|
||||
.or_else(|| settings.get("base_url").and_then(|v| v.as_str()))
|
||||
.or_else(|| settings.get("baseURL").and_then(|v| v.as_str()))
|
||||
.or_else(|| settings.get("apiEndpoint").and_then(|v| v.as_str()));
|
||||
|
||||
base_url.map(|u| u.trim_end_matches('/')) == Some(DEEPSEEK_OFFICIAL_ANTHROPIC_URL)
|
||||
}
|
||||
|
||||
/// DeepSeek's official Anthropic-compatible endpoint treats
|
||||
/// `thinking: { type: "disabled" }` and effort parameters (`output_config.effort`
|
||||
/// or `reasoning_effort`) as mutually exclusive, returning HTTP 400:
|
||||
/// "thinking options type cannot be disabled when reasoning_effort is set".
|
||||
/// This breaks Claude Code 2.1.166+ Workflow/Dynamic Workflow features.
|
||||
///
|
||||
/// Rather than overriding Claude Code's intentional `thinking: disabled` for
|
||||
/// sub-agents, we respect that decision and remove the conflicting effort
|
||||
/// parameters instead. `thinking: disabled` means "don't output thinking
|
||||
/// blocks", which is the correct behavior for sub-agents that don't need
|
||||
/// to display reasoning to the user.
|
||||
///
|
||||
/// <https://github.com/deepseek-ai/DeepSeek-V3/issues/1397>
|
||||
pub fn normalize_deepseek_thinking_disabled_strip_effort(
|
||||
body: &mut Value,
|
||||
provider: &Provider,
|
||||
) -> bool {
|
||||
if !is_deepseek_official_anthropic_endpoint(provider) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let thinking_type = body
|
||||
.get("thinking")
|
||||
.and_then(|t| t.get("type"))
|
||||
.and_then(|t| t.as_str());
|
||||
|
||||
if thinking_type != Some("disabled") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut changed = false;
|
||||
|
||||
// Remove output_config.effort (Anthropic format)
|
||||
if let Some(oc) = body
|
||||
.get_mut("output_config")
|
||||
.and_then(|v| v.as_object_mut())
|
||||
{
|
||||
changed |= oc.remove("effort").is_some();
|
||||
// Clean up empty output_config
|
||||
if oc.is_empty() {
|
||||
body.as_object_mut().unwrap().remove("output_config");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove reasoning_effort (OpenAI format, may be present in passthrough)
|
||||
if body.get("reasoning_effort").is_some() {
|
||||
body.as_object_mut().unwrap().remove("reasoning_effort");
|
||||
changed = true;
|
||||
}
|
||||
|
||||
changed
|
||||
}
|
||||
|
||||
pub fn normalize_anthropic_messages_for_provider(
|
||||
body: &mut Value,
|
||||
provider: &Provider,
|
||||
@@ -154,77 +225,12 @@ pub fn normalize_anthropic_messages_for_provider(
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut changed = normalize_anthropic_system_role_messages(body);
|
||||
changed |= normalize_anthropic_tool_thinking_history_for_provider(body, provider, api_format);
|
||||
let mut changed =
|
||||
normalize_anthropic_tool_thinking_history_for_provider(body, provider, api_format);
|
||||
changed |= normalize_deepseek_thinking_disabled_strip_effort(body, provider);
|
||||
changed
|
||||
}
|
||||
|
||||
fn normalize_anthropic_system_role_messages(body: &mut Value) -> bool {
|
||||
let mut system_parts = Vec::new();
|
||||
let changed = {
|
||||
let Some(messages) = body.get_mut("messages").and_then(Value::as_array_mut) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let original_len = messages.len();
|
||||
let mut kept_messages = Vec::with_capacity(messages.len());
|
||||
for message in std::mem::take(messages) {
|
||||
if message.get("role").and_then(Value::as_str) == Some("system") {
|
||||
if let Some(content) = message.get("content") {
|
||||
append_anthropic_system_parts(content, &mut system_parts);
|
||||
}
|
||||
} else {
|
||||
kept_messages.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
let changed = kept_messages.len() != original_len;
|
||||
*messages = kept_messages;
|
||||
changed
|
||||
};
|
||||
|
||||
if !changed || system_parts.is_empty() {
|
||||
return changed;
|
||||
}
|
||||
|
||||
let mut merged_parts = Vec::new();
|
||||
if let Some(existing) = body.get("system") {
|
||||
append_anthropic_system_parts(existing, &mut merged_parts);
|
||||
}
|
||||
merged_parts.extend(system_parts);
|
||||
|
||||
if !merged_parts.is_empty() {
|
||||
body["system"] = Value::Array(merged_parts);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn append_anthropic_system_parts(content: &Value, parts: &mut Vec<Value>) {
|
||||
match content {
|
||||
Value::String(text) if !text.trim().is_empty() => {
|
||||
parts.push(json!({
|
||||
"type": "text",
|
||||
"text": text
|
||||
}));
|
||||
}
|
||||
Value::Array(items) => {
|
||||
for item in items {
|
||||
append_anthropic_system_parts(item, parts);
|
||||
}
|
||||
}
|
||||
Value::Object(obj)
|
||||
if obj
|
||||
.get("text")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|text| !text.trim().is_empty()) =>
|
||||
{
|
||||
parts.push(Value::Object(obj.clone()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_anthropic_tool_thinking_history(body: &mut Value) -> bool {
|
||||
let Some(messages) = body.get_mut("messages").and_then(Value::as_array_mut) else {
|
||||
return false;
|
||||
@@ -2117,7 +2123,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anthropic_system_role_messages_move_to_top_level_system() {
|
||||
fn test_anthropic_messages_no_longer_hoists_system_role_messages() {
|
||||
// After reverting #3775, role=system messages are left in `messages[]`
|
||||
// (DeepSeek's endpoint accepts them natively) and the top-level `system`
|
||||
// field is untouched, preserving the request prefix.
|
||||
let provider = create_provider(json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.deepseek.com/anthropic",
|
||||
@@ -2139,15 +2148,13 @@ mod tests {
|
||||
|
||||
let changed = normalize_anthropic_messages_for_provider(&mut body, &provider, "anthropic");
|
||||
|
||||
assert!(changed);
|
||||
assert!(!changed);
|
||||
let messages = body["messages"].as_array().unwrap();
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(messages[0]["role"], "user");
|
||||
|
||||
let system = body["system"].as_array().unwrap();
|
||||
assert_eq!(system[0]["text"], "Existing top-level system.");
|
||||
assert_eq!(system[1]["text"], "Message system one.");
|
||||
assert_eq!(system[2]["text"], "Message system two.");
|
||||
assert_eq!(messages.len(), 3);
|
||||
assert_eq!(messages[0]["role"], "system");
|
||||
assert_eq!(messages[1]["role"], "user");
|
||||
assert_eq!(messages[2]["role"], "system");
|
||||
assert_eq!(body["system"], "Existing top-level system.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2300,4 +2307,245 @@ mod tests {
|
||||
assert!(!changed);
|
||||
assert_eq!(body, original);
|
||||
}
|
||||
|
||||
// ==================== normalize_deepseek_thinking_disabled_strip_effort 测试 ====================
|
||||
|
||||
fn deepseek_official_provider() -> Provider {
|
||||
create_provider(json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.deepseek.com/anthropic",
|
||||
"ANTHROPIC_API_KEY": "test-key"
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deepseek_official_strips_output_config_effort() {
|
||||
let mut body = json!({
|
||||
"model": "deepseek-v4-pro",
|
||||
"thinking": { "type": "disabled" },
|
||||
"output_config": { "effort": "max" },
|
||||
"max_tokens": 100000
|
||||
});
|
||||
|
||||
let changed = normalize_deepseek_thinking_disabled_strip_effort(
|
||||
&mut body,
|
||||
&deepseek_official_provider(),
|
||||
);
|
||||
|
||||
assert!(changed);
|
||||
assert_eq!(body["thinking"]["type"], "disabled");
|
||||
assert!(body.get("output_config").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deepseek_official_strips_reasoning_effort() {
|
||||
let mut body = json!({
|
||||
"model": "deepseek-v4-pro",
|
||||
"thinking": { "type": "disabled" },
|
||||
"reasoning_effort": "high",
|
||||
"max_tokens": 100000
|
||||
});
|
||||
|
||||
let changed = normalize_deepseek_thinking_disabled_strip_effort(
|
||||
&mut body,
|
||||
&deepseek_official_provider(),
|
||||
);
|
||||
|
||||
assert!(changed);
|
||||
assert_eq!(body["thinking"]["type"], "disabled");
|
||||
assert!(body.get("reasoning_effort").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deepseek_official_strips_both_effort_fields() {
|
||||
let mut body = json!({
|
||||
"model": "deepseek-v4-pro",
|
||||
"thinking": { "type": "disabled" },
|
||||
"output_config": { "effort": "max" },
|
||||
"reasoning_effort": "high",
|
||||
"max_tokens": 100000
|
||||
});
|
||||
|
||||
let changed = normalize_deepseek_thinking_disabled_strip_effort(
|
||||
&mut body,
|
||||
&deepseek_official_provider(),
|
||||
);
|
||||
|
||||
assert!(changed);
|
||||
assert_eq!(body["thinking"]["type"], "disabled");
|
||||
assert!(body.get("output_config").is_none());
|
||||
assert!(body.get("reasoning_effort").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deepseek_official_no_effort_no_change() {
|
||||
let mut body = json!({
|
||||
"model": "deepseek-v4-pro",
|
||||
"thinking": { "type": "disabled" },
|
||||
"max_tokens": 100000
|
||||
});
|
||||
let original = body.clone();
|
||||
|
||||
let changed = normalize_deepseek_thinking_disabled_strip_effort(
|
||||
&mut body,
|
||||
&deepseek_official_provider(),
|
||||
);
|
||||
|
||||
assert!(!changed);
|
||||
assert_eq!(body, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deepseek_official_preserves_output_config_other_fields() {
|
||||
let mut body = json!({
|
||||
"model": "deepseek-v4-pro",
|
||||
"thinking": { "type": "disabled" },
|
||||
"output_config": { "effort": "max", "temperature": 0.5 },
|
||||
"max_tokens": 100000
|
||||
});
|
||||
|
||||
let changed = normalize_deepseek_thinking_disabled_strip_effort(
|
||||
&mut body,
|
||||
&deepseek_official_provider(),
|
||||
);
|
||||
|
||||
assert!(changed);
|
||||
assert_eq!(body["output_config"]["temperature"], 0.5);
|
||||
assert!(body["output_config"].get("effort").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deepseek_official_non_disabled_not_modified() {
|
||||
let cases = vec![
|
||||
(
|
||||
"enabled",
|
||||
json!({ "type": "enabled", "budget_tokens": 16000 }),
|
||||
),
|
||||
("adaptive", json!({ "type": "adaptive" })),
|
||||
];
|
||||
|
||||
for (label, thinking_value) in cases {
|
||||
let mut body = json!({
|
||||
"model": "deepseek-v4-pro",
|
||||
"thinking": thinking_value,
|
||||
"output_config": { "effort": "max" },
|
||||
"max_tokens": 100000
|
||||
});
|
||||
let original = body.clone();
|
||||
|
||||
let changed = normalize_deepseek_thinking_disabled_strip_effort(
|
||||
&mut body,
|
||||
&deepseek_official_provider(),
|
||||
);
|
||||
|
||||
assert!(!changed, "should not modify thinking.type={label}");
|
||||
assert_eq!(body, original);
|
||||
}
|
||||
|
||||
// missing thinking field entirely
|
||||
let mut body = json!({
|
||||
"model": "deepseek-v4-pro",
|
||||
"output_config": { "effort": "max" },
|
||||
"max_tokens": 100000
|
||||
});
|
||||
let original = body.clone();
|
||||
assert!(!normalize_deepseek_thinking_disabled_strip_effort(
|
||||
&mut body,
|
||||
&deepseek_official_provider()
|
||||
));
|
||||
assert_eq!(body, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deepseek_official_url_with_trailing_slash() {
|
||||
let provider = create_provider(json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.deepseek.com/anthropic/",
|
||||
"ANTHROPIC_API_KEY": "test-key"
|
||||
}
|
||||
}));
|
||||
let mut body = json!({
|
||||
"model": "deepseek-v4-pro",
|
||||
"thinking": { "type": "disabled" },
|
||||
"output_config": { "effort": "max" },
|
||||
"max_tokens": 100000
|
||||
});
|
||||
|
||||
let changed = normalize_deepseek_thinking_disabled_strip_effort(&mut body, &provider);
|
||||
|
||||
assert!(changed);
|
||||
assert!(body.get("output_config").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deepseek_official_detected_via_base_url_fallback() {
|
||||
let provider = create_provider(json!({
|
||||
"base_url": "https://api.deepseek.com/anthropic",
|
||||
"ANTHROPIC_API_KEY": "test-key"
|
||||
}));
|
||||
let mut body = json!({
|
||||
"model": "deepseek-v4-pro",
|
||||
"thinking": { "type": "disabled" },
|
||||
"reasoning_effort": "high",
|
||||
"max_tokens": 100000
|
||||
});
|
||||
|
||||
let changed = normalize_deepseek_thinking_disabled_strip_effort(&mut body, &provider);
|
||||
|
||||
assert!(changed);
|
||||
assert!(body.get("reasoning_effort").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_deepseek_endpoint_not_modified() {
|
||||
let providers = vec![
|
||||
create_provider(json!({
|
||||
"env": { "ANTHROPIC_BASE_URL": "https://other-api.com/anthropic", "ANTHROPIC_API_KEY": "test-key" }
|
||||
})),
|
||||
create_provider(json!({
|
||||
"env": { "ANTHROPIC_BASE_URL": "https://api.anthropic.com", "ANTHROPIC_API_KEY": "test-key" }
|
||||
})),
|
||||
];
|
||||
|
||||
for provider in providers {
|
||||
let mut body = json!({
|
||||
"model": "deepseek-v4-pro",
|
||||
"thinking": { "type": "disabled" },
|
||||
"output_config": { "effort": "max" },
|
||||
"max_tokens": 100000
|
||||
});
|
||||
let original = body.clone();
|
||||
|
||||
let changed = normalize_deepseek_thinking_disabled_strip_effort(&mut body, &provider);
|
||||
|
||||
assert!(
|
||||
!changed,
|
||||
"should not modify for {}",
|
||||
provider.settings_config["env"]["ANTHROPIC_BASE_URL"]
|
||||
);
|
||||
assert_eq!(body, original);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_messages_pipeline_strips_effort_for_deepseek() {
|
||||
let mut body = json!({
|
||||
"model": "deepseek-v4-pro",
|
||||
"thinking": { "type": "disabled" },
|
||||
"output_config": { "effort": "max" },
|
||||
"max_tokens": 100000,
|
||||
"messages": [{ "role": "user", "content": "hello" }]
|
||||
});
|
||||
|
||||
let changed = normalize_anthropic_messages_for_provider(
|
||||
&mut body,
|
||||
&deepseek_official_provider(),
|
||||
"anthropic",
|
||||
);
|
||||
|
||||
assert!(changed);
|
||||
assert_eq!(body["thinking"]["type"], "disabled");
|
||||
assert!(body.get("output_config").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ impl CodexChatHistoryStore {
|
||||
Some(item_type) if is_call_item_type(item_type) => {
|
||||
if let Some(call_id) = response_item_call_id(&item) {
|
||||
if let Some(cached) = lookup.call(&call_id) {
|
||||
if enrich_call_item_reasoning(&mut item, cached) {
|
||||
if enrich_call_item_from_cache(&mut item, cached) {
|
||||
enriched += 1;
|
||||
}
|
||||
}
|
||||
@@ -466,17 +466,26 @@ fn is_call_output_item_type(item_type: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
fn enrich_call_item_reasoning(item: &mut Value, cached: &Value) -> bool {
|
||||
fn enrich_call_item_from_cache(item: &mut Value, cached: &Value) -> bool {
|
||||
let mut changed = false;
|
||||
for key in ["reasoning_content", "reasoning"] {
|
||||
for key in [
|
||||
"name",
|
||||
"namespace",
|
||||
"arguments",
|
||||
"input",
|
||||
"status",
|
||||
"execution",
|
||||
"reasoning_content",
|
||||
"reasoning",
|
||||
] {
|
||||
if item.get(key).is_some_and(|value| !is_empty_value(value)) {
|
||||
continue;
|
||||
}
|
||||
let Some(reasoning) = cached.get(key).filter(|value| !is_empty_value(value)) else {
|
||||
let Some(value) = cached.get(key).filter(|value| !is_empty_value(value)) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(object) = item.as_object_mut() {
|
||||
object.insert(key.to_string(), reasoning.clone());
|
||||
object.insert(key.to_string(), value.clone());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
@@ -675,6 +684,48 @@ mod tests {
|
||||
assert_eq!(input.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn enriches_existing_function_call_missing_name_and_arguments() {
|
||||
let history = CodexChatHistoryStore::default();
|
||||
history
|
||||
.record_response(&json!({
|
||||
"id": "resp_1",
|
||||
"output": [
|
||||
{
|
||||
"type": "function_call",
|
||||
"call_id": "call_1",
|
||||
"name": "read_file",
|
||||
"arguments": "{\"path\":\"README.md\"}",
|
||||
"reasoning_content": "Need to inspect the file."
|
||||
}
|
||||
]
|
||||
}))
|
||||
.await;
|
||||
|
||||
let mut request = json!({
|
||||
"previous_response_id": "resp_1",
|
||||
"input": [
|
||||
{
|
||||
"type": "function_call",
|
||||
"call_id": "call_1"
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "call_1",
|
||||
"output": "ok"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
assert_eq!(history.enrich_request(&mut request).await, 1);
|
||||
let input = request["input"].as_array().unwrap();
|
||||
assert_eq!(input[0]["type"], "function_call");
|
||||
assert_eq!(input[0]["name"], "read_file");
|
||||
assert_eq!(input[0]["arguments"], "{\"path\":\"README.md\"}");
|
||||
assert_eq!(input[0]["reasoning_content"], "Need to inspect the file.");
|
||||
assert_eq!(input[1]["type"], "function_call_output");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn restores_parallel_tool_calls_as_one_assistant_group() {
|
||||
let history = CodexChatHistoryStore::default();
|
||||
|
||||
@@ -429,8 +429,10 @@ impl ChatToResponsesState {
|
||||
if let Some(id) = id_delta {
|
||||
state.call_id = id;
|
||||
}
|
||||
if let Some(name) = name_delta {
|
||||
state.name = name;
|
||||
if let Some(ref name) = name_delta {
|
||||
if !name.is_empty() {
|
||||
state.name.clone_from(name);
|
||||
}
|
||||
}
|
||||
if !args_delta.is_empty() {
|
||||
state.arguments.push_str(&args_delta);
|
||||
@@ -442,7 +444,7 @@ impl ChatToResponsesState {
|
||||
}
|
||||
}
|
||||
|
||||
if !state.added && (!state.call_id.is_empty() || !state.name.is_empty()) {
|
||||
if !state.added && !state.call_id.is_empty() && !state.name.is_empty() {
|
||||
should_add = true;
|
||||
pending_arguments = state.arguments.clone();
|
||||
} else if state.added {
|
||||
@@ -464,9 +466,6 @@ impl ChatToResponsesState {
|
||||
if state.call_id.is_empty() {
|
||||
state.call_id = format!("call_{chat_index}");
|
||||
}
|
||||
if state.name.is_empty() {
|
||||
state.name = "unknown_tool".to_string();
|
||||
}
|
||||
state.output_index = Some(assigned);
|
||||
let is_custom_tool = self.tool_context.is_custom_tool_chat_name(&state.name);
|
||||
state.item_id = response_tool_call_item_id_from_chat_name(
|
||||
@@ -699,6 +698,21 @@ impl ChatToResponsesState {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip tool calls with missing names (defensive: some models generate
|
||||
// tool call deltas without providing a valid function name)
|
||||
let has_bad_name = self
|
||||
.tools
|
||||
.get(&key)
|
||||
.map(|state| state.name.is_empty())
|
||||
.unwrap_or(true);
|
||||
if has_bad_name {
|
||||
if let Some(state) = self.tools.get_mut(&key) {
|
||||
state.done = true;
|
||||
}
|
||||
log::warn!("[Codex] Skipping streaming tool call with missing name");
|
||||
continue;
|
||||
}
|
||||
|
||||
if self
|
||||
.tools
|
||||
.get(&key)
|
||||
@@ -713,9 +727,6 @@ impl ChatToResponsesState {
|
||||
if state.call_id.is_empty() {
|
||||
state.call_id = format!("call_{key}");
|
||||
}
|
||||
if state.name.is_empty() {
|
||||
state.name = "unknown_tool".to_string();
|
||||
}
|
||||
state.output_index = Some(assigned);
|
||||
state.item_id = response_tool_call_item_id_from_chat_name(
|
||||
&state.call_id,
|
||||
|
||||
@@ -1398,6 +1398,14 @@ fn chat_tool_calls_to_response_output_items(
|
||||
|
||||
if let Some(tool_calls) = message.get("tool_calls").and_then(|v| v.as_array()) {
|
||||
for (index, tool_call) in tool_calls.iter().enumerate() {
|
||||
// Skip tool calls with missing function names (defensive: some models
|
||||
// may generate tool calls without providing a valid name)
|
||||
let function = tool_call.get("function").unwrap_or(&Value::Null);
|
||||
let name = function.get("name").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if name.is_empty() {
|
||||
log::warn!("[Codex] Skipping tool call with missing name");
|
||||
continue;
|
||||
}
|
||||
output.push(chat_tool_call_to_response_item(
|
||||
tool_call,
|
||||
index,
|
||||
@@ -1406,11 +1414,11 @@ fn chat_tool_calls_to_response_output_items(
|
||||
));
|
||||
}
|
||||
} else if let Some(function_call) = message.get("function_call") {
|
||||
output.push(chat_legacy_function_call_to_response_item(
|
||||
function_call,
|
||||
reasoning,
|
||||
tool_context,
|
||||
));
|
||||
if let Some(item) =
|
||||
chat_legacy_function_call_to_response_item(function_call, reasoning, tool_context)
|
||||
{
|
||||
output.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
@@ -1448,7 +1456,7 @@ fn chat_legacy_function_call_to_response_item(
|
||||
function_call: &Value,
|
||||
reasoning: Option<&str>,
|
||||
tool_context: &CodexToolContext,
|
||||
) -> Value {
|
||||
) -> Option<Value> {
|
||||
let call_id = function_call
|
||||
.get("id")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -1458,10 +1466,18 @@ fn chat_legacy_function_call_to_response_item(
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
// Skip legacy function calls with missing names (defensive: some models
|
||||
// may generate function_call without providing a valid name)
|
||||
if name.is_empty() {
|
||||
log::warn!("[Codex] Skipping legacy function_call with missing name");
|
||||
return None;
|
||||
}
|
||||
|
||||
let arguments = canonicalize_tool_arguments(function_call.get("arguments"));
|
||||
|
||||
let item_id = response_tool_call_item_id_from_chat_name(call_id, name, tool_context);
|
||||
response_tool_call_item_from_chat_name(
|
||||
Some(response_tool_call_item_from_chat_name(
|
||||
&item_id,
|
||||
"completed",
|
||||
call_id,
|
||||
@@ -1469,7 +1485,7 @@ fn chat_legacy_function_call_to_response_item(
|
||||
&arguments,
|
||||
reasoning,
|
||||
tool_context,
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn response_tool_call_item_id_from_chat_name(
|
||||
|
||||
Reference in New Issue
Block a user