package protocol import ( "bytes" "context" "strings" "testing" ) func TestAnthropicThinkingOutput(t *testing.T) { for _, tool := range []bool{false, true} { visible := `{"type":"text","text":"OK"}` initial := `{"type":"text","text":""}` delta := `{"type":"text_delta","text":"OK"}` finish := "end_turn" if tool { visible = `{"type":"tool_use","id":"call_1","name":"weather","input":{}}` initial = visible delta = `{"type":"input_json_delta","partial_json":"{}"}` finish = "tool_use" } hidden := `{"type":"thinking","thinking":"PRIVATE","signature":"SIGNATURE"}` redacted := `{"type":"redacted_thinking","data":"CIPHER"}` body := `{"id":"m","type":"message","role":"assistant","model":"m","content":[` + hidden + "," + redacted + "," + visible + `],"stop_reason":"` + finish + `","usage":{"input_tokens":9,"output_tokens":7}}` events := []string{ `{"type":"message_start","message":{"id":"m","content":[],"usage":{"input_tokens":9,"output_tokens":0}}}`, `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"","signature":""}}`, `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"PRIVATE"}}`, `{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"SIGNATURE"}}`, `{"type":"content_block_stop","index":0}`, `{"type":"content_block_start","index":1,"content_block":` + redacted + "}", `{"type":"content_block_stop","index":1}`, `{"type":"content_block_start","index":2,"content_block":` + initial + "}", `{"type":"content_block_delta","index":2,"delta":` + delta + "}", `{"type":"content_block_stop","index":2}`, `{"type":"message_delta","delta":{"stop_reason":"` + finish + `"},"usage":{"output_tokens":7}}`, `{"type":"message_stop"}`, } src := reasoningSSE(events...) for _, target := range []Kind{Chat, Responses, Anthropic} { out, err := ConvertResponse(Anthropic, target, []byte(body)) if err != nil { t.Fatal(err) } var dst bytes.Buffer u, err := Stream(context.Background(), &dst, strings.NewReader(src), Anthropic, target, "m") if err != nil { t.Fatal(err) } if !u.Known || u.Input != 9 || u.Output != 7 { t.Fatalf("usage %+v", u) } if target == Anthropic { if string(out) != body || dst.String() != src { t.Fatal("native bytes changed") } continue } for _, result := range []string{string(out), dst.String()} { for _, secret := range []string{"PRIVATE", "SIGNATURE", "CIPHER", "thinking"} { if strings.Contains(result, secret) { t.Fatal("thinking leaked") } } want := "OK" if tool { want = "weather" } if !strings.Contains(result, want) { t.Fatal("lost visible output") } } terminal := "[DONE]" if target == Responses { terminal = "response.completed" } if !strings.Contains(dst.String(), terminal) { t.Fatal("no terminal") } } for _, bad := range []string{ strings.Replace(src, `{"type":"content_block_stop","index":0}`, `{"type":"ping"}`, 1), strings.Replace(src, `"type":"thinking_delta"`, `"type":"text_delta"`, 1), strings.Replace(src, `"index":2,"content_block"`, `"index":0,"content_block"`, 1), } { var dst bytes.Buffer if _, err := Stream(context.Background(), &dst, strings.NewReader(bad), Anthropic, Chat, "m"); err == nil { t.Fatal("accepted invalid thinking lifecycle") } } } for _, block := range []object{ {"type": "thinking", "thinking": true, "signature": ""}, {"type": "thinking", "thinking": "", "signature": "", "unknown": true}, {"type": "redacted_thinking", "data": nil}, } { if _, err := anthropicThinking(block); err == nil { t.Fatal("accepted malformed thinking") } } }