package claude import ( "context" "strings" "testing" "github.com/tidwall/gjson" ) func TestConvertCodexResponseToClaude_StreamThinkingIncludesSignature(t *testing.T) { ctx := context.Background() originalRequest := []byte(`{"messages":[]}`) var param any chunks := [][]byte{ []byte("data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_123\",\"model\":\"gpt-5\"}}"), []byte("data: {\"type\":\"response.reasoning_summary_part.added\"}"), []byte("data: {\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\"Let me think\"}"), []byte("data: {\"type\":\"response.reasoning_summary_part.done\"}"), []byte("data: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"reasoning\",\"encrypted_content\":\"enc_sig_123\"}}"), } var outputs [][]byte for _, chunk := range chunks { outputs = append(outputs, ConvertCodexResponseToClaude(ctx, "", originalRequest, nil, chunk, ¶m)...) } startFound := false signatureDeltaFound := false stopFound := false for _, out := range outputs { for _, line := range strings.Split(string(out), "\n") { if !strings.HasPrefix(line, "data: ") { continue } data := gjson.Parse(strings.TrimPrefix(line, "data: ")) switch data.Get("type").String() { case "content_block_start": if data.Get("content_block.type").String() == "thinking" { startFound = true if data.Get("content_block.signature").Exists() { t.Fatalf("thinking start block should NOT have signature field when signature is unknown: %s", line) } } case "content_block_delta": if data.Get("delta.type").String() == "signature_delta" { signatureDeltaFound = true if got := data.Get("delta.signature").String(); got != "enc_sig_123" { t.Fatalf("unexpected signature delta: %q", got) } } case "content_block_stop": stopFound = true } } } if !startFound { t.Fatal("expected thinking content_block_start event") } if !signatureDeltaFound { t.Fatal("expected signature_delta event for thinking block") } if !stopFound { t.Fatal("expected content_block_stop event for thinking block") } } func TestConvertCodexResponseToClaude_StreamThinkingWithoutReasoningItemStillIncludesSignatureField(t *testing.T) { ctx := context.Background() originalRequest := []byte(`{"messages":[]}`) var param any chunks := [][]byte{ []byte("data: {\"type\":\"response.reasoning_summary_part.added\"}"), []byte("data: {\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\"Let me think\"}"), []byte("data: {\"type\":\"response.reasoning_summary_part.done\"}"), []byte("data: {\"type\":\"response.completed\",\"response\":{\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}}"), } var outputs [][]byte for _, chunk := range chunks { outputs = append(outputs, ConvertCodexResponseToClaude(ctx, "", originalRequest, nil, chunk, ¶m)...) } thinkingStartFound := false thinkingStopFound := false signatureDeltaFound := false for _, out := range outputs { for _, line := range strings.Split(string(out), "\n") { if !strings.HasPrefix(line, "data: ") { continue } data := gjson.Parse(strings.TrimPrefix(line, "data: ")) if data.Get("type").String() == "content_block_start" && data.Get("content_block.type").String() == "thinking" { thinkingStartFound = true if data.Get("content_block.signature").Exists() { t.Fatalf("thinking start block should NOT have signature field without encrypted_content: %s", line) } } if data.Get("type").String() == "content_block_stop" && data.Get("index").Int() == 0 { thinkingStopFound = true } if data.Get("type").String() == "content_block_delta" && data.Get("delta.type").String() == "signature_delta" { signatureDeltaFound = true } } } if !thinkingStartFound { t.Fatal("expected thinking content_block_start event") } if !thinkingStopFound { t.Fatal("expected thinking content_block_stop event") } if signatureDeltaFound { t.Fatal("did not expect signature_delta without encrypted_content") } } func TestConvertCodexResponseToClaude_StreamThinkingFinalizesPendingBlockBeforeNextSummaryPart(t *testing.T) { ctx := context.Background() originalRequest := []byte(`{"messages":[]}`) var param any chunks := [][]byte{ []byte("data: {\"type\":\"response.reasoning_summary_part.added\"}"), []byte("data: {\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\"First part\"}"), []byte("data: {\"type\":\"response.reasoning_summary_part.done\"}"), []byte("data: {\"type\":\"response.reasoning_summary_part.added\"}"), } var outputs [][]byte for _, chunk := range chunks { outputs = append(outputs, ConvertCodexResponseToClaude(ctx, "", originalRequest, nil, chunk, ¶m)...) } startCount := 0 stopCount := 0 for _, out := range outputs { for _, line := range strings.Split(string(out), "\n") { if !strings.HasPrefix(line, "data: ") { continue } data := gjson.Parse(strings.TrimPrefix(line, "data: ")) if data.Get("type").String() == "content_block_start" && data.Get("content_block.type").String() == "thinking" { startCount++ } if data.Get("type").String() == "content_block_stop" { stopCount++ } } } if startCount != 2 { t.Fatalf("expected 2 thinking block starts, got %d", startCount) } if stopCount != 1 { t.Fatalf("expected pending thinking block to be finalized before second start, got %d stops", stopCount) } } func TestConvertCodexResponseToClaude_StreamThinkingRetainsSignatureAcrossMultipartReasoning(t *testing.T) { ctx := context.Background() originalRequest := []byte(`{"messages":[]}`) var param any chunks := [][]byte{ []byte("data: {\"type\":\"response.output_item.added\",\"item\":{\"type\":\"reasoning\",\"encrypted_content\":\"enc_sig_multipart\"}}"), []byte("data: {\"type\":\"response.reasoning_summary_part.added\"}"), []byte("data: {\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\"First part\"}"), []byte("data: {\"type\":\"response.reasoning_summary_part.done\"}"), []byte("data: {\"type\":\"response.reasoning_summary_part.added\"}"), []byte("data: {\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\"Second part\"}"), []byte("data: {\"type\":\"response.reasoning_summary_part.done\"}"), []byte("data: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"reasoning\"}}"), } var outputs [][]byte for _, chunk := range chunks { outputs = append(outputs, ConvertCodexResponseToClaude(ctx, "", originalRequest, nil, chunk, ¶m)...) } signatureDeltaCount := 0 for _, out := range outputs { for _, line := range strings.Split(string(out), "\n") { if !strings.HasPrefix(line, "data: ") { continue } data := gjson.Parse(strings.TrimPrefix(line, "data: ")) if data.Get("type").String() == "content_block_delta" && data.Get("delta.type").String() == "signature_delta" { signatureDeltaCount++ if got := data.Get("delta.signature").String(); got != "enc_sig_multipart" { t.Fatalf("unexpected signature delta: %q", got) } } } } if signatureDeltaCount != 2 { t.Fatalf("expected signature_delta for both multipart thinking blocks, got %d", signatureDeltaCount) } } func TestConvertCodexResponseToClaude_StreamThinkingUsesEarlyCapturedSignatureWhenDoneOmitsIt(t *testing.T) { ctx := context.Background() originalRequest := []byte(`{"messages":[]}`) var param any chunks := [][]byte{ []byte("data: {\"type\":\"response.output_item.added\",\"item\":{\"type\":\"reasoning\",\"encrypted_content\":\"enc_sig_early\"}}"), []byte("data: {\"type\":\"response.reasoning_summary_part.added\"}"), []byte("data: {\"type\":\"response.reasoning_summary_text.delta\",\"delta\":\"Let me think\"}"), []byte("data: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"reasoning\"}}"), } var outputs [][]byte for _, chunk := range chunks { outputs = append(outputs, ConvertCodexResponseToClaude(ctx, "", originalRequest, nil, chunk, ¶m)...) } signatureDeltaCount := 0 for _, out := range outputs { for _, line := range strings.Split(string(out), "\n") { if !strings.HasPrefix(line, "data: ") { continue } data := gjson.Parse(strings.TrimPrefix(line, "data: ")) if data.Get("type").String() == "content_block_delta" && data.Get("delta.type").String() == "signature_delta" { signatureDeltaCount++ if got := data.Get("delta.signature").String(); got != "enc_sig_early" { t.Fatalf("unexpected signature delta: %q", got) } } } } if signatureDeltaCount != 1 { t.Fatalf("expected signature_delta from early-captured signature, got %d", signatureDeltaCount) } } func TestConvertCodexResponseToClaudeNonStream_ThinkingIncludesSignature(t *testing.T) { ctx := context.Background() originalRequest := []byte(`{"messages":[]}`) response := []byte(`{ "type":"response.completed", "response":{ "id":"resp_123", "model":"gpt-5", "usage":{"input_tokens":10,"output_tokens":20}, "output":[ { "type":"reasoning", "encrypted_content":"enc_sig_nonstream", "summary":[{"type":"summary_text","text":"internal reasoning"}] }, { "type":"message", "content":[{"type":"output_text","text":"final answer"}] } ] } }`) out := ConvertCodexResponseToClaudeNonStream(ctx, "", originalRequest, nil, response, nil) parsed := gjson.ParseBytes(out) thinking := parsed.Get("content.0") if thinking.Get("type").String() != "thinking" { t.Fatalf("expected first content block to be thinking, got %s", thinking.Raw) } if got := thinking.Get("signature").String(); got != "enc_sig_nonstream" { t.Fatalf("expected signature to be preserved, got %q", got) } if got := thinking.Get("thinking").String(); got != "internal reasoning" { t.Fatalf("unexpected thinking text: %q", got) } }