responses_to_anthropic_tools_test.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. package apicompat
  2. import (
  3. "encoding/json"
  4. "testing"
  5. "github.com/stretchr/testify/assert"
  6. "github.com/stretchr/testify/require"
  7. )
  8. func requireObjectInputSchema(t *testing.T, schema json.RawMessage) map[string]json.RawMessage {
  9. t.Helper()
  10. require.NotEmpty(t, schema)
  11. var parsed map[string]json.RawMessage
  12. require.NoError(t, json.Unmarshal(schema, &parsed))
  13. require.JSONEq(t, `"object"`, string(parsed["type"]))
  14. require.Contains(t, parsed, "properties")
  15. var properties map[string]json.RawMessage
  16. require.NoError(t, json.Unmarshal(parsed["properties"], &properties))
  17. return parsed
  18. }
  19. func TestResponsesToAnthropic_CustomGrammarToolUsesObjectSchema(t *testing.T) {
  20. body := []byte(`{
  21. "model": "gpt-5.2",
  22. "input": "apply this patch",
  23. "tools": [{
  24. "type": "custom",
  25. "name": "apply_patch",
  26. "description": "Apply a patch to the working tree",
  27. "format": {
  28. "type": "grammar",
  29. "syntax": "lark",
  30. "definition": "start: /.+/"
  31. }
  32. }]
  33. }`)
  34. var req ResponsesRequest
  35. require.NoError(t, json.Unmarshal(body, &req))
  36. anthropicReq, err := ResponsesToAnthropicRequest(&req)
  37. require.NoError(t, err)
  38. require.Len(t, anthropicReq.Tools, 1)
  39. tool := anthropicReq.Tools[0]
  40. assert.Empty(t, tool.Type)
  41. assert.Equal(t, "apply_patch", tool.Name)
  42. assert.Equal(t, "Apply a patch to the working tree", tool.Description)
  43. requireObjectInputSchema(t, tool.InputSchema)
  44. assert.JSONEq(t, `{"type":"object","properties":{}}`, string(tool.InputSchema))
  45. wire, err := json.Marshal(tool)
  46. require.NoError(t, err)
  47. assert.NotContains(t, string(wire), `"type":"custom"`)
  48. assert.NotContains(t, string(wire), `"format"`)
  49. assert.NotContains(t, string(wire), `"grammar"`)
  50. }
  51. func TestResponsesToAnthropic_CustomToolPreservesSchemaParameters(t *testing.T) {
  52. tools := convertResponsesToAnthropicTools([]ResponsesTool{{
  53. Type: "custom",
  54. Name: "edit_file",
  55. Description: "Edit a file",
  56. Parameters: json.RawMessage(`{"type":"object","properties":{"patch":{"type":"string"}},"required":["patch"]}`),
  57. }})
  58. require.Len(t, tools, 1)
  59. assert.Empty(t, tools[0].Type)
  60. assert.Equal(t, "edit_file", tools[0].Name)
  61. schema := requireObjectInputSchema(t, tools[0].InputSchema)
  62. assert.JSONEq(t, `{"patch":{"type":"string"}}`, string(schema["properties"]))
  63. assert.JSONEq(t, `["patch"]`, string(schema["required"]))
  64. }
  65. func TestResponsesToAnthropic_FunctionToolSchemaUnchanged(t *testing.T) {
  66. parameters := json.RawMessage(`{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}`)
  67. tools := convertResponsesToAnthropicTools([]ResponsesTool{{
  68. Type: "function",
  69. Name: "get_weather",
  70. Description: "Get weather",
  71. Parameters: parameters,
  72. }})
  73. require.Len(t, tools, 1)
  74. assert.Empty(t, tools[0].Type)
  75. assert.Equal(t, "get_weather", tools[0].Name)
  76. assert.Equal(t, "Get weather", tools[0].Description)
  77. assert.JSONEq(t, string(parameters), string(tools[0].InputSchema))
  78. }
  79. func TestResponsesToAnthropic_MixedToolsProduceValidAnthropicTools(t *testing.T) {
  80. tools := convertResponsesToAnthropicTools([]ResponsesTool{
  81. {
  82. Type: "function",
  83. Name: "read_file",
  84. Parameters: json.RawMessage(`{"type":"object","properties":{"path":{"type":"string"}}}`),
  85. },
  86. {
  87. Type: "custom",
  88. Name: "apply_patch",
  89. },
  90. {
  91. Type: "web_search",
  92. },
  93. })
  94. require.Len(t, tools, 3)
  95. assert.Empty(t, tools[0].Type)
  96. assert.Equal(t, "read_file", tools[0].Name)
  97. requireObjectInputSchema(t, tools[0].InputSchema)
  98. assert.Empty(t, tools[1].Type)
  99. assert.Equal(t, "apply_patch", tools[1].Name)
  100. assert.JSONEq(t, `{"type":"object","properties":{}}`, string(tools[1].InputSchema))
  101. assert.Equal(t, "web_search_20250305", tools[2].Type)
  102. assert.Equal(t, "web_search", tools[2].Name)
  103. assert.Empty(t, tools[2].InputSchema)
  104. serverToolWire, err := json.Marshal(tools[2])
  105. require.NoError(t, err)
  106. assert.NotContains(t, string(serverToolWire), `"input_schema"`)
  107. }
  108. func TestResponsesToAnthropic_DefaultToolNormalizesInputSchema(t *testing.T) {
  109. tools := convertResponsesToAnthropicTools([]ResponsesTool{{
  110. Type: "local_shell",
  111. Name: "shell",
  112. }})
  113. require.Len(t, tools, 1)
  114. assert.Equal(t, "local_shell", tools[0].Type)
  115. assert.Equal(t, "shell", tools[0].Name)
  116. assert.JSONEq(t, `{"type":"object","properties":{}}`, string(tools[0].InputSchema))
  117. }