responses_namespace_test.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. package apicompat
  2. import (
  3. "testing"
  4. "github.com/stretchr/testify/require"
  5. )
  6. func TestFlattenResponsesNamespaces_RewritesDeclarationHistoryAndChoice(t *testing.T) {
  7. req := map[string]any{
  8. "model": "gpt-5.5",
  9. "tools": []any{
  10. map[string]any{"type": "function", "name": "plain", "description": "keep"},
  11. map[string]any{
  12. "type": "namespace",
  13. "name": "collaboration",
  14. "tools": []any{
  15. map[string]any{"type": "function", "name": "spawn_agent", "description": "spawn", "parameters": map[string]any{"type": "object"}},
  16. },
  17. },
  18. },
  19. "tool_choice": map[string]any{"type": "function", "name": "spawn_agent", "namespace": "collaboration"},
  20. "input": []any{
  21. map[string]any{"type": "function_call", "call_id": "call_1", "name": "spawn_agent", "namespace": "collaboration", "arguments": "{}"},
  22. map[string]any{"type": "message", "role": "user", "content": "hi", "name": "spawn_agent", "namespace": "collaboration"},
  23. },
  24. }
  25. names, changed, err := FlattenResponsesNamespaces(req)
  26. require.NoError(t, err)
  27. require.True(t, changed)
  28. require.Equal(t, ResponsesNamespaceName{Namespace: "collaboration", Name: "spawn_agent"}, names["collaboration__spawn_agent"])
  29. tools, ok := req["tools"].([]any)
  30. require.True(t, ok)
  31. require.Len(t, tools, 2)
  32. plainTool, ok := tools[0].(map[string]any)
  33. require.True(t, ok)
  34. require.Equal(t, "plain", plainTool["name"])
  35. flatTool, ok := tools[1].(map[string]any)
  36. require.True(t, ok)
  37. require.Equal(t, "collaboration__spawn_agent", flatTool["name"])
  38. require.Equal(t, "spawn", flatTool["description"])
  39. choice, ok := req["tool_choice"].(map[string]any)
  40. require.True(t, ok)
  41. require.Equal(t, "collaboration__spawn_agent", choice["name"])
  42. require.NotContains(t, choice, "namespace")
  43. input, ok := req["input"].([]any)
  44. require.True(t, ok)
  45. require.Len(t, input, 2)
  46. call, ok := input[0].(map[string]any)
  47. require.True(t, ok)
  48. require.Equal(t, "collaboration__spawn_agent", call["name"])
  49. require.NotContains(t, call, "namespace")
  50. message, ok := input[1].(map[string]any)
  51. require.True(t, ok)
  52. require.Equal(t, "spawn_agent", message["name"])
  53. require.Equal(t, "collaboration", message["namespace"])
  54. require.Equal(t, "gpt-5.5", req["model"])
  55. }
  56. func TestFlattenResponsesNamespaces_RejectsFlatNameCollision(t *testing.T) {
  57. req := map[string]any{"tools": []any{
  58. map[string]any{"type": "function", "name": "collaboration__spawn_agent"},
  59. map[string]any{"type": "namespace", "name": "collaboration", "tools": []any{
  60. map[string]any{"type": "function", "name": "spawn_agent"},
  61. }},
  62. }}
  63. _, _, err := FlattenResponsesNamespaces(req)
  64. require.ErrorContains(t, err, "conflicts with a top-level tool")
  65. }
  66. func TestFlattenResponsesNamespaces_NamespaceGroupChoiceFallsBackToAuto(t *testing.T) {
  67. req := map[string]any{
  68. "tools": []any{map[string]any{
  69. "type": "namespace", "name": "collaboration", "tools": []any{
  70. map[string]any{"type": "function", "name": "spawn_agent"},
  71. map[string]any{"type": "function", "name": "send_message"},
  72. },
  73. }},
  74. "tool_choice": map[string]any{"type": "namespace", "name": "collaboration"},
  75. }
  76. _, changed, err := FlattenResponsesNamespaces(req)
  77. require.NoError(t, err)
  78. require.True(t, changed)
  79. require.Equal(t, "auto", req["tool_choice"])
  80. }
  81. func TestFlattenResponsesNamespacesExcept_PreservesBuiltInNamespaceAndChoice(t *testing.T) {
  82. req := map[string]any{
  83. "tools": []any{
  84. map[string]any{"type": "namespace", "name": "image_gen", "tools": []any{
  85. map[string]any{"type": "function", "name": "imagegen"},
  86. }},
  87. map[string]any{"type": "namespace", "name": "collaboration", "tools": []any{
  88. map[string]any{"type": "function", "name": "spawn_agent"},
  89. }},
  90. },
  91. "tool_choice": map[string]any{"type": "namespace", "name": "image_gen"},
  92. }
  93. names, changed, err := FlattenResponsesNamespacesExcept(req, map[string]bool{"image_gen": true})
  94. require.NoError(t, err)
  95. require.True(t, changed)
  96. require.Contains(t, names, "collaboration__spawn_agent")
  97. tools, ok := req["tools"].([]any)
  98. require.True(t, ok)
  99. require.Len(t, tools, 2)
  100. preservedTool, ok := tools[0].(map[string]any)
  101. require.True(t, ok)
  102. require.Equal(t, "namespace", preservedTool["type"])
  103. require.Equal(t, "image_gen", preservedTool["name"])
  104. flatTool, ok := tools[1].(map[string]any)
  105. require.True(t, ok)
  106. require.Equal(t, "function", flatTool["type"])
  107. require.Equal(t, "collaboration__spawn_agent", flatTool["name"])
  108. require.Equal(t, map[string]any{"type": "namespace", "name": "image_gen"}, req["tool_choice"])
  109. }
  110. func TestFlattenResponsesNamespaces_RejectsNamespaceCollision(t *testing.T) {
  111. req := map[string]any{"tools": []any{
  112. map[string]any{"type": "namespace", "name": "a", "tools": []any{
  113. map[string]any{"type": "function", "name": "b__c"},
  114. }},
  115. map[string]any{"type": "namespace", "name": "a__b", "tools": []any{
  116. map[string]any{"type": "function", "name": "c"},
  117. }},
  118. }}
  119. _, _, err := FlattenResponsesNamespaces(req)
  120. require.ErrorContains(t, err, "both flatten")
  121. }
  122. func TestRestoreResponsesNamespaceCalls_RewritesOnlyFunctionCalls(t *testing.T) {
  123. payload := []byte(`{"type":"response.completed","response":{"output":[{"type":"function_call","name":"collaboration__spawn_agent","call_id":"call_1","arguments":"{}","extra":"keep"},{"type":"function_call","name":"plain","arguments":"{}"},{"type":"message","name":"collaboration__spawn_agent","content":"<tag>&value</tag>"}]}}`)
  124. names := map[string]ResponsesNamespaceName{
  125. "collaboration__spawn_agent": {Namespace: "collaboration", Name: "spawn_agent"},
  126. }
  127. got, changed, err := RestoreResponsesNamespaceCalls(payload, names)
  128. require.NoError(t, err)
  129. require.True(t, changed)
  130. require.JSONEq(t, `{"type":"response.completed","response":{"output":[{"type":"function_call","name":"spawn_agent","namespace":"collaboration","call_id":"call_1","arguments":"{}","extra":"keep"},{"type":"function_call","name":"plain","arguments":"{}"},{"type":"message","name":"collaboration__spawn_agent","content":"<tag>&value</tag>"}]}}`, string(got))
  131. require.Contains(t, string(got), "<tag>&value</tag>")
  132. require.NotContains(t, string(got), `\u003c`)
  133. }
  134. func TestRestoreResponsesNamespaceCalls_RewritesLifecycleItems(t *testing.T) {
  135. for _, eventType := range []string{"response.output_item.added", "response.output_item.done"} {
  136. t.Run(eventType, func(t *testing.T) {
  137. payload := []byte(`{"type":"` + eventType + `","item":{"type":"function_call","name":"collaboration__spawn_agent","arguments":"{}"}}`)
  138. got, changed, err := RestoreResponsesNamespaceCalls(payload, map[string]ResponsesNamespaceName{
  139. "collaboration__spawn_agent": {Namespace: "collaboration", Name: "spawn_agent"},
  140. })
  141. require.NoError(t, err)
  142. require.True(t, changed)
  143. require.JSONEq(t, `{"type":"`+eventType+`","item":{"type":"function_call","name":"spawn_agent","namespace":"collaboration","arguments":"{}"}}`, string(got))
  144. })
  145. }
  146. }