diff --git a/internal/translator/openai/claude/openai_claude_request.go b/internal/translator/openai/claude/openai_claude_request.go index 5a769ca41..2498f2f6e 100644 --- a/internal/translator/openai/claude/openai_claude_request.go +++ b/internal/translator/openai/claude/openai_claude_request.go @@ -313,7 +313,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream // Convert Anthropic input_schema to OpenAI function parameters if inputSchema := tool.Get("input_schema"); inputSchema.Exists() { - openAIToolJSON, _ = sjson.SetBytes(openAIToolJSON, "function.parameters", inputSchema.Value()) + openAIToolJSON, _ = sjson.SetBytes(openAIToolJSON, "function.parameters", normalizeObjectSchemaProperties(inputSchema.Value())) } toolsJSON, _ = sjson.SetRawBytes(toolsJSON, "-1", openAIToolJSON) @@ -352,6 +352,28 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream return out } +func normalizeObjectSchemaProperties(schema any) any { + switch value := schema.(type) { + case map[string]any: + if schemaType, ok := value["type"].(string); ok && schemaType == "object" { + if _, ok := value["properties"]; !ok { + value["properties"] = map[string]any{} + } + } + for key, child := range value { + value[key] = normalizeObjectSchemaProperties(child) + } + return value + case []any: + for i, child := range value { + value[i] = normalizeObjectSchemaProperties(child) + } + return value + default: + return schema + } +} + func shouldMapClaudeThinkingToGPTReasoning(part gjson.Result) bool { signature := part.Get("signature") if !signature.Exists() || strings.TrimSpace(signature.String()) == "" { diff --git a/internal/translator/openai/claude/openai_claude_request_test.go b/internal/translator/openai/claude/openai_claude_request_test.go index 34de754de..cbc57b527 100644 --- a/internal/translator/openai/claude/openai_claude_request_test.go +++ b/internal/translator/openai/claude/openai_claude_request_test.go @@ -496,6 +496,47 @@ func TestConvertClaudeRequestToOpenAI_SystemMessageScenarios(t *testing.T) { } } +func TestConvertClaudeRequestToOpenAI_ToolSchemaAddsMissingObjectProperties(t *testing.T) { + inputJSON := []byte(`{ + "model": "claude-3-opus", + "tools": [ + { + "name": "empty_params", + "description": "No args", + "input_schema": {"type": "object"} + }, + { + "name": "nested_params", + "description": "Nested args", + "input_schema": { + "type": "object", + "properties": { + "nested": {"type": "object"}, + "items": { + "type": "array", + "items": {"type": "object"} + } + } + } + } + ], + "messages": [{"role": "user", "content": "hello"}] + }`) + + output := ConvertClaudeRequestToOpenAI("test-model", inputJSON, false) + outputJSON := gjson.ParseBytes(output) + + if got := outputJSON.Get("tools.0.function.parameters.properties"); !got.Exists() || !got.IsObject() { + t.Fatalf("root object properties missing or invalid: %s", outputJSON.Get("tools.0.function.parameters").Raw) + } + if got := outputJSON.Get("tools.1.function.parameters.properties.nested.properties"); !got.Exists() || !got.IsObject() { + t.Fatalf("nested object properties missing or invalid: %s", outputJSON.Get("tools.1.function.parameters").Raw) + } + if got := outputJSON.Get("tools.1.function.parameters.properties.items.items.properties"); !got.Exists() || !got.IsObject() { + t.Fatalf("array item object properties missing or invalid: %s", outputJSON.Get("tools.1.function.parameters").Raw) + } +} + func TestConvertClaudeRequestToOpenAI_ToolResultOrderAndContent(t *testing.T) { inputJSON := `{ "model": "claude-3-opus",