anthropic_responses_test.go 61 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842
  1. package apicompat
  2. import (
  3. "encoding/json"
  4. "testing"
  5. "github.com/stretchr/testify/assert"
  6. "github.com/stretchr/testify/require"
  7. )
  8. // ---------------------------------------------------------------------------
  9. // AnthropicToResponses tests
  10. // ---------------------------------------------------------------------------
  11. func TestAnthropicToResponses_BasicText(t *testing.T) {
  12. req := &AnthropicRequest{
  13. Model: "gpt-5.2",
  14. MaxTokens: 1024,
  15. Stream: true,
  16. Messages: []AnthropicMessage{
  17. {Role: "user", Content: json.RawMessage(`"Hello"`)},
  18. },
  19. }
  20. resp, err := AnthropicToResponses(req)
  21. require.NoError(t, err)
  22. assert.Equal(t, "gpt-5.2", resp.Model)
  23. assert.True(t, resp.Stream)
  24. assert.Equal(t, 1024, *resp.MaxOutputTokens)
  25. assert.False(t, *resp.Store)
  26. var items []ResponsesInputItem
  27. require.NoError(t, json.Unmarshal(resp.Input, &items))
  28. require.Len(t, items, 1)
  29. assert.Equal(t, "message", items[0].Type)
  30. assert.Equal(t, "user", items[0].Role)
  31. var parts []ResponsesContentPart
  32. require.NoError(t, json.Unmarshal(items[0].Content, &parts))
  33. require.Len(t, parts, 1)
  34. assert.Equal(t, "input_text", parts[0].Type)
  35. assert.Equal(t, "Hello", parts[0].Text)
  36. }
  37. func TestAnthropicToResponses_SystemPrompt(t *testing.T) {
  38. t.Run("string", func(t *testing.T) {
  39. req := &AnthropicRequest{
  40. Model: "gpt-5.2",
  41. MaxTokens: 100,
  42. System: json.RawMessage(`"You are helpful."`),
  43. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}},
  44. }
  45. resp, err := AnthropicToResponses(req)
  46. require.NoError(t, err)
  47. var items []ResponsesInputItem
  48. require.NoError(t, json.Unmarshal(resp.Input, &items))
  49. require.Len(t, items, 2)
  50. assert.Equal(t, "developer", items[0].Role)
  51. var parts []ResponsesContentPart
  52. require.NoError(t, json.Unmarshal(items[0].Content, &parts))
  53. require.Len(t, parts, 1)
  54. assert.Equal(t, "input_text", parts[0].Type)
  55. assert.Equal(t, "You are helpful.", parts[0].Text)
  56. })
  57. t.Run("array", func(t *testing.T) {
  58. req := &AnthropicRequest{
  59. Model: "gpt-5.2",
  60. MaxTokens: 100,
  61. System: json.RawMessage(`[{"type":"text","text":"Part 1"},{"type":"text","text":"Part 2"}]`),
  62. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}},
  63. }
  64. resp, err := AnthropicToResponses(req)
  65. require.NoError(t, err)
  66. var items []ResponsesInputItem
  67. require.NoError(t, json.Unmarshal(resp.Input, &items))
  68. require.Len(t, items, 2)
  69. assert.Equal(t, "developer", items[0].Role)
  70. var parts []ResponsesContentPart
  71. require.NoError(t, json.Unmarshal(items[0].Content, &parts))
  72. require.Len(t, parts, 2)
  73. assert.Equal(t, "input_text", parts[0].Type)
  74. assert.Equal(t, "Part 1", parts[0].Text)
  75. assert.Equal(t, "input_text", parts[1].Type)
  76. assert.Equal(t, "Part 2", parts[1].Text)
  77. })
  78. t.Run("billing header skipped", func(t *testing.T) {
  79. req := &AnthropicRequest{
  80. Model: "gpt-5.2",
  81. MaxTokens: 100,
  82. System: json.RawMessage(`[{"type":"text","text":"x-anthropic-billing-header: cc_version=1;"},{"type":"text","text":"Project prompt"}]`),
  83. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}},
  84. }
  85. resp, err := AnthropicToResponses(req)
  86. require.NoError(t, err)
  87. var items []ResponsesInputItem
  88. require.NoError(t, json.Unmarshal(resp.Input, &items))
  89. require.Len(t, items, 2)
  90. var parts []ResponsesContentPart
  91. require.NoError(t, json.Unmarshal(items[0].Content, &parts))
  92. require.Len(t, parts, 1)
  93. assert.Equal(t, "Project prompt", parts[0].Text)
  94. })
  95. }
  96. func TestAnthropicToResponses_ToolUse(t *testing.T) {
  97. req := &AnthropicRequest{
  98. Model: "gpt-5.2",
  99. MaxTokens: 1024,
  100. Messages: []AnthropicMessage{
  101. {Role: "user", Content: json.RawMessage(`"What is the weather?"`)},
  102. {Role: "assistant", Content: json.RawMessage(`[{"type":"text","text":"Let me check."},{"type":"tool_use","id":"call_1","name":"get_weather","input":{"city":"NYC"}}]`)},
  103. {Role: "user", Content: json.RawMessage(`[{"type":"tool_result","tool_use_id":"call_1","content":"Sunny, 72°F"}]`)},
  104. },
  105. Tools: []AnthropicTool{
  106. {Name: "get_weather", Description: "Get weather", InputSchema: json.RawMessage(`{"type":"object","properties":{"city":{"type":"string"}}}`)},
  107. },
  108. }
  109. resp, err := AnthropicToResponses(req)
  110. require.NoError(t, err)
  111. // Check tools
  112. require.Len(t, resp.Tools, 1)
  113. assert.Equal(t, "function", resp.Tools[0].Type)
  114. assert.Equal(t, "get_weather", resp.Tools[0].Name)
  115. require.NotNil(t, resp.Tools[0].Strict)
  116. assert.False(t, *resp.Tools[0].Strict)
  117. // Check input items
  118. var items []ResponsesInputItem
  119. require.NoError(t, json.Unmarshal(resp.Input, &items))
  120. // user + assistant + function_call + function_call_output = 4
  121. require.Len(t, items, 4)
  122. assert.Equal(t, "user", items[0].Role)
  123. assert.Equal(t, "assistant", items[1].Role)
  124. assert.Equal(t, "function_call", items[2].Type)
  125. assert.Equal(t, "call_1", items[2].CallID)
  126. assert.Empty(t, items[2].ID)
  127. assert.Equal(t, "function_call_output", items[3].Type)
  128. assert.Equal(t, "call_1", items[3].CallID)
  129. assert.Equal(t, "Sunny, 72°F", items[3].Output)
  130. }
  131. func TestAnthropicToResponses_ThinkingWithoutSignatureIgnored(t *testing.T) {
  132. req := &AnthropicRequest{
  133. Model: "gpt-5.2",
  134. MaxTokens: 1024,
  135. Messages: []AnthropicMessage{
  136. {Role: "user", Content: json.RawMessage(`"Hello"`)},
  137. {Role: "assistant", Content: json.RawMessage(`[{"type":"thinking","thinking":"deep thought"},{"type":"text","text":"Hi!"}]`)},
  138. {Role: "user", Content: json.RawMessage(`"More"`)},
  139. },
  140. }
  141. resp, err := AnthropicToResponses(req)
  142. require.NoError(t, err)
  143. var items []ResponsesInputItem
  144. require.NoError(t, json.Unmarshal(resp.Input, &items))
  145. // user + assistant(text only, thinking without signature ignored) + user = 3
  146. require.Len(t, items, 3)
  147. assert.Equal(t, "assistant", items[1].Role)
  148. // Assistant content should only have text, not thinking.
  149. var parts []ResponsesContentPart
  150. require.NoError(t, json.Unmarshal(items[1].Content, &parts))
  151. require.Len(t, parts, 1)
  152. assert.Equal(t, "output_text", parts[0].Type)
  153. assert.Equal(t, "Hi!", parts[0].Text)
  154. }
  155. func TestAnthropicToResponses_ThinkingSignatureBecomesReasoning(t *testing.T) {
  156. req := &AnthropicRequest{
  157. Model: "grok-4.5",
  158. MaxTokens: 1024,
  159. Messages: []AnthropicMessage{
  160. {Role: "user", Content: json.RawMessage(`"Hello"`)},
  161. {Role: "assistant", Content: json.RawMessage(`[{"type":"thinking","thinking":"plan","signature":"enc-rs-1"},{"type":"text","text":"Hi!"},{"type":"tool_use","id":"toolu_1","name":"Bash","input":{"command":"ls"}}]`)},
  162. {Role: "user", Content: json.RawMessage(`[{"type":"tool_result","tool_use_id":"toolu_1","content":"ok"}]`)},
  163. },
  164. }
  165. resp, err := AnthropicToResponses(req)
  166. require.NoError(t, err)
  167. var items []ResponsesInputItem
  168. require.NoError(t, json.Unmarshal(resp.Input, &items))
  169. // user + reasoning + assistant text + function_call + function_call_output
  170. require.GreaterOrEqual(t, len(items), 4)
  171. assert.Equal(t, "reasoning", items[1].Type)
  172. assert.Equal(t, "enc-rs-1", items[1].EncryptedContent)
  173. assert.Equal(t, "assistant", items[2].Role)
  174. assert.Equal(t, "function_call", items[3].Type)
  175. }
  176. func TestAnthropicToResponses_MaxTokensFloor(t *testing.T) {
  177. req := &AnthropicRequest{
  178. Model: "gpt-5.2",
  179. MaxTokens: 10, // below minMaxOutputTokens (128)
  180. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}},
  181. }
  182. resp, err := AnthropicToResponses(req)
  183. require.NoError(t, err)
  184. assert.Equal(t, 128, *resp.MaxOutputTokens)
  185. }
  186. // ---------------------------------------------------------------------------
  187. // ResponsesToAnthropic (non-streaming) tests
  188. // ---------------------------------------------------------------------------
  189. func TestResponsesToAnthropic_TextOnly(t *testing.T) {
  190. resp := &ResponsesResponse{
  191. ID: "resp_123",
  192. Model: "gpt-5.2",
  193. Status: "completed",
  194. Output: []ResponsesOutput{
  195. {
  196. Type: "message",
  197. Content: []ResponsesContentPart{
  198. {Type: "output_text", Text: "Hello there!"},
  199. },
  200. },
  201. },
  202. Usage: &ResponsesUsage{InputTokens: 10, OutputTokens: 5, TotalTokens: 15},
  203. }
  204. anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
  205. assert.Equal(t, "resp_123", anth.ID)
  206. assert.Equal(t, "claude-opus-4-6", anth.Model)
  207. assert.Equal(t, "end_turn", AnthropicStopReasonString(anth.StopReason))
  208. require.Len(t, anth.Content, 1)
  209. assert.Equal(t, "text", anth.Content[0].Type)
  210. assert.Equal(t, "Hello there!", anth.Content[0].Text)
  211. assert.Equal(t, 10, anth.Usage.InputTokens)
  212. assert.Equal(t, 5, anth.Usage.OutputTokens)
  213. }
  214. func TestResponsesToAnthropic_CachedTokensUseAnthropicInputSemantics(t *testing.T) {
  215. resp := &ResponsesResponse{
  216. ID: "resp_cached",
  217. Model: "gpt-5.2",
  218. Status: "completed",
  219. Output: []ResponsesOutput{
  220. {
  221. Type: "message",
  222. Content: []ResponsesContentPart{
  223. {Type: "output_text", Text: "Cached response"},
  224. },
  225. },
  226. },
  227. Usage: &ResponsesUsage{
  228. InputTokens: 54006,
  229. OutputTokens: 123,
  230. TotalTokens: 54129,
  231. InputTokensDetails: &ResponsesInputTokensDetails{
  232. CachedTokens: 50688,
  233. },
  234. },
  235. }
  236. anth := ResponsesToAnthropic(resp, "claude-sonnet-4-5-20250929")
  237. assert.Equal(t, 3318, anth.Usage.InputTokens)
  238. assert.Equal(t, 50688, anth.Usage.CacheReadInputTokens)
  239. assert.Equal(t, 123, anth.Usage.OutputTokens)
  240. }
  241. func TestResponsesToAnthropic_CachedTokensClampInputTokens(t *testing.T) {
  242. resp := &ResponsesResponse{
  243. ID: "resp_cached_clamp",
  244. Model: "gpt-5.2",
  245. Status: "completed",
  246. Usage: &ResponsesUsage{
  247. InputTokens: 100,
  248. OutputTokens: 5,
  249. InputTokensDetails: &ResponsesInputTokensDetails{
  250. CachedTokens: 150,
  251. },
  252. },
  253. }
  254. anth := ResponsesToAnthropic(resp, "claude-sonnet-4-5-20250929")
  255. assert.Equal(t, 0, anth.Usage.InputTokens)
  256. assert.Equal(t, 150, anth.Usage.CacheReadInputTokens)
  257. assert.Equal(t, 5, anth.Usage.OutputTokens)
  258. }
  259. func TestResponsesToAnthropic_ToolUse(t *testing.T) {
  260. resp := &ResponsesResponse{
  261. ID: "resp_456",
  262. Model: "gpt-5.2",
  263. Status: "completed",
  264. Output: []ResponsesOutput{
  265. {
  266. Type: "message",
  267. Content: []ResponsesContentPart{
  268. {Type: "output_text", Text: "Let me check."},
  269. },
  270. },
  271. {
  272. Type: "function_call",
  273. CallID: "call_1",
  274. Name: "get_weather",
  275. Arguments: `{"city":"NYC"}`,
  276. },
  277. },
  278. }
  279. anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
  280. assert.Equal(t, "tool_use", AnthropicStopReasonString(anth.StopReason))
  281. require.Len(t, anth.Content, 2)
  282. assert.Equal(t, "text", anth.Content[0].Type)
  283. assert.Equal(t, "tool_use", anth.Content[1].Type)
  284. assert.Equal(t, "call_1", anth.Content[1].ID)
  285. assert.Equal(t, "get_weather", anth.Content[1].Name)
  286. assert.JSONEq(t, `{"city":"NYC"}`, string(anth.Content[1].Input))
  287. }
  288. func TestResponsesToAnthropic_ToolUseStopReasonDoesNotDependOnLastBlock(t *testing.T) {
  289. resp := &ResponsesResponse{
  290. ID: "resp_tool_then_text",
  291. Model: "gpt-5.5",
  292. Status: "completed",
  293. Output: []ResponsesOutput{
  294. {
  295. Type: "function_call",
  296. CallID: "call_todo",
  297. Name: "TodoWrite",
  298. Arguments: `{"todos":[{"content":"review changes","status":"in_progress"}]}`,
  299. },
  300. {
  301. Type: "message",
  302. Content: []ResponsesContentPart{
  303. {Type: "output_text", Text: "Task list updated."},
  304. },
  305. },
  306. },
  307. }
  308. anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
  309. assert.Equal(t, "tool_use", AnthropicStopReasonString(anth.StopReason))
  310. require.Len(t, anth.Content, 2)
  311. assert.Equal(t, "tool_use", anth.Content[0].Type)
  312. assert.Equal(t, "text", anth.Content[1].Type)
  313. }
  314. func TestResponsesToAnthropic_ReadToolDropsEmptyPages(t *testing.T) {
  315. resp := &ResponsesResponse{
  316. ID: "resp_read",
  317. Model: "gpt-5.5",
  318. Status: "completed",
  319. Output: []ResponsesOutput{
  320. {
  321. Type: "function_call",
  322. CallID: "call_read",
  323. Name: "Read",
  324. Arguments: `{"file_path":"/tmp/demo.py","limit":2000,"offset":0,"pages":""}`,
  325. },
  326. },
  327. }
  328. anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
  329. require.Len(t, anth.Content, 1)
  330. assert.Equal(t, "tool_use", anth.Content[0].Type)
  331. assert.JSONEq(t, `{"file_path":"/tmp/demo.py","limit":2000,"offset":0}`, string(anth.Content[0].Input))
  332. }
  333. func TestResponsesToAnthropic_PreservesEmptyStringsForOtherTools(t *testing.T) {
  334. resp := &ResponsesResponse{
  335. ID: "resp_other",
  336. Model: "gpt-5.5",
  337. Status: "completed",
  338. Output: []ResponsesOutput{
  339. {
  340. Type: "function_call",
  341. CallID: "call_other",
  342. Name: "Search",
  343. Arguments: `{"query":""}`,
  344. },
  345. },
  346. }
  347. anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
  348. require.Len(t, anth.Content, 1)
  349. assert.JSONEq(t, `{"query":""}`, string(anth.Content[0].Input))
  350. }
  351. func TestResponsesToAnthropic_Reasoning(t *testing.T) {
  352. resp := &ResponsesResponse{
  353. ID: "resp_789",
  354. Model: "gpt-5.2",
  355. Status: "completed",
  356. Output: []ResponsesOutput{
  357. {
  358. Type: "reasoning",
  359. EncryptedContent: "enc-rs-roundtrip",
  360. Summary: []ResponsesSummary{
  361. {Type: "summary_text", Text: "Thinking about the answer..."},
  362. },
  363. },
  364. {
  365. Type: "message",
  366. Content: []ResponsesContentPart{
  367. {Type: "output_text", Text: "42"},
  368. },
  369. },
  370. },
  371. }
  372. anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
  373. require.Len(t, anth.Content, 2)
  374. assert.Equal(t, "thinking", anth.Content[0].Type)
  375. assert.Equal(t, "Thinking about the answer...", anth.Content[0].Thinking)
  376. assert.Equal(t, "enc-rs-roundtrip", anth.Content[0].Signature)
  377. assert.Equal(t, "text", anth.Content[1].Type)
  378. assert.Equal(t, "42", anth.Content[1].Text)
  379. }
  380. func TestResponsesToAnthropic_StreamEmitsThinkingSignature(t *testing.T) {
  381. state := NewResponsesEventToAnthropicState()
  382. var all []AnthropicStreamEvent
  383. appendAll := func(events []AnthropicStreamEvent) {
  384. all = append(all, events...)
  385. }
  386. appendAll(ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  387. Type: "response.output_item.added",
  388. OutputIndex: 0,
  389. Item: &ResponsesOutput{Type: "reasoning", ID: "rs_1"},
  390. }, state))
  391. appendAll(ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  392. Type: "response.reasoning_summary_text.delta",
  393. OutputIndex: 0,
  394. Delta: "thinking...",
  395. SummaryIndex: 0,
  396. }, state))
  397. // summary.done must not close the thinking block before encrypted_content arrives
  398. appendAll(ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  399. Type: "response.reasoning_summary_text.done",
  400. OutputIndex: 0,
  401. SummaryIndex: 0,
  402. }, state))
  403. appendAll(ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  404. Type: "response.output_item.done",
  405. OutputIndex: 0,
  406. Item: &ResponsesOutput{
  407. Type: "reasoning",
  408. ID: "rs_1",
  409. EncryptedContent: "enc-stream-1",
  410. Status: "completed",
  411. },
  412. }, state))
  413. var sawSignature bool
  414. for _, ev := range all {
  415. if ev.Type == "content_block_delta" && ev.Delta != nil && ev.Delta.Type == "signature_delta" {
  416. assert.Equal(t, "enc-stream-1", ev.Delta.Signature)
  417. sawSignature = true
  418. }
  419. }
  420. require.True(t, sawSignature, "expected signature_delta with encrypted_content")
  421. }
  422. func TestResponsesToAnthropic_Incomplete(t *testing.T) {
  423. resp := &ResponsesResponse{
  424. ID: "resp_inc",
  425. Model: "gpt-5.2",
  426. Status: "incomplete",
  427. IncompleteDetails: &ResponsesIncompleteDetails{
  428. Reason: "max_output_tokens",
  429. },
  430. Output: []ResponsesOutput{
  431. {
  432. Type: "message",
  433. Content: []ResponsesContentPart{{Type: "output_text", Text: "Partial..."}},
  434. },
  435. },
  436. }
  437. anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
  438. assert.Equal(t, "max_tokens", AnthropicStopReasonString(anth.StopReason))
  439. }
  440. func TestResponsesToAnthropic_EmptyOutput(t *testing.T) {
  441. resp := &ResponsesResponse{
  442. ID: "resp_empty",
  443. Model: "gpt-5.2",
  444. Status: "completed",
  445. Output: []ResponsesOutput{},
  446. }
  447. anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
  448. require.Len(t, anth.Content, 1)
  449. assert.Equal(t, "text", anth.Content[0].Type)
  450. assert.Equal(t, "", anth.Content[0].Text)
  451. }
  452. // ---------------------------------------------------------------------------
  453. // Streaming: ResponsesEventToAnthropicEvents tests
  454. // ---------------------------------------------------------------------------
  455. func TestStreamingTextOnly(t *testing.T) {
  456. state := NewResponsesEventToAnthropicState()
  457. // 1. response.created
  458. events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  459. Type: "response.created",
  460. Response: &ResponsesResponse{
  461. ID: "resp_1",
  462. Model: "gpt-5.2",
  463. },
  464. }, state)
  465. require.Len(t, events, 1)
  466. assert.Equal(t, "message_start", events[0].Type)
  467. // 2. output_item.added (message)
  468. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  469. Type: "response.output_item.added",
  470. OutputIndex: 0,
  471. Item: &ResponsesOutput{Type: "message"},
  472. }, state)
  473. assert.Len(t, events, 0) // message item doesn't emit events
  474. // 3. text delta
  475. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  476. Type: "response.output_text.delta",
  477. Delta: "Hello",
  478. }, state)
  479. require.Len(t, events, 2) // content_block_start + content_block_delta
  480. assert.Equal(t, "content_block_start", events[0].Type)
  481. assert.Equal(t, "text", events[0].ContentBlock.Type)
  482. assert.Equal(t, "content_block_delta", events[1].Type)
  483. assert.Equal(t, "text_delta", events[1].Delta.Type)
  484. assert.Equal(t, "Hello", events[1].Delta.Text)
  485. // 4. more text
  486. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  487. Type: "response.output_text.delta",
  488. Delta: " world",
  489. }, state)
  490. require.Len(t, events, 1) // only delta, no new block start
  491. assert.Equal(t, "content_block_delta", events[0].Type)
  492. // 5. text done
  493. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  494. Type: "response.output_text.done",
  495. }, state)
  496. require.Len(t, events, 1)
  497. assert.Equal(t, "content_block_stop", events[0].Type)
  498. // 6. completed
  499. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  500. Type: "response.completed",
  501. Response: &ResponsesResponse{
  502. Status: "completed",
  503. Usage: &ResponsesUsage{InputTokens: 10, OutputTokens: 5},
  504. },
  505. }, state)
  506. require.Len(t, events, 2) // message_delta + message_stop
  507. assert.Equal(t, "message_delta", events[0].Type)
  508. assert.Equal(t, "end_turn", events[0].Delta.StopReason)
  509. assert.Equal(t, 10, events[0].Usage.InputTokens)
  510. assert.Equal(t, 5, events[0].Usage.OutputTokens)
  511. assert.Equal(t, "message_stop", events[1].Type)
  512. }
  513. func TestResponsesEventToAnthropicEvents_ResponseDone(t *testing.T) {
  514. state := NewResponsesEventToAnthropicState()
  515. state.Model = "gpt-4o"
  516. events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  517. Type: "response.done",
  518. Response: &ResponsesResponse{
  519. Status: "completed",
  520. Usage: &ResponsesUsage{InputTokens: 12, OutputTokens: 4},
  521. },
  522. }, state)
  523. require.Len(t, events, 2)
  524. assert.Equal(t, "message_delta", events[0].Type)
  525. assert.Equal(t, "end_turn", events[0].Delta.StopReason)
  526. assert.Equal(t, 12, events[0].Usage.InputTokens)
  527. assert.Equal(t, 4, events[0].Usage.OutputTokens)
  528. assert.Equal(t, "message_stop", events[1].Type)
  529. assert.Nil(t, FinalizeResponsesAnthropicStream(state))
  530. }
  531. func TestResponsesEventToAnthropicEvents_TopLevelTerminalUsage(t *testing.T) {
  532. state := NewResponsesEventToAnthropicState()
  533. state.Model = "gpt-4o"
  534. events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  535. Type: "response.completed",
  536. Response: &ResponsesResponse{
  537. Status: "completed",
  538. },
  539. Usage: &ResponsesUsage{
  540. InputTokens: 20,
  541. OutputTokens: 6,
  542. InputTokensDetails: &ResponsesInputTokensDetails{
  543. CachedTokens: 5,
  544. },
  545. },
  546. }, state)
  547. require.Len(t, events, 2)
  548. assert.Equal(t, "message_delta", events[0].Type)
  549. require.NotNil(t, events[0].Usage)
  550. assert.Equal(t, 15, events[0].Usage.InputTokens)
  551. assert.Equal(t, 5, events[0].Usage.CacheReadInputTokens)
  552. assert.Equal(t, 6, events[0].Usage.OutputTokens)
  553. assert.Equal(t, "message_stop", events[1].Type)
  554. }
  555. func TestResponsesEventToAnthropicEvents_ResponseDoneIncomplete(t *testing.T) {
  556. state := NewResponsesEventToAnthropicState()
  557. state.Model = "gpt-4o"
  558. events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  559. Type: "response.done",
  560. Response: &ResponsesResponse{
  561. Status: "incomplete",
  562. IncompleteDetails: &ResponsesIncompleteDetails{Reason: "max_output_tokens"},
  563. Usage: &ResponsesUsage{InputTokens: 12, OutputTokens: 4},
  564. },
  565. }, state)
  566. require.Len(t, events, 2)
  567. assert.Equal(t, "message_delta", events[0].Type)
  568. assert.Equal(t, "max_tokens", events[0].Delta.StopReason)
  569. assert.Equal(t, "message_stop", events[1].Type)
  570. assert.Nil(t, FinalizeResponsesAnthropicStream(state))
  571. }
  572. func TestStreamingCachedTokensUseAnthropicInputSemantics(t *testing.T) {
  573. state := NewResponsesEventToAnthropicState()
  574. ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  575. Type: "response.created",
  576. Response: &ResponsesResponse{ID: "resp_cached_stream", Model: "gpt-5.2"},
  577. }, state)
  578. events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  579. Type: "response.completed",
  580. Response: &ResponsesResponse{
  581. Status: "completed",
  582. Usage: &ResponsesUsage{
  583. InputTokens: 54006,
  584. OutputTokens: 123,
  585. TotalTokens: 54129,
  586. InputTokensDetails: &ResponsesInputTokensDetails{
  587. CachedTokens: 50688,
  588. },
  589. },
  590. },
  591. }, state)
  592. require.Len(t, events, 2)
  593. assert.Equal(t, "message_delta", events[0].Type)
  594. assert.Equal(t, 3318, events[0].Usage.InputTokens)
  595. assert.Equal(t, 50688, events[0].Usage.CacheReadInputTokens)
  596. assert.Equal(t, 123, events[0].Usage.OutputTokens)
  597. assert.Equal(t, "message_stop", events[1].Type)
  598. }
  599. func TestStreamingToolCall(t *testing.T) {
  600. state := NewResponsesEventToAnthropicState()
  601. // 1. response.created
  602. ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  603. Type: "response.created",
  604. Response: &ResponsesResponse{ID: "resp_2", Model: "gpt-5.2"},
  605. }, state)
  606. // 2. function_call added
  607. events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  608. Type: "response.output_item.added",
  609. OutputIndex: 0,
  610. Item: &ResponsesOutput{Type: "function_call", CallID: "call_1", Name: "get_weather"},
  611. }, state)
  612. require.Len(t, events, 1)
  613. assert.Equal(t, "content_block_start", events[0].Type)
  614. assert.Equal(t, "tool_use", events[0].ContentBlock.Type)
  615. assert.Equal(t, "call_1", events[0].ContentBlock.ID)
  616. // 3. arguments delta
  617. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  618. Type: "response.function_call_arguments.delta",
  619. OutputIndex: 0,
  620. Delta: `{"city":`,
  621. }, state)
  622. require.Len(t, events, 1)
  623. assert.Equal(t, "content_block_delta", events[0].Type)
  624. assert.Equal(t, "input_json_delta", events[0].Delta.Type)
  625. assert.Equal(t, `{"city":`, events[0].Delta.PartialJSON)
  626. // 4. arguments done
  627. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  628. Type: "response.function_call_arguments.done",
  629. }, state)
  630. require.Len(t, events, 1)
  631. assert.Equal(t, "content_block_stop", events[0].Type)
  632. // 5. completed with tool_calls
  633. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  634. Type: "response.completed",
  635. Response: &ResponsesResponse{
  636. Status: "completed",
  637. Usage: &ResponsesUsage{InputTokens: 20, OutputTokens: 10},
  638. },
  639. }, state)
  640. require.Len(t, events, 2)
  641. assert.Equal(t, "tool_use", events[0].Delta.StopReason)
  642. }
  643. func TestStreamingToolCallStopReasonSurvivesLaterText(t *testing.T) {
  644. state := NewResponsesEventToAnthropicState()
  645. ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  646. Type: "response.created",
  647. Response: &ResponsesResponse{ID: "resp_tool_then_text", Model: "gpt-5.5"},
  648. }, state)
  649. events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  650. Type: "response.output_item.added",
  651. OutputIndex: 0,
  652. Item: &ResponsesOutput{Type: "function_call", CallID: "call_todo", Name: "TodoWrite"},
  653. }, state)
  654. require.Len(t, events, 1)
  655. assert.Equal(t, "content_block_start", events[0].Type)
  656. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  657. Type: "response.function_call_arguments.done",
  658. OutputIndex: 0,
  659. Arguments: `{"todos":[{"content":"review changes","status":"in_progress","activeForm":"reviewing changes"}]}`,
  660. }, state)
  661. require.Len(t, events, 2)
  662. assert.Equal(t, "content_block_delta", events[0].Type)
  663. assert.Equal(t, "content_block_stop", events[1].Type)
  664. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  665. Type: "response.output_text.delta",
  666. OutputIndex: 1,
  667. Delta: "I will continue after the task list updates.",
  668. }, state)
  669. require.Len(t, events, 2)
  670. assert.Equal(t, "content_block_start", events[0].Type)
  671. assert.Equal(t, "content_block_delta", events[1].Type)
  672. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  673. Type: "response.completed",
  674. Response: &ResponsesResponse{
  675. Status: "completed",
  676. Usage: &ResponsesUsage{InputTokens: 20, OutputTokens: 10},
  677. },
  678. }, state)
  679. require.Len(t, events, 3)
  680. assert.Equal(t, "content_block_stop", events[0].Type)
  681. assert.Equal(t, "tool_use", events[1].Delta.StopReason)
  682. assert.Equal(t, "message_stop", events[2].Type)
  683. }
  684. func TestStreamingToolCallDoneWithoutDeltaEmitsArguments(t *testing.T) {
  685. state := NewResponsesEventToAnthropicState()
  686. ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  687. Type: "response.created",
  688. Response: &ResponsesResponse{ID: "resp_bash", Model: "gpt-5.5"},
  689. }, state)
  690. events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  691. Type: "response.output_item.added",
  692. OutputIndex: 0,
  693. Item: &ResponsesOutput{Type: "function_call", CallID: "call_bash", Name: "Bash"},
  694. }, state)
  695. require.Len(t, events, 1)
  696. assert.Equal(t, "content_block_start", events[0].Type)
  697. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  698. Type: "response.function_call_arguments.done",
  699. OutputIndex: 0,
  700. Arguments: `{"command":"git -C \"/mnt/d/nodejs/other/edmt\" status --short --ignored"}`,
  701. }, state)
  702. require.Len(t, events, 2)
  703. assert.Equal(t, "content_block_delta", events[0].Type)
  704. assert.Equal(t, "input_json_delta", events[0].Delta.Type)
  705. assert.JSONEq(t, `{"command":"git -C \"/mnt/d/nodejs/other/edmt\" status --short --ignored"}`, events[0].Delta.PartialJSON)
  706. assert.Equal(t, "content_block_stop", events[1].Type)
  707. }
  708. func TestStreamingReadToolStreamsDeltas(t *testing.T) {
  709. state := NewResponsesEventToAnthropicState()
  710. ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  711. Type: "response.created",
  712. Response: &ResponsesResponse{ID: "resp_read_stream", Model: "gpt-5.5"},
  713. }, state)
  714. events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  715. Type: "response.output_item.added",
  716. OutputIndex: 0,
  717. Item: &ResponsesOutput{Type: "function_call", CallID: "call_read", Name: "Read"},
  718. }, state)
  719. require.Len(t, events, 1)
  720. assert.Equal(t, "content_block_start", events[0].Type)
  721. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  722. Type: "response.function_call_arguments.delta",
  723. OutputIndex: 0,
  724. Delta: `{"file_path":"/tmp/demo.py","limit":2000,"offset":0,"pages":""}`,
  725. }, state)
  726. require.Len(t, events, 1, "Read tool deltas must be streamed like any other tool")
  727. assert.Equal(t, "content_block_delta", events[0].Type)
  728. assert.Equal(t, "input_json_delta", events[0].Delta.Type)
  729. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  730. Type: "response.function_call_arguments.done",
  731. OutputIndex: 0,
  732. Arguments: `{"file_path":"/tmp/demo.py","limit":2000,"offset":0,"pages":""}`,
  733. }, state)
  734. require.Len(t, events, 1, "after streaming deltas, .done should just close the block")
  735. assert.Equal(t, "content_block_stop", events[0].Type)
  736. }
  737. func TestStreamingReasoning(t *testing.T) {
  738. state := NewResponsesEventToAnthropicState()
  739. ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  740. Type: "response.created",
  741. Response: &ResponsesResponse{ID: "resp_3", Model: "gpt-5.2"},
  742. }, state)
  743. // reasoning item added
  744. events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  745. Type: "response.output_item.added",
  746. OutputIndex: 0,
  747. Item: &ResponsesOutput{Type: "reasoning"},
  748. }, state)
  749. require.Len(t, events, 1)
  750. assert.Equal(t, "content_block_start", events[0].Type)
  751. assert.Equal(t, "thinking", events[0].ContentBlock.Type)
  752. sse, err := ResponsesAnthropicEventToSSE(events[0])
  753. require.NoError(t, err)
  754. assert.Contains(t, sse, `"content_block":{"thinking":"","type":"thinking"}`)
  755. // reasoning text delta
  756. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  757. Type: "response.reasoning_summary_text.delta",
  758. OutputIndex: 0,
  759. Delta: "Let me think...",
  760. }, state)
  761. require.Len(t, events, 1)
  762. assert.Equal(t, "content_block_delta", events[0].Type)
  763. assert.Equal(t, "thinking_delta", events[0].Delta.Type)
  764. assert.Equal(t, "Let me think...", events[0].Delta.Thinking)
  765. // summary.done keeps thinking open until output_item.done (for signature)
  766. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  767. Type: "response.reasoning_summary_text.done",
  768. }, state)
  769. require.Len(t, events, 0)
  770. events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  771. Type: "response.output_item.done",
  772. OutputIndex: 0,
  773. Item: &ResponsesOutput{
  774. Type: "reasoning",
  775. EncryptedContent: "enc-rs-stream",
  776. Status: "completed",
  777. },
  778. }, state)
  779. require.Len(t, events, 2)
  780. assert.Equal(t, "content_block_delta", events[0].Type)
  781. assert.Equal(t, "signature_delta", events[0].Delta.Type)
  782. assert.Equal(t, "enc-rs-stream", events[0].Delta.Signature)
  783. assert.Equal(t, "content_block_stop", events[1].Type)
  784. }
  785. func TestStreamingIncomplete(t *testing.T) {
  786. state := NewResponsesEventToAnthropicState()
  787. ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  788. Type: "response.created",
  789. Response: &ResponsesResponse{ID: "resp_4", Model: "gpt-5.2"},
  790. }, state)
  791. ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  792. Type: "response.output_text.delta",
  793. Delta: "Partial output...",
  794. }, state)
  795. events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  796. Type: "response.incomplete",
  797. Response: &ResponsesResponse{
  798. Status: "incomplete",
  799. IncompleteDetails: &ResponsesIncompleteDetails{Reason: "max_output_tokens"},
  800. Usage: &ResponsesUsage{InputTokens: 100, OutputTokens: 4096},
  801. },
  802. }, state)
  803. // Should close the text block + message_delta + message_stop
  804. require.Len(t, events, 3)
  805. assert.Equal(t, "content_block_stop", events[0].Type)
  806. assert.Equal(t, "message_delta", events[1].Type)
  807. assert.Equal(t, "max_tokens", events[1].Delta.StopReason)
  808. assert.Equal(t, "message_stop", events[2].Type)
  809. }
  810. func TestFinalizeStream_NeverStarted(t *testing.T) {
  811. state := NewResponsesEventToAnthropicState()
  812. events := FinalizeResponsesAnthropicStream(state)
  813. assert.Nil(t, events)
  814. }
  815. func TestFinalizeStream_AlreadyCompleted(t *testing.T) {
  816. state := NewResponsesEventToAnthropicState()
  817. state.MessageStartSent = true
  818. state.MessageStopSent = true
  819. events := FinalizeResponsesAnthropicStream(state)
  820. assert.Nil(t, events)
  821. }
  822. func TestFinalizeStream_AbnormalTermination(t *testing.T) {
  823. state := NewResponsesEventToAnthropicState()
  824. // Simulate a stream that started but never completed
  825. ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  826. Type: "response.created",
  827. Response: &ResponsesResponse{ID: "resp_5", Model: "gpt-5.2"},
  828. }, state)
  829. ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  830. Type: "response.output_text.delta",
  831. Delta: "Interrupted...",
  832. }, state)
  833. // Stream ends without response.completed
  834. events := FinalizeResponsesAnthropicStream(state)
  835. require.Len(t, events, 3) // content_block_stop + message_delta + message_stop
  836. assert.Equal(t, "content_block_stop", events[0].Type)
  837. assert.Equal(t, "message_delta", events[1].Type)
  838. assert.Equal(t, "end_turn", events[1].Delta.StopReason)
  839. assert.Equal(t, "message_stop", events[2].Type)
  840. }
  841. func TestFinalizeStream_ToolCallAbnormalTerminationKeepsToolUseStopReason(t *testing.T) {
  842. state := NewResponsesEventToAnthropicState()
  843. ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  844. Type: "response.created",
  845. Response: &ResponsesResponse{ID: "resp_tool_interrupted", Model: "gpt-5.5"},
  846. }, state)
  847. ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  848. Type: "response.output_item.added",
  849. OutputIndex: 0,
  850. Item: &ResponsesOutput{Type: "function_call", CallID: "call_todo", Name: "TodoWrite"},
  851. }, state)
  852. events := FinalizeResponsesAnthropicStream(state)
  853. require.Len(t, events, 3)
  854. assert.Equal(t, "content_block_stop", events[0].Type)
  855. assert.Equal(t, "message_delta", events[1].Type)
  856. assert.Equal(t, "tool_use", events[1].Delta.StopReason)
  857. assert.Equal(t, "message_stop", events[2].Type)
  858. }
  859. func TestStreamingEmptyResponse(t *testing.T) {
  860. state := NewResponsesEventToAnthropicState()
  861. ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  862. Type: "response.created",
  863. Response: &ResponsesResponse{ID: "resp_6", Model: "gpt-5.2"},
  864. }, state)
  865. events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  866. Type: "response.completed",
  867. Response: &ResponsesResponse{
  868. Status: "completed",
  869. Usage: &ResponsesUsage{InputTokens: 5, OutputTokens: 0},
  870. },
  871. }, state)
  872. require.Len(t, events, 2) // message_delta + message_stop
  873. assert.Equal(t, "message_delta", events[0].Type)
  874. assert.Equal(t, "end_turn", events[0].Delta.StopReason)
  875. }
  876. func TestResponsesAnthropicEventToSSE(t *testing.T) {
  877. evt := AnthropicStreamEvent{
  878. Type: "message_start",
  879. Message: &AnthropicResponse{
  880. ID: "resp_1",
  881. Type: "message",
  882. Role: "assistant",
  883. },
  884. }
  885. sse, err := ResponsesAnthropicEventToSSE(evt)
  886. require.NoError(t, err)
  887. assert.Contains(t, sse, "event: message_start\n")
  888. assert.Contains(t, sse, "data: ")
  889. assert.Contains(t, sse, `"resp_1"`)
  890. }
  891. // ---------------------------------------------------------------------------
  892. // response.failed tests
  893. // ---------------------------------------------------------------------------
  894. func TestStreamingFailed(t *testing.T) {
  895. state := NewResponsesEventToAnthropicState()
  896. // 1. response.created
  897. ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  898. Type: "response.created",
  899. Response: &ResponsesResponse{ID: "resp_fail_1", Model: "gpt-5.2"},
  900. }, state)
  901. // 2. Some text output before failure
  902. ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  903. Type: "response.output_text.delta",
  904. Delta: "Partial output before failure",
  905. }, state)
  906. // 3. response.failed
  907. events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  908. Type: "response.failed",
  909. Response: &ResponsesResponse{
  910. Status: "failed",
  911. Error: &ResponsesError{Code: "server_error", Message: "Internal error"},
  912. Usage: &ResponsesUsage{InputTokens: 50, OutputTokens: 10},
  913. },
  914. }, state)
  915. // Should close text block + message_delta + message_stop
  916. require.Len(t, events, 3)
  917. assert.Equal(t, "content_block_stop", events[0].Type)
  918. assert.Equal(t, "message_delta", events[1].Type)
  919. assert.Equal(t, "end_turn", events[1].Delta.StopReason)
  920. assert.Equal(t, 50, events[1].Usage.InputTokens)
  921. assert.Equal(t, 10, events[1].Usage.OutputTokens)
  922. assert.Equal(t, "message_stop", events[2].Type)
  923. }
  924. func TestStreamingFailedNoOutput(t *testing.T) {
  925. state := NewResponsesEventToAnthropicState()
  926. // 1. response.created
  927. ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  928. Type: "response.created",
  929. Response: &ResponsesResponse{ID: "resp_fail_2", Model: "gpt-5.2"},
  930. }, state)
  931. // 2. response.failed with no prior output
  932. events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  933. Type: "response.failed",
  934. Response: &ResponsesResponse{
  935. Status: "failed",
  936. Error: &ResponsesError{Code: "rate_limit_error", Message: "Too many requests"},
  937. Usage: &ResponsesUsage{InputTokens: 20, OutputTokens: 0},
  938. },
  939. }, state)
  940. // Should emit message_delta + message_stop (no block to close)
  941. require.Len(t, events, 2)
  942. assert.Equal(t, "message_delta", events[0].Type)
  943. assert.Equal(t, "end_turn", events[0].Delta.StopReason)
  944. assert.Equal(t, "message_stop", events[1].Type)
  945. }
  946. func TestResponsesToAnthropic_Failed(t *testing.T) {
  947. resp := &ResponsesResponse{
  948. ID: "resp_fail_3",
  949. Model: "gpt-5.2",
  950. Status: "failed",
  951. Error: &ResponsesError{Code: "server_error", Message: "Something went wrong"},
  952. Output: []ResponsesOutput{},
  953. Usage: &ResponsesUsage{InputTokens: 30, OutputTokens: 0},
  954. }
  955. anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
  956. // Failed status defaults to "end_turn" stop reason
  957. assert.Equal(t, "end_turn", AnthropicStopReasonString(anth.StopReason))
  958. // Should have at least an empty text block
  959. require.Len(t, anth.Content, 1)
  960. assert.Equal(t, "text", anth.Content[0].Type)
  961. }
  962. // ---------------------------------------------------------------------------
  963. // thinking → reasoning conversion tests
  964. // ---------------------------------------------------------------------------
  965. func TestAnthropicToResponses_ThinkingEnabled(t *testing.T) {
  966. req := &AnthropicRequest{
  967. Model: "gpt-5.2",
  968. MaxTokens: 1024,
  969. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
  970. Thinking: &AnthropicThinking{Type: "enabled", BudgetTokens: 10000},
  971. }
  972. resp, err := AnthropicToResponses(req)
  973. require.NoError(t, err)
  974. require.NotNil(t, resp.Reasoning)
  975. // thinking.type is ignored for effort; Codex bridge default medium applies.
  976. assert.Equal(t, "medium", resp.Reasoning.Effort)
  977. assert.Equal(t, "auto", resp.Reasoning.Summary)
  978. assert.Contains(t, resp.Include, "reasoning.encrypted_content")
  979. assert.NotContains(t, resp.Include, "reasoning.summary")
  980. }
  981. func TestAnthropicToResponses_ThinkingAdaptive(t *testing.T) {
  982. req := &AnthropicRequest{
  983. Model: "gpt-5.2",
  984. MaxTokens: 1024,
  985. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
  986. Thinking: &AnthropicThinking{Type: "adaptive", BudgetTokens: 5000},
  987. }
  988. resp, err := AnthropicToResponses(req)
  989. require.NoError(t, err)
  990. require.NotNil(t, resp.Reasoning)
  991. // thinking.type is ignored for effort; Codex bridge default medium applies.
  992. assert.Equal(t, "medium", resp.Reasoning.Effort)
  993. assert.Equal(t, "auto", resp.Reasoning.Summary)
  994. assert.NotContains(t, resp.Include, "reasoning.summary")
  995. }
  996. func TestAnthropicToResponses_ThinkingDisabled(t *testing.T) {
  997. req := &AnthropicRequest{
  998. Model: "gpt-5.2",
  999. MaxTokens: 1024,
  1000. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
  1001. Thinking: &AnthropicThinking{Type: "disabled"},
  1002. }
  1003. resp, err := AnthropicToResponses(req)
  1004. require.NoError(t, err)
  1005. // Default effort applies (medium) even when thinking is disabled.
  1006. require.NotNil(t, resp.Reasoning)
  1007. assert.Equal(t, "medium", resp.Reasoning.Effort)
  1008. }
  1009. func TestAnthropicToResponses_NoThinking(t *testing.T) {
  1010. req := &AnthropicRequest{
  1011. Model: "gpt-5.2",
  1012. MaxTokens: 1024,
  1013. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
  1014. }
  1015. resp, err := AnthropicToResponses(req)
  1016. require.NoError(t, err)
  1017. // Default effort applies (medium) when no thinking/output_config is set.
  1018. require.NotNil(t, resp.Reasoning)
  1019. assert.Equal(t, "medium", resp.Reasoning.Effort)
  1020. }
  1021. // ---------------------------------------------------------------------------
  1022. // output_config.effort override tests
  1023. // ---------------------------------------------------------------------------
  1024. func TestAnthropicToResponses_OutputConfigOverridesDefault(t *testing.T) {
  1025. // Default is medium, but output_config.effort="low" overrides. low→low after mapping.
  1026. req := &AnthropicRequest{
  1027. Model: "gpt-5.2",
  1028. MaxTokens: 1024,
  1029. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
  1030. Thinking: &AnthropicThinking{Type: "enabled", BudgetTokens: 10000},
  1031. OutputConfig: &AnthropicOutputConfig{Effort: "low"},
  1032. }
  1033. resp, err := AnthropicToResponses(req)
  1034. require.NoError(t, err)
  1035. require.NotNil(t, resp.Reasoning)
  1036. assert.Equal(t, "low", resp.Reasoning.Effort)
  1037. assert.Equal(t, "auto", resp.Reasoning.Summary)
  1038. }
  1039. func TestAnthropicToResponses_OutputConfigWithoutThinking(t *testing.T) {
  1040. // No thinking field, but output_config.effort="medium" → creates reasoning.
  1041. // medium→medium after 1:1 mapping.
  1042. req := &AnthropicRequest{
  1043. Model: "gpt-5.2",
  1044. MaxTokens: 1024,
  1045. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
  1046. OutputConfig: &AnthropicOutputConfig{Effort: "medium"},
  1047. }
  1048. resp, err := AnthropicToResponses(req)
  1049. require.NoError(t, err)
  1050. require.NotNil(t, resp.Reasoning)
  1051. assert.Equal(t, "medium", resp.Reasoning.Effort)
  1052. assert.Equal(t, "auto", resp.Reasoning.Summary)
  1053. }
  1054. func TestAnthropicToResponses_OutputConfigHigh(t *testing.T) {
  1055. // output_config.effort="high" → mapped to "high" (1:1).
  1056. req := &AnthropicRequest{
  1057. Model: "gpt-5.2",
  1058. MaxTokens: 1024,
  1059. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
  1060. OutputConfig: &AnthropicOutputConfig{Effort: "high"},
  1061. }
  1062. resp, err := AnthropicToResponses(req)
  1063. require.NoError(t, err)
  1064. require.NotNil(t, resp.Reasoning)
  1065. assert.Equal(t, "high", resp.Reasoning.Effort)
  1066. assert.Equal(t, "auto", resp.Reasoning.Summary)
  1067. }
  1068. func TestAnthropicToResponses_OutputConfigMax(t *testing.T) {
  1069. // output_config.effort="max" → mapped to OpenAI's highest supported level "xhigh".
  1070. req := &AnthropicRequest{
  1071. Model: "gpt-5.2",
  1072. MaxTokens: 1024,
  1073. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
  1074. OutputConfig: &AnthropicOutputConfig{Effort: "max"},
  1075. }
  1076. resp, err := AnthropicToResponses(req)
  1077. require.NoError(t, err)
  1078. require.NotNil(t, resp.Reasoning)
  1079. assert.Equal(t, "xhigh", resp.Reasoning.Effort)
  1080. assert.Equal(t, "auto", resp.Reasoning.Summary)
  1081. }
  1082. func TestAnthropicToResponses_NoOutputConfig(t *testing.T) {
  1083. // No output_config → default medium regardless of thinking.type.
  1084. req := &AnthropicRequest{
  1085. Model: "gpt-5.2",
  1086. MaxTokens: 1024,
  1087. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
  1088. Thinking: &AnthropicThinking{Type: "enabled", BudgetTokens: 10000},
  1089. }
  1090. resp, err := AnthropicToResponses(req)
  1091. require.NoError(t, err)
  1092. require.NotNil(t, resp.Reasoning)
  1093. assert.Equal(t, "medium", resp.Reasoning.Effort)
  1094. }
  1095. func TestAnthropicToResponses_OutputConfigWithoutEffort(t *testing.T) {
  1096. // output_config present but effort empty (e.g. only format set) → default medium.
  1097. req := &AnthropicRequest{
  1098. Model: "gpt-5.2",
  1099. MaxTokens: 1024,
  1100. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
  1101. OutputConfig: &AnthropicOutputConfig{},
  1102. }
  1103. resp, err := AnthropicToResponses(req)
  1104. require.NoError(t, err)
  1105. require.NotNil(t, resp.Reasoning)
  1106. assert.Equal(t, "medium", resp.Reasoning.Effort)
  1107. }
  1108. // ---------------------------------------------------------------------------
  1109. // tool_choice conversion tests
  1110. // ---------------------------------------------------------------------------
  1111. func TestAnthropicToResponses_ToolChoiceAuto(t *testing.T) {
  1112. req := &AnthropicRequest{
  1113. Model: "gpt-5.2",
  1114. MaxTokens: 1024,
  1115. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
  1116. ToolChoice: json.RawMessage(`{"type":"auto"}`),
  1117. }
  1118. resp, err := AnthropicToResponses(req)
  1119. require.NoError(t, err)
  1120. var tc string
  1121. require.NoError(t, json.Unmarshal(resp.ToolChoice, &tc))
  1122. assert.Equal(t, "auto", tc)
  1123. }
  1124. func TestAnthropicToResponses_ToolChoiceAny(t *testing.T) {
  1125. req := &AnthropicRequest{
  1126. Model: "gpt-5.2",
  1127. MaxTokens: 1024,
  1128. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
  1129. ToolChoice: json.RawMessage(`{"type":"any"}`),
  1130. }
  1131. resp, err := AnthropicToResponses(req)
  1132. require.NoError(t, err)
  1133. var tc string
  1134. require.NoError(t, json.Unmarshal(resp.ToolChoice, &tc))
  1135. assert.Equal(t, "required", tc)
  1136. }
  1137. func TestAnthropicToResponses_ToolChoiceSpecific(t *testing.T) {
  1138. req := &AnthropicRequest{
  1139. Model: "gpt-5.2",
  1140. MaxTokens: 1024,
  1141. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
  1142. ToolChoice: json.RawMessage(`{"type":"tool","name":"get_weather"}`),
  1143. }
  1144. resp, err := AnthropicToResponses(req)
  1145. require.NoError(t, err)
  1146. var tc map[string]any
  1147. require.NoError(t, json.Unmarshal(resp.ToolChoice, &tc))
  1148. assert.Equal(t, "function", tc["type"])
  1149. assert.Equal(t, "get_weather", tc["name"])
  1150. assert.NotContains(t, tc, "function")
  1151. }
  1152. func TestResponsesToAnthropicRequest_ToolChoiceFunctionName(t *testing.T) {
  1153. req := &ResponsesRequest{
  1154. Model: "gpt-5.2",
  1155. Input: json.RawMessage(`[{"role":"user","content":"Hello"}]`),
  1156. ToolChoice: json.RawMessage(`{"type":"function","name":"get_weather"}`),
  1157. }
  1158. resp, err := ResponsesToAnthropicRequest(req)
  1159. require.NoError(t, err)
  1160. var tc map[string]string
  1161. require.NoError(t, json.Unmarshal(resp.ToolChoice, &tc))
  1162. assert.Equal(t, "tool", tc["type"])
  1163. assert.Equal(t, "get_weather", tc["name"])
  1164. }
  1165. func TestResponsesToAnthropicRequest_ToolChoiceLegacyFunctionName(t *testing.T) {
  1166. req := &ResponsesRequest{
  1167. Model: "gpt-5.2",
  1168. Input: json.RawMessage(`[{"role":"user","content":"Hello"}]`),
  1169. ToolChoice: json.RawMessage(`{"type":"function","function":{"name":"get_weather"}}`),
  1170. }
  1171. resp, err := ResponsesToAnthropicRequest(req)
  1172. require.NoError(t, err)
  1173. var tc map[string]string
  1174. require.NoError(t, json.Unmarshal(resp.ToolChoice, &tc))
  1175. assert.Equal(t, "tool", tc["type"])
  1176. assert.Equal(t, "get_weather", tc["name"])
  1177. }
  1178. // ---------------------------------------------------------------------------
  1179. // Image content block conversion tests
  1180. // ---------------------------------------------------------------------------
  1181. func TestAnthropicToResponses_UserImageBlock(t *testing.T) {
  1182. req := &AnthropicRequest{
  1183. Model: "gpt-5.2",
  1184. MaxTokens: 1024,
  1185. Messages: []AnthropicMessage{
  1186. {Role: "user", Content: json.RawMessage(`[
  1187. {"type":"text","text":"What is in this image?"},
  1188. {"type":"image","source":{"type":"base64","media_type":"image/png","data":"iVBOR"}}
  1189. ]`)},
  1190. },
  1191. }
  1192. resp, err := AnthropicToResponses(req)
  1193. require.NoError(t, err)
  1194. var items []ResponsesInputItem
  1195. require.NoError(t, json.Unmarshal(resp.Input, &items))
  1196. require.Len(t, items, 1)
  1197. assert.Equal(t, "user", items[0].Role)
  1198. var parts []ResponsesContentPart
  1199. require.NoError(t, json.Unmarshal(items[0].Content, &parts))
  1200. require.Len(t, parts, 2)
  1201. assert.Equal(t, "input_text", parts[0].Type)
  1202. assert.Equal(t, "What is in this image?", parts[0].Text)
  1203. assert.Equal(t, "input_image", parts[1].Type)
  1204. assert.Equal(t, "data:image/png;base64,iVBOR", parts[1].ImageURL)
  1205. }
  1206. func TestAnthropicToResponses_ImageOnlyUserMessage(t *testing.T) {
  1207. req := &AnthropicRequest{
  1208. Model: "gpt-5.2",
  1209. MaxTokens: 1024,
  1210. Messages: []AnthropicMessage{
  1211. {Role: "user", Content: json.RawMessage(`[
  1212. {"type":"image","source":{"type":"base64","media_type":"image/jpeg","data":"/9j/4AAQ"}}
  1213. ]`)},
  1214. },
  1215. }
  1216. resp, err := AnthropicToResponses(req)
  1217. require.NoError(t, err)
  1218. var items []ResponsesInputItem
  1219. require.NoError(t, json.Unmarshal(resp.Input, &items))
  1220. require.Len(t, items, 1)
  1221. var parts []ResponsesContentPart
  1222. require.NoError(t, json.Unmarshal(items[0].Content, &parts))
  1223. require.Len(t, parts, 1)
  1224. assert.Equal(t, "input_image", parts[0].Type)
  1225. assert.Equal(t, "data:image/jpeg;base64,/9j/4AAQ", parts[0].ImageURL)
  1226. }
  1227. func TestAnthropicToResponses_ToolResultWithImage(t *testing.T) {
  1228. req := &AnthropicRequest{
  1229. Model: "gpt-5.2",
  1230. MaxTokens: 1024,
  1231. Messages: []AnthropicMessage{
  1232. {Role: "user", Content: json.RawMessage(`"Read the screenshot"`)},
  1233. {Role: "assistant", Content: json.RawMessage(`[{"type":"tool_use","id":"toolu_1","name":"Read","input":{"file_path":"/tmp/screen.png"}}]`)},
  1234. {Role: "user", Content: json.RawMessage(`[
  1235. {"type":"tool_result","tool_use_id":"toolu_1","content":[
  1236. {"type":"image","source":{"type":"base64","media_type":"image/png","data":"iVBOR"}}
  1237. ]}
  1238. ]`)},
  1239. },
  1240. }
  1241. resp, err := AnthropicToResponses(req)
  1242. require.NoError(t, err)
  1243. var items []ResponsesInputItem
  1244. require.NoError(t, json.Unmarshal(resp.Input, &items))
  1245. // user + function_call + function_call_output + user(image) = 4
  1246. require.Len(t, items, 4)
  1247. // function_call_output should have text-only output (no image).
  1248. assert.Equal(t, "function_call_output", items[2].Type)
  1249. assert.Equal(t, "toolu_1", items[2].CallID)
  1250. assert.Equal(t, "(empty)", items[2].Output)
  1251. // Image should be in a separate user message.
  1252. assert.Equal(t, "user", items[3].Role)
  1253. var parts []ResponsesContentPart
  1254. require.NoError(t, json.Unmarshal(items[3].Content, &parts))
  1255. require.Len(t, parts, 1)
  1256. assert.Equal(t, "input_image", parts[0].Type)
  1257. assert.Equal(t, "data:image/png;base64,iVBOR", parts[0].ImageURL)
  1258. }
  1259. func TestAnthropicToResponses_ToolResultMixed(t *testing.T) {
  1260. req := &AnthropicRequest{
  1261. Model: "gpt-5.2",
  1262. MaxTokens: 1024,
  1263. Messages: []AnthropicMessage{
  1264. {Role: "user", Content: json.RawMessage(`"Describe the file"`)},
  1265. {Role: "assistant", Content: json.RawMessage(`[{"type":"tool_use","id":"toolu_2","name":"Read","input":{"file_path":"/tmp/photo.png"}}]`)},
  1266. {Role: "user", Content: json.RawMessage(`[
  1267. {"type":"tool_result","tool_use_id":"toolu_2","content":[
  1268. {"type":"text","text":"File metadata: 800x600 PNG"},
  1269. {"type":"image","source":{"type":"base64","media_type":"image/png","data":"AAAA"}}
  1270. ]}
  1271. ]`)},
  1272. },
  1273. }
  1274. resp, err := AnthropicToResponses(req)
  1275. require.NoError(t, err)
  1276. var items []ResponsesInputItem
  1277. require.NoError(t, json.Unmarshal(resp.Input, &items))
  1278. // user + function_call + function_call_output + user(image) = 4
  1279. require.Len(t, items, 4)
  1280. // function_call_output should have text-only output.
  1281. assert.Equal(t, "function_call_output", items[2].Type)
  1282. assert.Equal(t, "File metadata: 800x600 PNG", items[2].Output)
  1283. // Image should be in a separate user message.
  1284. assert.Equal(t, "user", items[3].Role)
  1285. var parts []ResponsesContentPart
  1286. require.NoError(t, json.Unmarshal(items[3].Content, &parts))
  1287. require.Len(t, parts, 1)
  1288. assert.Equal(t, "input_image", parts[0].Type)
  1289. assert.Equal(t, "data:image/png;base64,AAAA", parts[0].ImageURL)
  1290. }
  1291. func TestAnthropicToResponses_TextOnlyToolResultBackwardCompat(t *testing.T) {
  1292. req := &AnthropicRequest{
  1293. Model: "gpt-5.2",
  1294. MaxTokens: 1024,
  1295. Messages: []AnthropicMessage{
  1296. {Role: "user", Content: json.RawMessage(`"Check weather"`)},
  1297. {Role: "assistant", Content: json.RawMessage(`[{"type":"tool_use","id":"call_1","name":"get_weather","input":{"city":"NYC"}}]`)},
  1298. {Role: "user", Content: json.RawMessage(`[
  1299. {"type":"tool_result","tool_use_id":"call_1","content":[
  1300. {"type":"text","text":"Sunny, 72°F"}
  1301. ]}
  1302. ]`)},
  1303. },
  1304. }
  1305. resp, err := AnthropicToResponses(req)
  1306. require.NoError(t, err)
  1307. var items []ResponsesInputItem
  1308. require.NoError(t, json.Unmarshal(resp.Input, &items))
  1309. // user + function_call + function_call_output = 3
  1310. require.Len(t, items, 3)
  1311. // Text-only tool_result should produce a plain string.
  1312. assert.Equal(t, "Sunny, 72°F", items[2].Output)
  1313. }
  1314. func TestAnthropicToResponses_ImageEmptyMediaType(t *testing.T) {
  1315. req := &AnthropicRequest{
  1316. Model: "gpt-5.2",
  1317. MaxTokens: 1024,
  1318. Messages: []AnthropicMessage{
  1319. {Role: "user", Content: json.RawMessage(`[
  1320. {"type":"image","source":{"type":"base64","media_type":"","data":"iVBOR"}}
  1321. ]`)},
  1322. },
  1323. }
  1324. resp, err := AnthropicToResponses(req)
  1325. require.NoError(t, err)
  1326. var items []ResponsesInputItem
  1327. require.NoError(t, json.Unmarshal(resp.Input, &items))
  1328. require.Len(t, items, 1)
  1329. var parts []ResponsesContentPart
  1330. require.NoError(t, json.Unmarshal(items[0].Content, &parts))
  1331. require.Len(t, parts, 1)
  1332. assert.Equal(t, "input_image", parts[0].Type)
  1333. // Should default to image/png when media_type is empty.
  1334. assert.Equal(t, "data:image/png;base64,iVBOR", parts[0].ImageURL)
  1335. }
  1336. // ---------------------------------------------------------------------------
  1337. // normalizeToolParameters tests
  1338. // ---------------------------------------------------------------------------
  1339. func TestNormalizeToolParameters(t *testing.T) {
  1340. tests := []struct {
  1341. name string
  1342. input json.RawMessage
  1343. expected string
  1344. }{
  1345. {
  1346. name: "nil input",
  1347. input: nil,
  1348. expected: `{"type":"object","properties":{}}`,
  1349. },
  1350. {
  1351. name: "empty input",
  1352. input: json.RawMessage(``),
  1353. expected: `{"type":"object","properties":{}}`,
  1354. },
  1355. {
  1356. name: "null input",
  1357. input: json.RawMessage(`null`),
  1358. expected: `{"type":"object","properties":{}}`,
  1359. },
  1360. {
  1361. name: "object without properties",
  1362. input: json.RawMessage(`{"type":"object"}`),
  1363. expected: `{"type":"object","properties":{}}`,
  1364. },
  1365. {
  1366. name: "object with properties",
  1367. input: json.RawMessage(`{"type":"object","properties":{"city":{"type":"string"}}}`),
  1368. expected: `{"type":"object","properties":{"city":{"type":"string"}}}`,
  1369. },
  1370. {
  1371. name: "non-object type",
  1372. input: json.RawMessage(`{"type":"string"}`),
  1373. expected: `{"type":"string"}`,
  1374. },
  1375. {
  1376. name: "object with additional fields preserved",
  1377. input: json.RawMessage(`{"type":"object","required":["name"]}`),
  1378. expected: `{"type":"object","required":["name"],"properties":{}}`,
  1379. },
  1380. {
  1381. name: "invalid JSON passthrough",
  1382. input: json.RawMessage(`not json`),
  1383. expected: `not json`,
  1384. },
  1385. }
  1386. for _, tt := range tests {
  1387. t.Run(tt.name, func(t *testing.T) {
  1388. result := normalizeToolParameters(tt.input)
  1389. if tt.name == "invalid JSON passthrough" {
  1390. assert.Equal(t, tt.expected, string(result))
  1391. } else {
  1392. assert.JSONEq(t, tt.expected, string(result))
  1393. }
  1394. })
  1395. }
  1396. }
  1397. func TestAnthropicToResponses_ToolWithoutProperties(t *testing.T) {
  1398. req := &AnthropicRequest{
  1399. Model: "gpt-5.2",
  1400. MaxTokens: 1024,
  1401. Messages: []AnthropicMessage{
  1402. {Role: "user", Content: json.RawMessage(`"Hello"`)},
  1403. },
  1404. Tools: []AnthropicTool{
  1405. {Name: "mcp__pencil__get_style_guide_tags", Description: "Get style tags", InputSchema: json.RawMessage(`{"type":"object"}`)},
  1406. },
  1407. }
  1408. resp, err := AnthropicToResponses(req)
  1409. require.NoError(t, err)
  1410. require.Len(t, resp.Tools, 1)
  1411. assert.Equal(t, "function", resp.Tools[0].Type)
  1412. assert.Equal(t, "mcp__pencil__get_style_guide_tags", resp.Tools[0].Name)
  1413. // Parameters must have "properties" field after normalization.
  1414. var params map[string]json.RawMessage
  1415. require.NoError(t, json.Unmarshal(resp.Tools[0].Parameters, &params))
  1416. assert.Contains(t, params, "properties")
  1417. }
  1418. func TestAnthropicToResponses_ToolWithNilSchema(t *testing.T) {
  1419. req := &AnthropicRequest{
  1420. Model: "gpt-5.2",
  1421. MaxTokens: 1024,
  1422. Messages: []AnthropicMessage{
  1423. {Role: "user", Content: json.RawMessage(`"Hello"`)},
  1424. },
  1425. Tools: []AnthropicTool{
  1426. {Name: "simple_tool", Description: "A tool"},
  1427. },
  1428. }
  1429. resp, err := AnthropicToResponses(req)
  1430. require.NoError(t, err)
  1431. require.Len(t, resp.Tools, 1)
  1432. var params map[string]json.RawMessage
  1433. require.NoError(t, json.Unmarshal(resp.Tools[0].Parameters, &params))
  1434. assert.JSONEq(t, `"object"`, string(params["type"]))
  1435. assert.JSONEq(t, `{}`, string(params["properties"]))
  1436. }
  1437. // ---------------------------------------------------------------------------
  1438. // isReasoningModel / temperature-stripping tests
  1439. // ---------------------------------------------------------------------------
  1440. func TestAnthropicToResponses_TemperatureStrippedForReasoningModel(t *testing.T) {
  1441. temp := 0.7
  1442. req := &AnthropicRequest{
  1443. Model: "gpt-5.2",
  1444. MaxTokens: 1024,
  1445. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
  1446. Temperature: &temp,
  1447. TopP: &temp,
  1448. }
  1449. resp, err := AnthropicToResponses(req)
  1450. require.NoError(t, err)
  1451. assert.Nil(t, resp.Temperature, "reasoning model: temperature must be stripped")
  1452. assert.Nil(t, resp.TopP, "reasoning model: top_p must be stripped")
  1453. // Verify the fields are absent from the serialised JSON.
  1454. b, err := json.Marshal(resp)
  1455. require.NoError(t, err)
  1456. assert.NotContains(t, string(b), `"temperature"`)
  1457. assert.NotContains(t, string(b), `"top_p"`)
  1458. }
  1459. func TestAnthropicToResponses_TemperatureStrippedForAllGpt5Variants(t *testing.T) {
  1460. temp := 1.0
  1461. models := []string{"gpt-5.2", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex", "gpt-5.5"}
  1462. for _, model := range models {
  1463. t.Run(model, func(t *testing.T) {
  1464. req := &AnthropicRequest{
  1465. Model: model,
  1466. MaxTokens: 1024,
  1467. Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
  1468. Temperature: &temp,
  1469. TopP: &temp,
  1470. }
  1471. resp, err := AnthropicToResponses(req)
  1472. require.NoError(t, err)
  1473. assert.Nil(t, resp.Temperature, "model %s: temperature must be stripped", model)
  1474. assert.Nil(t, resp.TopP, "model %s: top_p must be stripped", model)
  1475. })
  1476. }
  1477. }
  1478. // ---------------------------------------------------------------------------
  1479. // AnthropicToResponsesResponse: Anthropic input_tokens excludes cached tokens
  1480. // while OpenAI Responses input_tokens is the total including cached tokens.
  1481. // ---------------------------------------------------------------------------
  1482. func TestAnthropicToResponsesResponse_CacheTokensUseOpenAIInputSemantics(t *testing.T) {
  1483. resp := &AnthropicResponse{
  1484. ID: "msg_cache",
  1485. Model: "claude-sonnet-4-5-20250929",
  1486. Content: []AnthropicContentBlock{
  1487. {Type: "text", Text: "ok"},
  1488. },
  1489. StopReason: AnthropicStopReasonPtr("end_turn"),
  1490. Usage: AnthropicUsage{
  1491. InputTokens: 3318,
  1492. OutputTokens: 123,
  1493. CacheReadInputTokens: 50688,
  1494. CacheCreationInputTokens: 200,
  1495. },
  1496. }
  1497. out := AnthropicToResponsesResponse(resp)
  1498. require.NotNil(t, out.Usage)
  1499. // 3318 (uncached) + 50688 (read) + 200 (creation) = 54206
  1500. assert.Equal(t, 54206, out.Usage.InputTokens)
  1501. assert.Equal(t, 123, out.Usage.OutputTokens)
  1502. assert.Equal(t, 54329, out.Usage.TotalTokens)
  1503. require.NotNil(t, out.Usage.InputTokensDetails)
  1504. assert.Equal(t, 50688, out.Usage.InputTokensDetails.CachedTokens)
  1505. }
  1506. func TestAnthropicToResponsesResponse_NoCacheTokens(t *testing.T) {
  1507. resp := &AnthropicResponse{
  1508. ID: "msg_nocache",
  1509. Model: "claude-sonnet-4-5-20250929",
  1510. Content: []AnthropicContentBlock{
  1511. {Type: "text", Text: "ok"},
  1512. },
  1513. StopReason: AnthropicStopReasonPtr("end_turn"),
  1514. Usage: AnthropicUsage{
  1515. InputTokens: 100,
  1516. OutputTokens: 50,
  1517. },
  1518. }
  1519. out := AnthropicToResponsesResponse(resp)
  1520. require.NotNil(t, out.Usage)
  1521. assert.Equal(t, 100, out.Usage.InputTokens)
  1522. assert.Equal(t, 50, out.Usage.OutputTokens)
  1523. assert.Equal(t, 150, out.Usage.TotalTokens)
  1524. assert.Nil(t, out.Usage.InputTokensDetails)
  1525. }
  1526. func TestAnthropicEventToResponses_CacheTokensRoundTripFromMessageStart(t *testing.T) {
  1527. state := NewAnthropicEventToResponsesState()
  1528. // message_start carries cache fields on the initial Usage object.
  1529. AnthropicEventToResponsesEvents(&AnthropicStreamEvent{
  1530. Type: "message_start",
  1531. Message: &AnthropicResponse{
  1532. ID: "msg_stream_cache",
  1533. Model: "claude-sonnet-4-5-20250929",
  1534. Usage: AnthropicUsage{
  1535. InputTokens: 12,
  1536. CacheReadInputTokens: 9,
  1537. CacheCreationInputTokens: 3,
  1538. },
  1539. },
  1540. }, state)
  1541. AnthropicEventToResponsesEvents(&AnthropicStreamEvent{
  1542. Type: "message_delta",
  1543. Usage: &AnthropicUsage{
  1544. OutputTokens: 7,
  1545. },
  1546. }, state)
  1547. events := AnthropicEventToResponsesEvents(&AnthropicStreamEvent{Type: "message_stop"}, state)
  1548. // The terminal response.completed event must include OpenAI-semantic usage.
  1549. var completed *ResponsesStreamEvent
  1550. for i := range events {
  1551. if events[i].Type == "response.completed" {
  1552. completed = &events[i]
  1553. }
  1554. }
  1555. require.NotNil(t, completed, "response.completed event must be emitted")
  1556. require.NotNil(t, completed.Response)
  1557. require.NotNil(t, completed.Response.Usage)
  1558. // 12 (uncached) + 9 (read) + 3 (creation) = 24
  1559. assert.Equal(t, 24, completed.Response.Usage.InputTokens)
  1560. assert.Equal(t, 7, completed.Response.Usage.OutputTokens)
  1561. assert.Equal(t, 31, completed.Response.Usage.TotalTokens)
  1562. require.NotNil(t, completed.Response.Usage.InputTokensDetails)
  1563. assert.Equal(t, 9, completed.Response.Usage.InputTokensDetails.CachedTokens)
  1564. }
  1565. func TestAnthropicEventToResponses_CacheTokensFromMessageDelta(t *testing.T) {
  1566. state := NewAnthropicEventToResponsesState()
  1567. AnthropicEventToResponsesEvents(&AnthropicStreamEvent{
  1568. Type: "message_start",
  1569. Message: &AnthropicResponse{
  1570. ID: "msg_delta_cache",
  1571. Model: "claude-sonnet-4-5-20250929",
  1572. Usage: AnthropicUsage{InputTokens: 20},
  1573. },
  1574. }, state)
  1575. // Some upstreams only emit cache fields on the final message_delta.
  1576. AnthropicEventToResponsesEvents(&AnthropicStreamEvent{
  1577. Type: "message_delta",
  1578. Usage: &AnthropicUsage{
  1579. OutputTokens: 8,
  1580. CacheReadInputTokens: 11,
  1581. CacheCreationInputTokens: 4,
  1582. },
  1583. }, state)
  1584. events := AnthropicEventToResponsesEvents(&AnthropicStreamEvent{Type: "message_stop"}, state)
  1585. var completed *ResponsesStreamEvent
  1586. for i := range events {
  1587. if events[i].Type == "response.completed" {
  1588. completed = &events[i]
  1589. }
  1590. }
  1591. require.NotNil(t, completed)
  1592. require.NotNil(t, completed.Response.Usage)
  1593. // 20 (uncached) + 11 (read) + 4 (creation) = 35
  1594. assert.Equal(t, 35, completed.Response.Usage.InputTokens)
  1595. assert.Equal(t, 8, completed.Response.Usage.OutputTokens)
  1596. require.NotNil(t, completed.Response.Usage.InputTokensDetails)
  1597. assert.Equal(t, 11, completed.Response.Usage.InputTokensDetails.CachedTokens)
  1598. }
  1599. func TestMessageStartSSE_StopReasonIsJSONNull(t *testing.T) {
  1600. state := NewResponsesEventToAnthropicState()
  1601. state.Model = "grok-4.5"
  1602. events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
  1603. Type: "response.created",
  1604. Response: &ResponsesResponse{
  1605. ID: "resp_test",
  1606. Model: "grok-4.5",
  1607. },
  1608. }, state)
  1609. require.Len(t, events, 1)
  1610. require.Equal(t, "message_start", events[0].Type)
  1611. require.NotNil(t, events[0].Message)
  1612. require.Nil(t, events[0].Message.StopReason, "message_start must use null stop_reason, not empty string")
  1613. sse, err := ResponsesAnthropicEventToSSE(events[0])
  1614. require.NoError(t, err)
  1615. // Official Anthropic wire: "stop_reason":null
  1616. require.Contains(t, sse, `"stop_reason":null`)
  1617. require.NotContains(t, sse, `"stop_reason":""`)
  1618. }