| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842 |
- package apicompat
- import (
- "encoding/json"
- "testing"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- // ---------------------------------------------------------------------------
- // AnthropicToResponses tests
- // ---------------------------------------------------------------------------
- func TestAnthropicToResponses_BasicText(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Stream: true,
- Messages: []AnthropicMessage{
- {Role: "user", Content: json.RawMessage(`"Hello"`)},
- },
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- assert.Equal(t, "gpt-5.2", resp.Model)
- assert.True(t, resp.Stream)
- assert.Equal(t, 1024, *resp.MaxOutputTokens)
- assert.False(t, *resp.Store)
- var items []ResponsesInputItem
- require.NoError(t, json.Unmarshal(resp.Input, &items))
- require.Len(t, items, 1)
- assert.Equal(t, "message", items[0].Type)
- assert.Equal(t, "user", items[0].Role)
- var parts []ResponsesContentPart
- require.NoError(t, json.Unmarshal(items[0].Content, &parts))
- require.Len(t, parts, 1)
- assert.Equal(t, "input_text", parts[0].Type)
- assert.Equal(t, "Hello", parts[0].Text)
- }
- func TestAnthropicToResponses_SystemPrompt(t *testing.T) {
- t.Run("string", func(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 100,
- System: json.RawMessage(`"You are helpful."`),
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}},
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- var items []ResponsesInputItem
- require.NoError(t, json.Unmarshal(resp.Input, &items))
- require.Len(t, items, 2)
- assert.Equal(t, "developer", items[0].Role)
- var parts []ResponsesContentPart
- require.NoError(t, json.Unmarshal(items[0].Content, &parts))
- require.Len(t, parts, 1)
- assert.Equal(t, "input_text", parts[0].Type)
- assert.Equal(t, "You are helpful.", parts[0].Text)
- })
- t.Run("array", func(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 100,
- System: json.RawMessage(`[{"type":"text","text":"Part 1"},{"type":"text","text":"Part 2"}]`),
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}},
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- var items []ResponsesInputItem
- require.NoError(t, json.Unmarshal(resp.Input, &items))
- require.Len(t, items, 2)
- assert.Equal(t, "developer", items[0].Role)
- var parts []ResponsesContentPart
- require.NoError(t, json.Unmarshal(items[0].Content, &parts))
- require.Len(t, parts, 2)
- assert.Equal(t, "input_text", parts[0].Type)
- assert.Equal(t, "Part 1", parts[0].Text)
- assert.Equal(t, "input_text", parts[1].Type)
- assert.Equal(t, "Part 2", parts[1].Text)
- })
- t.Run("billing header skipped", func(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 100,
- System: json.RawMessage(`[{"type":"text","text":"x-anthropic-billing-header: cc_version=1;"},{"type":"text","text":"Project prompt"}]`),
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}},
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- var items []ResponsesInputItem
- require.NoError(t, json.Unmarshal(resp.Input, &items))
- require.Len(t, items, 2)
- var parts []ResponsesContentPart
- require.NoError(t, json.Unmarshal(items[0].Content, &parts))
- require.Len(t, parts, 1)
- assert.Equal(t, "Project prompt", parts[0].Text)
- })
- }
- func TestAnthropicToResponses_ToolUse(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{
- {Role: "user", Content: json.RawMessage(`"What is the weather?"`)},
- {Role: "assistant", Content: json.RawMessage(`[{"type":"text","text":"Let me check."},{"type":"tool_use","id":"call_1","name":"get_weather","input":{"city":"NYC"}}]`)},
- {Role: "user", Content: json.RawMessage(`[{"type":"tool_result","tool_use_id":"call_1","content":"Sunny, 72°F"}]`)},
- },
- Tools: []AnthropicTool{
- {Name: "get_weather", Description: "Get weather", InputSchema: json.RawMessage(`{"type":"object","properties":{"city":{"type":"string"}}}`)},
- },
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- // Check tools
- require.Len(t, resp.Tools, 1)
- assert.Equal(t, "function", resp.Tools[0].Type)
- assert.Equal(t, "get_weather", resp.Tools[0].Name)
- require.NotNil(t, resp.Tools[0].Strict)
- assert.False(t, *resp.Tools[0].Strict)
- // Check input items
- var items []ResponsesInputItem
- require.NoError(t, json.Unmarshal(resp.Input, &items))
- // user + assistant + function_call + function_call_output = 4
- require.Len(t, items, 4)
- assert.Equal(t, "user", items[0].Role)
- assert.Equal(t, "assistant", items[1].Role)
- assert.Equal(t, "function_call", items[2].Type)
- assert.Equal(t, "call_1", items[2].CallID)
- assert.Empty(t, items[2].ID)
- assert.Equal(t, "function_call_output", items[3].Type)
- assert.Equal(t, "call_1", items[3].CallID)
- assert.Equal(t, "Sunny, 72°F", items[3].Output)
- }
- func TestAnthropicToResponses_ThinkingWithoutSignatureIgnored(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{
- {Role: "user", Content: json.RawMessage(`"Hello"`)},
- {Role: "assistant", Content: json.RawMessage(`[{"type":"thinking","thinking":"deep thought"},{"type":"text","text":"Hi!"}]`)},
- {Role: "user", Content: json.RawMessage(`"More"`)},
- },
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- var items []ResponsesInputItem
- require.NoError(t, json.Unmarshal(resp.Input, &items))
- // user + assistant(text only, thinking without signature ignored) + user = 3
- require.Len(t, items, 3)
- assert.Equal(t, "assistant", items[1].Role)
- // Assistant content should only have text, not thinking.
- var parts []ResponsesContentPart
- require.NoError(t, json.Unmarshal(items[1].Content, &parts))
- require.Len(t, parts, 1)
- assert.Equal(t, "output_text", parts[0].Type)
- assert.Equal(t, "Hi!", parts[0].Text)
- }
- func TestAnthropicToResponses_ThinkingSignatureBecomesReasoning(t *testing.T) {
- req := &AnthropicRequest{
- Model: "grok-4.5",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{
- {Role: "user", Content: json.RawMessage(`"Hello"`)},
- {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"}}]`)},
- {Role: "user", Content: json.RawMessage(`[{"type":"tool_result","tool_use_id":"toolu_1","content":"ok"}]`)},
- },
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- var items []ResponsesInputItem
- require.NoError(t, json.Unmarshal(resp.Input, &items))
- // user + reasoning + assistant text + function_call + function_call_output
- require.GreaterOrEqual(t, len(items), 4)
- assert.Equal(t, "reasoning", items[1].Type)
- assert.Equal(t, "enc-rs-1", items[1].EncryptedContent)
- assert.Equal(t, "assistant", items[2].Role)
- assert.Equal(t, "function_call", items[3].Type)
- }
- func TestAnthropicToResponses_MaxTokensFloor(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 10, // below minMaxOutputTokens (128)
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hi"`)}},
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- assert.Equal(t, 128, *resp.MaxOutputTokens)
- }
- // ---------------------------------------------------------------------------
- // ResponsesToAnthropic (non-streaming) tests
- // ---------------------------------------------------------------------------
- func TestResponsesToAnthropic_TextOnly(t *testing.T) {
- resp := &ResponsesResponse{
- ID: "resp_123",
- Model: "gpt-5.2",
- Status: "completed",
- Output: []ResponsesOutput{
- {
- Type: "message",
- Content: []ResponsesContentPart{
- {Type: "output_text", Text: "Hello there!"},
- },
- },
- },
- Usage: &ResponsesUsage{InputTokens: 10, OutputTokens: 5, TotalTokens: 15},
- }
- anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
- assert.Equal(t, "resp_123", anth.ID)
- assert.Equal(t, "claude-opus-4-6", anth.Model)
- assert.Equal(t, "end_turn", AnthropicStopReasonString(anth.StopReason))
- require.Len(t, anth.Content, 1)
- assert.Equal(t, "text", anth.Content[0].Type)
- assert.Equal(t, "Hello there!", anth.Content[0].Text)
- assert.Equal(t, 10, anth.Usage.InputTokens)
- assert.Equal(t, 5, anth.Usage.OutputTokens)
- }
- func TestResponsesToAnthropic_CachedTokensUseAnthropicInputSemantics(t *testing.T) {
- resp := &ResponsesResponse{
- ID: "resp_cached",
- Model: "gpt-5.2",
- Status: "completed",
- Output: []ResponsesOutput{
- {
- Type: "message",
- Content: []ResponsesContentPart{
- {Type: "output_text", Text: "Cached response"},
- },
- },
- },
- Usage: &ResponsesUsage{
- InputTokens: 54006,
- OutputTokens: 123,
- TotalTokens: 54129,
- InputTokensDetails: &ResponsesInputTokensDetails{
- CachedTokens: 50688,
- },
- },
- }
- anth := ResponsesToAnthropic(resp, "claude-sonnet-4-5-20250929")
- assert.Equal(t, 3318, anth.Usage.InputTokens)
- assert.Equal(t, 50688, anth.Usage.CacheReadInputTokens)
- assert.Equal(t, 123, anth.Usage.OutputTokens)
- }
- func TestResponsesToAnthropic_CachedTokensClampInputTokens(t *testing.T) {
- resp := &ResponsesResponse{
- ID: "resp_cached_clamp",
- Model: "gpt-5.2",
- Status: "completed",
- Usage: &ResponsesUsage{
- InputTokens: 100,
- OutputTokens: 5,
- InputTokensDetails: &ResponsesInputTokensDetails{
- CachedTokens: 150,
- },
- },
- }
- anth := ResponsesToAnthropic(resp, "claude-sonnet-4-5-20250929")
- assert.Equal(t, 0, anth.Usage.InputTokens)
- assert.Equal(t, 150, anth.Usage.CacheReadInputTokens)
- assert.Equal(t, 5, anth.Usage.OutputTokens)
- }
- func TestResponsesToAnthropic_ToolUse(t *testing.T) {
- resp := &ResponsesResponse{
- ID: "resp_456",
- Model: "gpt-5.2",
- Status: "completed",
- Output: []ResponsesOutput{
- {
- Type: "message",
- Content: []ResponsesContentPart{
- {Type: "output_text", Text: "Let me check."},
- },
- },
- {
- Type: "function_call",
- CallID: "call_1",
- Name: "get_weather",
- Arguments: `{"city":"NYC"}`,
- },
- },
- }
- anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
- assert.Equal(t, "tool_use", AnthropicStopReasonString(anth.StopReason))
- require.Len(t, anth.Content, 2)
- assert.Equal(t, "text", anth.Content[0].Type)
- assert.Equal(t, "tool_use", anth.Content[1].Type)
- assert.Equal(t, "call_1", anth.Content[1].ID)
- assert.Equal(t, "get_weather", anth.Content[1].Name)
- assert.JSONEq(t, `{"city":"NYC"}`, string(anth.Content[1].Input))
- }
- func TestResponsesToAnthropic_ToolUseStopReasonDoesNotDependOnLastBlock(t *testing.T) {
- resp := &ResponsesResponse{
- ID: "resp_tool_then_text",
- Model: "gpt-5.5",
- Status: "completed",
- Output: []ResponsesOutput{
- {
- Type: "function_call",
- CallID: "call_todo",
- Name: "TodoWrite",
- Arguments: `{"todos":[{"content":"review changes","status":"in_progress"}]}`,
- },
- {
- Type: "message",
- Content: []ResponsesContentPart{
- {Type: "output_text", Text: "Task list updated."},
- },
- },
- },
- }
- anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
- assert.Equal(t, "tool_use", AnthropicStopReasonString(anth.StopReason))
- require.Len(t, anth.Content, 2)
- assert.Equal(t, "tool_use", anth.Content[0].Type)
- assert.Equal(t, "text", anth.Content[1].Type)
- }
- func TestResponsesToAnthropic_ReadToolDropsEmptyPages(t *testing.T) {
- resp := &ResponsesResponse{
- ID: "resp_read",
- Model: "gpt-5.5",
- Status: "completed",
- Output: []ResponsesOutput{
- {
- Type: "function_call",
- CallID: "call_read",
- Name: "Read",
- Arguments: `{"file_path":"/tmp/demo.py","limit":2000,"offset":0,"pages":""}`,
- },
- },
- }
- anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
- require.Len(t, anth.Content, 1)
- assert.Equal(t, "tool_use", anth.Content[0].Type)
- assert.JSONEq(t, `{"file_path":"/tmp/demo.py","limit":2000,"offset":0}`, string(anth.Content[0].Input))
- }
- func TestResponsesToAnthropic_PreservesEmptyStringsForOtherTools(t *testing.T) {
- resp := &ResponsesResponse{
- ID: "resp_other",
- Model: "gpt-5.5",
- Status: "completed",
- Output: []ResponsesOutput{
- {
- Type: "function_call",
- CallID: "call_other",
- Name: "Search",
- Arguments: `{"query":""}`,
- },
- },
- }
- anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
- require.Len(t, anth.Content, 1)
- assert.JSONEq(t, `{"query":""}`, string(anth.Content[0].Input))
- }
- func TestResponsesToAnthropic_Reasoning(t *testing.T) {
- resp := &ResponsesResponse{
- ID: "resp_789",
- Model: "gpt-5.2",
- Status: "completed",
- Output: []ResponsesOutput{
- {
- Type: "reasoning",
- EncryptedContent: "enc-rs-roundtrip",
- Summary: []ResponsesSummary{
- {Type: "summary_text", Text: "Thinking about the answer..."},
- },
- },
- {
- Type: "message",
- Content: []ResponsesContentPart{
- {Type: "output_text", Text: "42"},
- },
- },
- },
- }
- anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
- require.Len(t, anth.Content, 2)
- assert.Equal(t, "thinking", anth.Content[0].Type)
- assert.Equal(t, "Thinking about the answer...", anth.Content[0].Thinking)
- assert.Equal(t, "enc-rs-roundtrip", anth.Content[0].Signature)
- assert.Equal(t, "text", anth.Content[1].Type)
- assert.Equal(t, "42", anth.Content[1].Text)
- }
- func TestResponsesToAnthropic_StreamEmitsThinkingSignature(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- var all []AnthropicStreamEvent
- appendAll := func(events []AnthropicStreamEvent) {
- all = append(all, events...)
- }
- appendAll(ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_item.added",
- OutputIndex: 0,
- Item: &ResponsesOutput{Type: "reasoning", ID: "rs_1"},
- }, state))
- appendAll(ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.reasoning_summary_text.delta",
- OutputIndex: 0,
- Delta: "thinking...",
- SummaryIndex: 0,
- }, state))
- // summary.done must not close the thinking block before encrypted_content arrives
- appendAll(ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.reasoning_summary_text.done",
- OutputIndex: 0,
- SummaryIndex: 0,
- }, state))
- appendAll(ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_item.done",
- OutputIndex: 0,
- Item: &ResponsesOutput{
- Type: "reasoning",
- ID: "rs_1",
- EncryptedContent: "enc-stream-1",
- Status: "completed",
- },
- }, state))
- var sawSignature bool
- for _, ev := range all {
- if ev.Type == "content_block_delta" && ev.Delta != nil && ev.Delta.Type == "signature_delta" {
- assert.Equal(t, "enc-stream-1", ev.Delta.Signature)
- sawSignature = true
- }
- }
- require.True(t, sawSignature, "expected signature_delta with encrypted_content")
- }
- func TestResponsesToAnthropic_Incomplete(t *testing.T) {
- resp := &ResponsesResponse{
- ID: "resp_inc",
- Model: "gpt-5.2",
- Status: "incomplete",
- IncompleteDetails: &ResponsesIncompleteDetails{
- Reason: "max_output_tokens",
- },
- Output: []ResponsesOutput{
- {
- Type: "message",
- Content: []ResponsesContentPart{{Type: "output_text", Text: "Partial..."}},
- },
- },
- }
- anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
- assert.Equal(t, "max_tokens", AnthropicStopReasonString(anth.StopReason))
- }
- func TestResponsesToAnthropic_EmptyOutput(t *testing.T) {
- resp := &ResponsesResponse{
- ID: "resp_empty",
- Model: "gpt-5.2",
- Status: "completed",
- Output: []ResponsesOutput{},
- }
- anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
- require.Len(t, anth.Content, 1)
- assert.Equal(t, "text", anth.Content[0].Type)
- assert.Equal(t, "", anth.Content[0].Text)
- }
- // ---------------------------------------------------------------------------
- // Streaming: ResponsesEventToAnthropicEvents tests
- // ---------------------------------------------------------------------------
- func TestStreamingTextOnly(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- // 1. response.created
- events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.created",
- Response: &ResponsesResponse{
- ID: "resp_1",
- Model: "gpt-5.2",
- },
- }, state)
- require.Len(t, events, 1)
- assert.Equal(t, "message_start", events[0].Type)
- // 2. output_item.added (message)
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_item.added",
- OutputIndex: 0,
- Item: &ResponsesOutput{Type: "message"},
- }, state)
- assert.Len(t, events, 0) // message item doesn't emit events
- // 3. text delta
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_text.delta",
- Delta: "Hello",
- }, state)
- require.Len(t, events, 2) // content_block_start + content_block_delta
- assert.Equal(t, "content_block_start", events[0].Type)
- assert.Equal(t, "text", events[0].ContentBlock.Type)
- assert.Equal(t, "content_block_delta", events[1].Type)
- assert.Equal(t, "text_delta", events[1].Delta.Type)
- assert.Equal(t, "Hello", events[1].Delta.Text)
- // 4. more text
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_text.delta",
- Delta: " world",
- }, state)
- require.Len(t, events, 1) // only delta, no new block start
- assert.Equal(t, "content_block_delta", events[0].Type)
- // 5. text done
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_text.done",
- }, state)
- require.Len(t, events, 1)
- assert.Equal(t, "content_block_stop", events[0].Type)
- // 6. completed
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.completed",
- Response: &ResponsesResponse{
- Status: "completed",
- Usage: &ResponsesUsage{InputTokens: 10, OutputTokens: 5},
- },
- }, state)
- require.Len(t, events, 2) // message_delta + message_stop
- assert.Equal(t, "message_delta", events[0].Type)
- assert.Equal(t, "end_turn", events[0].Delta.StopReason)
- assert.Equal(t, 10, events[0].Usage.InputTokens)
- assert.Equal(t, 5, events[0].Usage.OutputTokens)
- assert.Equal(t, "message_stop", events[1].Type)
- }
- func TestResponsesEventToAnthropicEvents_ResponseDone(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- state.Model = "gpt-4o"
- events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.done",
- Response: &ResponsesResponse{
- Status: "completed",
- Usage: &ResponsesUsage{InputTokens: 12, OutputTokens: 4},
- },
- }, state)
- require.Len(t, events, 2)
- assert.Equal(t, "message_delta", events[0].Type)
- assert.Equal(t, "end_turn", events[0].Delta.StopReason)
- assert.Equal(t, 12, events[0].Usage.InputTokens)
- assert.Equal(t, 4, events[0].Usage.OutputTokens)
- assert.Equal(t, "message_stop", events[1].Type)
- assert.Nil(t, FinalizeResponsesAnthropicStream(state))
- }
- func TestResponsesEventToAnthropicEvents_TopLevelTerminalUsage(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- state.Model = "gpt-4o"
- events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.completed",
- Response: &ResponsesResponse{
- Status: "completed",
- },
- Usage: &ResponsesUsage{
- InputTokens: 20,
- OutputTokens: 6,
- InputTokensDetails: &ResponsesInputTokensDetails{
- CachedTokens: 5,
- },
- },
- }, state)
- require.Len(t, events, 2)
- assert.Equal(t, "message_delta", events[0].Type)
- require.NotNil(t, events[0].Usage)
- assert.Equal(t, 15, events[0].Usage.InputTokens)
- assert.Equal(t, 5, events[0].Usage.CacheReadInputTokens)
- assert.Equal(t, 6, events[0].Usage.OutputTokens)
- assert.Equal(t, "message_stop", events[1].Type)
- }
- func TestResponsesEventToAnthropicEvents_ResponseDoneIncomplete(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- state.Model = "gpt-4o"
- events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.done",
- Response: &ResponsesResponse{
- Status: "incomplete",
- IncompleteDetails: &ResponsesIncompleteDetails{Reason: "max_output_tokens"},
- Usage: &ResponsesUsage{InputTokens: 12, OutputTokens: 4},
- },
- }, state)
- require.Len(t, events, 2)
- assert.Equal(t, "message_delta", events[0].Type)
- assert.Equal(t, "max_tokens", events[0].Delta.StopReason)
- assert.Equal(t, "message_stop", events[1].Type)
- assert.Nil(t, FinalizeResponsesAnthropicStream(state))
- }
- func TestStreamingCachedTokensUseAnthropicInputSemantics(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.created",
- Response: &ResponsesResponse{ID: "resp_cached_stream", Model: "gpt-5.2"},
- }, state)
- events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.completed",
- Response: &ResponsesResponse{
- Status: "completed",
- Usage: &ResponsesUsage{
- InputTokens: 54006,
- OutputTokens: 123,
- TotalTokens: 54129,
- InputTokensDetails: &ResponsesInputTokensDetails{
- CachedTokens: 50688,
- },
- },
- },
- }, state)
- require.Len(t, events, 2)
- assert.Equal(t, "message_delta", events[0].Type)
- assert.Equal(t, 3318, events[0].Usage.InputTokens)
- assert.Equal(t, 50688, events[0].Usage.CacheReadInputTokens)
- assert.Equal(t, 123, events[0].Usage.OutputTokens)
- assert.Equal(t, "message_stop", events[1].Type)
- }
- func TestStreamingToolCall(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- // 1. response.created
- ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.created",
- Response: &ResponsesResponse{ID: "resp_2", Model: "gpt-5.2"},
- }, state)
- // 2. function_call added
- events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_item.added",
- OutputIndex: 0,
- Item: &ResponsesOutput{Type: "function_call", CallID: "call_1", Name: "get_weather"},
- }, state)
- require.Len(t, events, 1)
- assert.Equal(t, "content_block_start", events[0].Type)
- assert.Equal(t, "tool_use", events[0].ContentBlock.Type)
- assert.Equal(t, "call_1", events[0].ContentBlock.ID)
- // 3. arguments delta
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.function_call_arguments.delta",
- OutputIndex: 0,
- Delta: `{"city":`,
- }, state)
- require.Len(t, events, 1)
- assert.Equal(t, "content_block_delta", events[0].Type)
- assert.Equal(t, "input_json_delta", events[0].Delta.Type)
- assert.Equal(t, `{"city":`, events[0].Delta.PartialJSON)
- // 4. arguments done
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.function_call_arguments.done",
- }, state)
- require.Len(t, events, 1)
- assert.Equal(t, "content_block_stop", events[0].Type)
- // 5. completed with tool_calls
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.completed",
- Response: &ResponsesResponse{
- Status: "completed",
- Usage: &ResponsesUsage{InputTokens: 20, OutputTokens: 10},
- },
- }, state)
- require.Len(t, events, 2)
- assert.Equal(t, "tool_use", events[0].Delta.StopReason)
- }
- func TestStreamingToolCallStopReasonSurvivesLaterText(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.created",
- Response: &ResponsesResponse{ID: "resp_tool_then_text", Model: "gpt-5.5"},
- }, state)
- events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_item.added",
- OutputIndex: 0,
- Item: &ResponsesOutput{Type: "function_call", CallID: "call_todo", Name: "TodoWrite"},
- }, state)
- require.Len(t, events, 1)
- assert.Equal(t, "content_block_start", events[0].Type)
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.function_call_arguments.done",
- OutputIndex: 0,
- Arguments: `{"todos":[{"content":"review changes","status":"in_progress","activeForm":"reviewing changes"}]}`,
- }, state)
- require.Len(t, events, 2)
- assert.Equal(t, "content_block_delta", events[0].Type)
- assert.Equal(t, "content_block_stop", events[1].Type)
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_text.delta",
- OutputIndex: 1,
- Delta: "I will continue after the task list updates.",
- }, state)
- require.Len(t, events, 2)
- assert.Equal(t, "content_block_start", events[0].Type)
- assert.Equal(t, "content_block_delta", events[1].Type)
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.completed",
- Response: &ResponsesResponse{
- Status: "completed",
- Usage: &ResponsesUsage{InputTokens: 20, OutputTokens: 10},
- },
- }, state)
- require.Len(t, events, 3)
- assert.Equal(t, "content_block_stop", events[0].Type)
- assert.Equal(t, "tool_use", events[1].Delta.StopReason)
- assert.Equal(t, "message_stop", events[2].Type)
- }
- func TestStreamingToolCallDoneWithoutDeltaEmitsArguments(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.created",
- Response: &ResponsesResponse{ID: "resp_bash", Model: "gpt-5.5"},
- }, state)
- events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_item.added",
- OutputIndex: 0,
- Item: &ResponsesOutput{Type: "function_call", CallID: "call_bash", Name: "Bash"},
- }, state)
- require.Len(t, events, 1)
- assert.Equal(t, "content_block_start", events[0].Type)
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.function_call_arguments.done",
- OutputIndex: 0,
- Arguments: `{"command":"git -C \"/mnt/d/nodejs/other/edmt\" status --short --ignored"}`,
- }, state)
- require.Len(t, events, 2)
- assert.Equal(t, "content_block_delta", events[0].Type)
- assert.Equal(t, "input_json_delta", events[0].Delta.Type)
- assert.JSONEq(t, `{"command":"git -C \"/mnt/d/nodejs/other/edmt\" status --short --ignored"}`, events[0].Delta.PartialJSON)
- assert.Equal(t, "content_block_stop", events[1].Type)
- }
- func TestStreamingReadToolStreamsDeltas(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.created",
- Response: &ResponsesResponse{ID: "resp_read_stream", Model: "gpt-5.5"},
- }, state)
- events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_item.added",
- OutputIndex: 0,
- Item: &ResponsesOutput{Type: "function_call", CallID: "call_read", Name: "Read"},
- }, state)
- require.Len(t, events, 1)
- assert.Equal(t, "content_block_start", events[0].Type)
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.function_call_arguments.delta",
- OutputIndex: 0,
- Delta: `{"file_path":"/tmp/demo.py","limit":2000,"offset":0,"pages":""}`,
- }, state)
- require.Len(t, events, 1, "Read tool deltas must be streamed like any other tool")
- assert.Equal(t, "content_block_delta", events[0].Type)
- assert.Equal(t, "input_json_delta", events[0].Delta.Type)
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.function_call_arguments.done",
- OutputIndex: 0,
- Arguments: `{"file_path":"/tmp/demo.py","limit":2000,"offset":0,"pages":""}`,
- }, state)
- require.Len(t, events, 1, "after streaming deltas, .done should just close the block")
- assert.Equal(t, "content_block_stop", events[0].Type)
- }
- func TestStreamingReasoning(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.created",
- Response: &ResponsesResponse{ID: "resp_3", Model: "gpt-5.2"},
- }, state)
- // reasoning item added
- events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_item.added",
- OutputIndex: 0,
- Item: &ResponsesOutput{Type: "reasoning"},
- }, state)
- require.Len(t, events, 1)
- assert.Equal(t, "content_block_start", events[0].Type)
- assert.Equal(t, "thinking", events[0].ContentBlock.Type)
- sse, err := ResponsesAnthropicEventToSSE(events[0])
- require.NoError(t, err)
- assert.Contains(t, sse, `"content_block":{"thinking":"","type":"thinking"}`)
- // reasoning text delta
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.reasoning_summary_text.delta",
- OutputIndex: 0,
- Delta: "Let me think...",
- }, state)
- require.Len(t, events, 1)
- assert.Equal(t, "content_block_delta", events[0].Type)
- assert.Equal(t, "thinking_delta", events[0].Delta.Type)
- assert.Equal(t, "Let me think...", events[0].Delta.Thinking)
- // summary.done keeps thinking open until output_item.done (for signature)
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.reasoning_summary_text.done",
- }, state)
- require.Len(t, events, 0)
- events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_item.done",
- OutputIndex: 0,
- Item: &ResponsesOutput{
- Type: "reasoning",
- EncryptedContent: "enc-rs-stream",
- Status: "completed",
- },
- }, state)
- require.Len(t, events, 2)
- assert.Equal(t, "content_block_delta", events[0].Type)
- assert.Equal(t, "signature_delta", events[0].Delta.Type)
- assert.Equal(t, "enc-rs-stream", events[0].Delta.Signature)
- assert.Equal(t, "content_block_stop", events[1].Type)
- }
- func TestStreamingIncomplete(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.created",
- Response: &ResponsesResponse{ID: "resp_4", Model: "gpt-5.2"},
- }, state)
- ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_text.delta",
- Delta: "Partial output...",
- }, state)
- events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.incomplete",
- Response: &ResponsesResponse{
- Status: "incomplete",
- IncompleteDetails: &ResponsesIncompleteDetails{Reason: "max_output_tokens"},
- Usage: &ResponsesUsage{InputTokens: 100, OutputTokens: 4096},
- },
- }, state)
- // Should close the text block + message_delta + message_stop
- require.Len(t, events, 3)
- assert.Equal(t, "content_block_stop", events[0].Type)
- assert.Equal(t, "message_delta", events[1].Type)
- assert.Equal(t, "max_tokens", events[1].Delta.StopReason)
- assert.Equal(t, "message_stop", events[2].Type)
- }
- func TestFinalizeStream_NeverStarted(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- events := FinalizeResponsesAnthropicStream(state)
- assert.Nil(t, events)
- }
- func TestFinalizeStream_AlreadyCompleted(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- state.MessageStartSent = true
- state.MessageStopSent = true
- events := FinalizeResponsesAnthropicStream(state)
- assert.Nil(t, events)
- }
- func TestFinalizeStream_AbnormalTermination(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- // Simulate a stream that started but never completed
- ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.created",
- Response: &ResponsesResponse{ID: "resp_5", Model: "gpt-5.2"},
- }, state)
- ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_text.delta",
- Delta: "Interrupted...",
- }, state)
- // Stream ends without response.completed
- events := FinalizeResponsesAnthropicStream(state)
- require.Len(t, events, 3) // content_block_stop + message_delta + message_stop
- assert.Equal(t, "content_block_stop", events[0].Type)
- assert.Equal(t, "message_delta", events[1].Type)
- assert.Equal(t, "end_turn", events[1].Delta.StopReason)
- assert.Equal(t, "message_stop", events[2].Type)
- }
- func TestFinalizeStream_ToolCallAbnormalTerminationKeepsToolUseStopReason(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.created",
- Response: &ResponsesResponse{ID: "resp_tool_interrupted", Model: "gpt-5.5"},
- }, state)
- ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_item.added",
- OutputIndex: 0,
- Item: &ResponsesOutput{Type: "function_call", CallID: "call_todo", Name: "TodoWrite"},
- }, state)
- events := FinalizeResponsesAnthropicStream(state)
- require.Len(t, events, 3)
- assert.Equal(t, "content_block_stop", events[0].Type)
- assert.Equal(t, "message_delta", events[1].Type)
- assert.Equal(t, "tool_use", events[1].Delta.StopReason)
- assert.Equal(t, "message_stop", events[2].Type)
- }
- func TestStreamingEmptyResponse(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.created",
- Response: &ResponsesResponse{ID: "resp_6", Model: "gpt-5.2"},
- }, state)
- events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.completed",
- Response: &ResponsesResponse{
- Status: "completed",
- Usage: &ResponsesUsage{InputTokens: 5, OutputTokens: 0},
- },
- }, state)
- require.Len(t, events, 2) // message_delta + message_stop
- assert.Equal(t, "message_delta", events[0].Type)
- assert.Equal(t, "end_turn", events[0].Delta.StopReason)
- }
- func TestResponsesAnthropicEventToSSE(t *testing.T) {
- evt := AnthropicStreamEvent{
- Type: "message_start",
- Message: &AnthropicResponse{
- ID: "resp_1",
- Type: "message",
- Role: "assistant",
- },
- }
- sse, err := ResponsesAnthropicEventToSSE(evt)
- require.NoError(t, err)
- assert.Contains(t, sse, "event: message_start\n")
- assert.Contains(t, sse, "data: ")
- assert.Contains(t, sse, `"resp_1"`)
- }
- // ---------------------------------------------------------------------------
- // response.failed tests
- // ---------------------------------------------------------------------------
- func TestStreamingFailed(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- // 1. response.created
- ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.created",
- Response: &ResponsesResponse{ID: "resp_fail_1", Model: "gpt-5.2"},
- }, state)
- // 2. Some text output before failure
- ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.output_text.delta",
- Delta: "Partial output before failure",
- }, state)
- // 3. response.failed
- events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.failed",
- Response: &ResponsesResponse{
- Status: "failed",
- Error: &ResponsesError{Code: "server_error", Message: "Internal error"},
- Usage: &ResponsesUsage{InputTokens: 50, OutputTokens: 10},
- },
- }, state)
- // Should close text block + message_delta + message_stop
- require.Len(t, events, 3)
- assert.Equal(t, "content_block_stop", events[0].Type)
- assert.Equal(t, "message_delta", events[1].Type)
- assert.Equal(t, "end_turn", events[1].Delta.StopReason)
- assert.Equal(t, 50, events[1].Usage.InputTokens)
- assert.Equal(t, 10, events[1].Usage.OutputTokens)
- assert.Equal(t, "message_stop", events[2].Type)
- }
- func TestStreamingFailedNoOutput(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- // 1. response.created
- ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.created",
- Response: &ResponsesResponse{ID: "resp_fail_2", Model: "gpt-5.2"},
- }, state)
- // 2. response.failed with no prior output
- events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.failed",
- Response: &ResponsesResponse{
- Status: "failed",
- Error: &ResponsesError{Code: "rate_limit_error", Message: "Too many requests"},
- Usage: &ResponsesUsage{InputTokens: 20, OutputTokens: 0},
- },
- }, state)
- // Should emit message_delta + message_stop (no block to close)
- require.Len(t, events, 2)
- assert.Equal(t, "message_delta", events[0].Type)
- assert.Equal(t, "end_turn", events[0].Delta.StopReason)
- assert.Equal(t, "message_stop", events[1].Type)
- }
- func TestResponsesToAnthropic_Failed(t *testing.T) {
- resp := &ResponsesResponse{
- ID: "resp_fail_3",
- Model: "gpt-5.2",
- Status: "failed",
- Error: &ResponsesError{Code: "server_error", Message: "Something went wrong"},
- Output: []ResponsesOutput{},
- Usage: &ResponsesUsage{InputTokens: 30, OutputTokens: 0},
- }
- anth := ResponsesToAnthropic(resp, "claude-opus-4-6")
- // Failed status defaults to "end_turn" stop reason
- assert.Equal(t, "end_turn", AnthropicStopReasonString(anth.StopReason))
- // Should have at least an empty text block
- require.Len(t, anth.Content, 1)
- assert.Equal(t, "text", anth.Content[0].Type)
- }
- // ---------------------------------------------------------------------------
- // thinking → reasoning conversion tests
- // ---------------------------------------------------------------------------
- func TestAnthropicToResponses_ThinkingEnabled(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
- Thinking: &AnthropicThinking{Type: "enabled", BudgetTokens: 10000},
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- require.NotNil(t, resp.Reasoning)
- // thinking.type is ignored for effort; Codex bridge default medium applies.
- assert.Equal(t, "medium", resp.Reasoning.Effort)
- assert.Equal(t, "auto", resp.Reasoning.Summary)
- assert.Contains(t, resp.Include, "reasoning.encrypted_content")
- assert.NotContains(t, resp.Include, "reasoning.summary")
- }
- func TestAnthropicToResponses_ThinkingAdaptive(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
- Thinking: &AnthropicThinking{Type: "adaptive", BudgetTokens: 5000},
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- require.NotNil(t, resp.Reasoning)
- // thinking.type is ignored for effort; Codex bridge default medium applies.
- assert.Equal(t, "medium", resp.Reasoning.Effort)
- assert.Equal(t, "auto", resp.Reasoning.Summary)
- assert.NotContains(t, resp.Include, "reasoning.summary")
- }
- func TestAnthropicToResponses_ThinkingDisabled(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
- Thinking: &AnthropicThinking{Type: "disabled"},
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- // Default effort applies (medium) even when thinking is disabled.
- require.NotNil(t, resp.Reasoning)
- assert.Equal(t, "medium", resp.Reasoning.Effort)
- }
- func TestAnthropicToResponses_NoThinking(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- // Default effort applies (medium) when no thinking/output_config is set.
- require.NotNil(t, resp.Reasoning)
- assert.Equal(t, "medium", resp.Reasoning.Effort)
- }
- // ---------------------------------------------------------------------------
- // output_config.effort override tests
- // ---------------------------------------------------------------------------
- func TestAnthropicToResponses_OutputConfigOverridesDefault(t *testing.T) {
- // Default is medium, but output_config.effort="low" overrides. low→low after mapping.
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
- Thinking: &AnthropicThinking{Type: "enabled", BudgetTokens: 10000},
- OutputConfig: &AnthropicOutputConfig{Effort: "low"},
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- require.NotNil(t, resp.Reasoning)
- assert.Equal(t, "low", resp.Reasoning.Effort)
- assert.Equal(t, "auto", resp.Reasoning.Summary)
- }
- func TestAnthropicToResponses_OutputConfigWithoutThinking(t *testing.T) {
- // No thinking field, but output_config.effort="medium" → creates reasoning.
- // medium→medium after 1:1 mapping.
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
- OutputConfig: &AnthropicOutputConfig{Effort: "medium"},
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- require.NotNil(t, resp.Reasoning)
- assert.Equal(t, "medium", resp.Reasoning.Effort)
- assert.Equal(t, "auto", resp.Reasoning.Summary)
- }
- func TestAnthropicToResponses_OutputConfigHigh(t *testing.T) {
- // output_config.effort="high" → mapped to "high" (1:1).
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
- OutputConfig: &AnthropicOutputConfig{Effort: "high"},
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- require.NotNil(t, resp.Reasoning)
- assert.Equal(t, "high", resp.Reasoning.Effort)
- assert.Equal(t, "auto", resp.Reasoning.Summary)
- }
- func TestAnthropicToResponses_OutputConfigMax(t *testing.T) {
- // output_config.effort="max" → mapped to OpenAI's highest supported level "xhigh".
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
- OutputConfig: &AnthropicOutputConfig{Effort: "max"},
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- require.NotNil(t, resp.Reasoning)
- assert.Equal(t, "xhigh", resp.Reasoning.Effort)
- assert.Equal(t, "auto", resp.Reasoning.Summary)
- }
- func TestAnthropicToResponses_NoOutputConfig(t *testing.T) {
- // No output_config → default medium regardless of thinking.type.
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
- Thinking: &AnthropicThinking{Type: "enabled", BudgetTokens: 10000},
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- require.NotNil(t, resp.Reasoning)
- assert.Equal(t, "medium", resp.Reasoning.Effort)
- }
- func TestAnthropicToResponses_OutputConfigWithoutEffort(t *testing.T) {
- // output_config present but effort empty (e.g. only format set) → default medium.
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
- OutputConfig: &AnthropicOutputConfig{},
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- require.NotNil(t, resp.Reasoning)
- assert.Equal(t, "medium", resp.Reasoning.Effort)
- }
- // ---------------------------------------------------------------------------
- // tool_choice conversion tests
- // ---------------------------------------------------------------------------
- func TestAnthropicToResponses_ToolChoiceAuto(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
- ToolChoice: json.RawMessage(`{"type":"auto"}`),
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- var tc string
- require.NoError(t, json.Unmarshal(resp.ToolChoice, &tc))
- assert.Equal(t, "auto", tc)
- }
- func TestAnthropicToResponses_ToolChoiceAny(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
- ToolChoice: json.RawMessage(`{"type":"any"}`),
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- var tc string
- require.NoError(t, json.Unmarshal(resp.ToolChoice, &tc))
- assert.Equal(t, "required", tc)
- }
- func TestAnthropicToResponses_ToolChoiceSpecific(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
- ToolChoice: json.RawMessage(`{"type":"tool","name":"get_weather"}`),
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- var tc map[string]any
- require.NoError(t, json.Unmarshal(resp.ToolChoice, &tc))
- assert.Equal(t, "function", tc["type"])
- assert.Equal(t, "get_weather", tc["name"])
- assert.NotContains(t, tc, "function")
- }
- func TestResponsesToAnthropicRequest_ToolChoiceFunctionName(t *testing.T) {
- req := &ResponsesRequest{
- Model: "gpt-5.2",
- Input: json.RawMessage(`[{"role":"user","content":"Hello"}]`),
- ToolChoice: json.RawMessage(`{"type":"function","name":"get_weather"}`),
- }
- resp, err := ResponsesToAnthropicRequest(req)
- require.NoError(t, err)
- var tc map[string]string
- require.NoError(t, json.Unmarshal(resp.ToolChoice, &tc))
- assert.Equal(t, "tool", tc["type"])
- assert.Equal(t, "get_weather", tc["name"])
- }
- func TestResponsesToAnthropicRequest_ToolChoiceLegacyFunctionName(t *testing.T) {
- req := &ResponsesRequest{
- Model: "gpt-5.2",
- Input: json.RawMessage(`[{"role":"user","content":"Hello"}]`),
- ToolChoice: json.RawMessage(`{"type":"function","function":{"name":"get_weather"}}`),
- }
- resp, err := ResponsesToAnthropicRequest(req)
- require.NoError(t, err)
- var tc map[string]string
- require.NoError(t, json.Unmarshal(resp.ToolChoice, &tc))
- assert.Equal(t, "tool", tc["type"])
- assert.Equal(t, "get_weather", tc["name"])
- }
- // ---------------------------------------------------------------------------
- // Image content block conversion tests
- // ---------------------------------------------------------------------------
- func TestAnthropicToResponses_UserImageBlock(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{
- {Role: "user", Content: json.RawMessage(`[
- {"type":"text","text":"What is in this image?"},
- {"type":"image","source":{"type":"base64","media_type":"image/png","data":"iVBOR"}}
- ]`)},
- },
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- var items []ResponsesInputItem
- require.NoError(t, json.Unmarshal(resp.Input, &items))
- require.Len(t, items, 1)
- assert.Equal(t, "user", items[0].Role)
- var parts []ResponsesContentPart
- require.NoError(t, json.Unmarshal(items[0].Content, &parts))
- require.Len(t, parts, 2)
- assert.Equal(t, "input_text", parts[0].Type)
- assert.Equal(t, "What is in this image?", parts[0].Text)
- assert.Equal(t, "input_image", parts[1].Type)
- assert.Equal(t, "data:image/png;base64,iVBOR", parts[1].ImageURL)
- }
- func TestAnthropicToResponses_ImageOnlyUserMessage(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{
- {Role: "user", Content: json.RawMessage(`[
- {"type":"image","source":{"type":"base64","media_type":"image/jpeg","data":"/9j/4AAQ"}}
- ]`)},
- },
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- var items []ResponsesInputItem
- require.NoError(t, json.Unmarshal(resp.Input, &items))
- require.Len(t, items, 1)
- var parts []ResponsesContentPart
- require.NoError(t, json.Unmarshal(items[0].Content, &parts))
- require.Len(t, parts, 1)
- assert.Equal(t, "input_image", parts[0].Type)
- assert.Equal(t, "data:image/jpeg;base64,/9j/4AAQ", parts[0].ImageURL)
- }
- func TestAnthropicToResponses_ToolResultWithImage(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{
- {Role: "user", Content: json.RawMessage(`"Read the screenshot"`)},
- {Role: "assistant", Content: json.RawMessage(`[{"type":"tool_use","id":"toolu_1","name":"Read","input":{"file_path":"/tmp/screen.png"}}]`)},
- {Role: "user", Content: json.RawMessage(`[
- {"type":"tool_result","tool_use_id":"toolu_1","content":[
- {"type":"image","source":{"type":"base64","media_type":"image/png","data":"iVBOR"}}
- ]}
- ]`)},
- },
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- var items []ResponsesInputItem
- require.NoError(t, json.Unmarshal(resp.Input, &items))
- // user + function_call + function_call_output + user(image) = 4
- require.Len(t, items, 4)
- // function_call_output should have text-only output (no image).
- assert.Equal(t, "function_call_output", items[2].Type)
- assert.Equal(t, "toolu_1", items[2].CallID)
- assert.Equal(t, "(empty)", items[2].Output)
- // Image should be in a separate user message.
- assert.Equal(t, "user", items[3].Role)
- var parts []ResponsesContentPart
- require.NoError(t, json.Unmarshal(items[3].Content, &parts))
- require.Len(t, parts, 1)
- assert.Equal(t, "input_image", parts[0].Type)
- assert.Equal(t, "data:image/png;base64,iVBOR", parts[0].ImageURL)
- }
- func TestAnthropicToResponses_ToolResultMixed(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{
- {Role: "user", Content: json.RawMessage(`"Describe the file"`)},
- {Role: "assistant", Content: json.RawMessage(`[{"type":"tool_use","id":"toolu_2","name":"Read","input":{"file_path":"/tmp/photo.png"}}]`)},
- {Role: "user", Content: json.RawMessage(`[
- {"type":"tool_result","tool_use_id":"toolu_2","content":[
- {"type":"text","text":"File metadata: 800x600 PNG"},
- {"type":"image","source":{"type":"base64","media_type":"image/png","data":"AAAA"}}
- ]}
- ]`)},
- },
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- var items []ResponsesInputItem
- require.NoError(t, json.Unmarshal(resp.Input, &items))
- // user + function_call + function_call_output + user(image) = 4
- require.Len(t, items, 4)
- // function_call_output should have text-only output.
- assert.Equal(t, "function_call_output", items[2].Type)
- assert.Equal(t, "File metadata: 800x600 PNG", items[2].Output)
- // Image should be in a separate user message.
- assert.Equal(t, "user", items[3].Role)
- var parts []ResponsesContentPart
- require.NoError(t, json.Unmarshal(items[3].Content, &parts))
- require.Len(t, parts, 1)
- assert.Equal(t, "input_image", parts[0].Type)
- assert.Equal(t, "data:image/png;base64,AAAA", parts[0].ImageURL)
- }
- func TestAnthropicToResponses_TextOnlyToolResultBackwardCompat(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{
- {Role: "user", Content: json.RawMessage(`"Check weather"`)},
- {Role: "assistant", Content: json.RawMessage(`[{"type":"tool_use","id":"call_1","name":"get_weather","input":{"city":"NYC"}}]`)},
- {Role: "user", Content: json.RawMessage(`[
- {"type":"tool_result","tool_use_id":"call_1","content":[
- {"type":"text","text":"Sunny, 72°F"}
- ]}
- ]`)},
- },
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- var items []ResponsesInputItem
- require.NoError(t, json.Unmarshal(resp.Input, &items))
- // user + function_call + function_call_output = 3
- require.Len(t, items, 3)
- // Text-only tool_result should produce a plain string.
- assert.Equal(t, "Sunny, 72°F", items[2].Output)
- }
- func TestAnthropicToResponses_ImageEmptyMediaType(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{
- {Role: "user", Content: json.RawMessage(`[
- {"type":"image","source":{"type":"base64","media_type":"","data":"iVBOR"}}
- ]`)},
- },
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- var items []ResponsesInputItem
- require.NoError(t, json.Unmarshal(resp.Input, &items))
- require.Len(t, items, 1)
- var parts []ResponsesContentPart
- require.NoError(t, json.Unmarshal(items[0].Content, &parts))
- require.Len(t, parts, 1)
- assert.Equal(t, "input_image", parts[0].Type)
- // Should default to image/png when media_type is empty.
- assert.Equal(t, "data:image/png;base64,iVBOR", parts[0].ImageURL)
- }
- // ---------------------------------------------------------------------------
- // normalizeToolParameters tests
- // ---------------------------------------------------------------------------
- func TestNormalizeToolParameters(t *testing.T) {
- tests := []struct {
- name string
- input json.RawMessage
- expected string
- }{
- {
- name: "nil input",
- input: nil,
- expected: `{"type":"object","properties":{}}`,
- },
- {
- name: "empty input",
- input: json.RawMessage(``),
- expected: `{"type":"object","properties":{}}`,
- },
- {
- name: "null input",
- input: json.RawMessage(`null`),
- expected: `{"type":"object","properties":{}}`,
- },
- {
- name: "object without properties",
- input: json.RawMessage(`{"type":"object"}`),
- expected: `{"type":"object","properties":{}}`,
- },
- {
- name: "object with properties",
- input: json.RawMessage(`{"type":"object","properties":{"city":{"type":"string"}}}`),
- expected: `{"type":"object","properties":{"city":{"type":"string"}}}`,
- },
- {
- name: "non-object type",
- input: json.RawMessage(`{"type":"string"}`),
- expected: `{"type":"string"}`,
- },
- {
- name: "object with additional fields preserved",
- input: json.RawMessage(`{"type":"object","required":["name"]}`),
- expected: `{"type":"object","required":["name"],"properties":{}}`,
- },
- {
- name: "invalid JSON passthrough",
- input: json.RawMessage(`not json`),
- expected: `not json`,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result := normalizeToolParameters(tt.input)
- if tt.name == "invalid JSON passthrough" {
- assert.Equal(t, tt.expected, string(result))
- } else {
- assert.JSONEq(t, tt.expected, string(result))
- }
- })
- }
- }
- func TestAnthropicToResponses_ToolWithoutProperties(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{
- {Role: "user", Content: json.RawMessage(`"Hello"`)},
- },
- Tools: []AnthropicTool{
- {Name: "mcp__pencil__get_style_guide_tags", Description: "Get style tags", InputSchema: json.RawMessage(`{"type":"object"}`)},
- },
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- require.Len(t, resp.Tools, 1)
- assert.Equal(t, "function", resp.Tools[0].Type)
- assert.Equal(t, "mcp__pencil__get_style_guide_tags", resp.Tools[0].Name)
- // Parameters must have "properties" field after normalization.
- var params map[string]json.RawMessage
- require.NoError(t, json.Unmarshal(resp.Tools[0].Parameters, ¶ms))
- assert.Contains(t, params, "properties")
- }
- func TestAnthropicToResponses_ToolWithNilSchema(t *testing.T) {
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{
- {Role: "user", Content: json.RawMessage(`"Hello"`)},
- },
- Tools: []AnthropicTool{
- {Name: "simple_tool", Description: "A tool"},
- },
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- require.Len(t, resp.Tools, 1)
- var params map[string]json.RawMessage
- require.NoError(t, json.Unmarshal(resp.Tools[0].Parameters, ¶ms))
- assert.JSONEq(t, `"object"`, string(params["type"]))
- assert.JSONEq(t, `{}`, string(params["properties"]))
- }
- // ---------------------------------------------------------------------------
- // isReasoningModel / temperature-stripping tests
- // ---------------------------------------------------------------------------
- func TestAnthropicToResponses_TemperatureStrippedForReasoningModel(t *testing.T) {
- temp := 0.7
- req := &AnthropicRequest{
- Model: "gpt-5.2",
- MaxTokens: 1024,
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
- Temperature: &temp,
- TopP: &temp,
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- assert.Nil(t, resp.Temperature, "reasoning model: temperature must be stripped")
- assert.Nil(t, resp.TopP, "reasoning model: top_p must be stripped")
- // Verify the fields are absent from the serialised JSON.
- b, err := json.Marshal(resp)
- require.NoError(t, err)
- assert.NotContains(t, string(b), `"temperature"`)
- assert.NotContains(t, string(b), `"top_p"`)
- }
- func TestAnthropicToResponses_TemperatureStrippedForAllGpt5Variants(t *testing.T) {
- temp := 1.0
- models := []string{"gpt-5.2", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex", "gpt-5.5"}
- for _, model := range models {
- t.Run(model, func(t *testing.T) {
- req := &AnthropicRequest{
- Model: model,
- MaxTokens: 1024,
- Messages: []AnthropicMessage{{Role: "user", Content: json.RawMessage(`"Hello"`)}},
- Temperature: &temp,
- TopP: &temp,
- }
- resp, err := AnthropicToResponses(req)
- require.NoError(t, err)
- assert.Nil(t, resp.Temperature, "model %s: temperature must be stripped", model)
- assert.Nil(t, resp.TopP, "model %s: top_p must be stripped", model)
- })
- }
- }
- // ---------------------------------------------------------------------------
- // AnthropicToResponsesResponse: Anthropic input_tokens excludes cached tokens
- // while OpenAI Responses input_tokens is the total including cached tokens.
- // ---------------------------------------------------------------------------
- func TestAnthropicToResponsesResponse_CacheTokensUseOpenAIInputSemantics(t *testing.T) {
- resp := &AnthropicResponse{
- ID: "msg_cache",
- Model: "claude-sonnet-4-5-20250929",
- Content: []AnthropicContentBlock{
- {Type: "text", Text: "ok"},
- },
- StopReason: AnthropicStopReasonPtr("end_turn"),
- Usage: AnthropicUsage{
- InputTokens: 3318,
- OutputTokens: 123,
- CacheReadInputTokens: 50688,
- CacheCreationInputTokens: 200,
- },
- }
- out := AnthropicToResponsesResponse(resp)
- require.NotNil(t, out.Usage)
- // 3318 (uncached) + 50688 (read) + 200 (creation) = 54206
- assert.Equal(t, 54206, out.Usage.InputTokens)
- assert.Equal(t, 123, out.Usage.OutputTokens)
- assert.Equal(t, 54329, out.Usage.TotalTokens)
- require.NotNil(t, out.Usage.InputTokensDetails)
- assert.Equal(t, 50688, out.Usage.InputTokensDetails.CachedTokens)
- }
- func TestAnthropicToResponsesResponse_NoCacheTokens(t *testing.T) {
- resp := &AnthropicResponse{
- ID: "msg_nocache",
- Model: "claude-sonnet-4-5-20250929",
- Content: []AnthropicContentBlock{
- {Type: "text", Text: "ok"},
- },
- StopReason: AnthropicStopReasonPtr("end_turn"),
- Usage: AnthropicUsage{
- InputTokens: 100,
- OutputTokens: 50,
- },
- }
- out := AnthropicToResponsesResponse(resp)
- require.NotNil(t, out.Usage)
- assert.Equal(t, 100, out.Usage.InputTokens)
- assert.Equal(t, 50, out.Usage.OutputTokens)
- assert.Equal(t, 150, out.Usage.TotalTokens)
- assert.Nil(t, out.Usage.InputTokensDetails)
- }
- func TestAnthropicEventToResponses_CacheTokensRoundTripFromMessageStart(t *testing.T) {
- state := NewAnthropicEventToResponsesState()
- // message_start carries cache fields on the initial Usage object.
- AnthropicEventToResponsesEvents(&AnthropicStreamEvent{
- Type: "message_start",
- Message: &AnthropicResponse{
- ID: "msg_stream_cache",
- Model: "claude-sonnet-4-5-20250929",
- Usage: AnthropicUsage{
- InputTokens: 12,
- CacheReadInputTokens: 9,
- CacheCreationInputTokens: 3,
- },
- },
- }, state)
- AnthropicEventToResponsesEvents(&AnthropicStreamEvent{
- Type: "message_delta",
- Usage: &AnthropicUsage{
- OutputTokens: 7,
- },
- }, state)
- events := AnthropicEventToResponsesEvents(&AnthropicStreamEvent{Type: "message_stop"}, state)
- // The terminal response.completed event must include OpenAI-semantic usage.
- var completed *ResponsesStreamEvent
- for i := range events {
- if events[i].Type == "response.completed" {
- completed = &events[i]
- }
- }
- require.NotNil(t, completed, "response.completed event must be emitted")
- require.NotNil(t, completed.Response)
- require.NotNil(t, completed.Response.Usage)
- // 12 (uncached) + 9 (read) + 3 (creation) = 24
- assert.Equal(t, 24, completed.Response.Usage.InputTokens)
- assert.Equal(t, 7, completed.Response.Usage.OutputTokens)
- assert.Equal(t, 31, completed.Response.Usage.TotalTokens)
- require.NotNil(t, completed.Response.Usage.InputTokensDetails)
- assert.Equal(t, 9, completed.Response.Usage.InputTokensDetails.CachedTokens)
- }
- func TestAnthropicEventToResponses_CacheTokensFromMessageDelta(t *testing.T) {
- state := NewAnthropicEventToResponsesState()
- AnthropicEventToResponsesEvents(&AnthropicStreamEvent{
- Type: "message_start",
- Message: &AnthropicResponse{
- ID: "msg_delta_cache",
- Model: "claude-sonnet-4-5-20250929",
- Usage: AnthropicUsage{InputTokens: 20},
- },
- }, state)
- // Some upstreams only emit cache fields on the final message_delta.
- AnthropicEventToResponsesEvents(&AnthropicStreamEvent{
- Type: "message_delta",
- Usage: &AnthropicUsage{
- OutputTokens: 8,
- CacheReadInputTokens: 11,
- CacheCreationInputTokens: 4,
- },
- }, state)
- events := AnthropicEventToResponsesEvents(&AnthropicStreamEvent{Type: "message_stop"}, state)
- var completed *ResponsesStreamEvent
- for i := range events {
- if events[i].Type == "response.completed" {
- completed = &events[i]
- }
- }
- require.NotNil(t, completed)
- require.NotNil(t, completed.Response.Usage)
- // 20 (uncached) + 11 (read) + 4 (creation) = 35
- assert.Equal(t, 35, completed.Response.Usage.InputTokens)
- assert.Equal(t, 8, completed.Response.Usage.OutputTokens)
- require.NotNil(t, completed.Response.Usage.InputTokensDetails)
- assert.Equal(t, 11, completed.Response.Usage.InputTokensDetails.CachedTokens)
- }
- func TestMessageStartSSE_StopReasonIsJSONNull(t *testing.T) {
- state := NewResponsesEventToAnthropicState()
- state.Model = "grok-4.5"
- events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
- Type: "response.created",
- Response: &ResponsesResponse{
- ID: "resp_test",
- Model: "grok-4.5",
- },
- }, state)
- require.Len(t, events, 1)
- require.Equal(t, "message_start", events[0].Type)
- require.NotNil(t, events[0].Message)
- require.Nil(t, events[0].Message.StopReason, "message_start must use null stop_reason, not empty string")
- sse, err := ResponsesAnthropicEventToSSE(events[0])
- require.NoError(t, err)
- // Official Anthropic wire: "stop_reason":null
- require.Contains(t, sse, `"stop_reason":null`)
- require.NotContains(t, sse, `"stop_reason":""`)
- }
|