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 TestConvertCodexResponseToClaude_StreamThinkingUsesFinalDoneSignature(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_initial\"}}"), []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_final\"}}"), } var outputs [][]byte for _, chunk := range chunks { outputs = append(outputs, ConvertCodexResponseToClaude(ctx, "", originalRequest, nil, chunk, ¶m)...) } signatureDeltaCount := 0 events := []string{} 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" { events = append(events, "thinking_start") } if data.Get("type").String() == "content_block_delta" && data.Get("delta.type").String() == "thinking_delta" { events = append(events, "thinking_delta") } if data.Get("type").String() == "content_block_stop" && data.Get("index").Int() == 0 { events = append(events, "thinking_stop") } if data.Get("type").String() != "content_block_delta" || data.Get("delta.type").String() != "signature_delta" { continue } events = append(events, "signature_delta") signatureDeltaCount++ if got := data.Get("delta.signature").String(); got != "enc_sig_final" { t.Fatalf("signature delta = %q, want final done signature", got) } } } if signatureDeltaCount != 1 { t.Fatalf("expected one signature_delta, got %d", signatureDeltaCount) } if got, want := strings.Join(events, ","), "thinking_start,thinking_delta,signature_delta,thinking_stop"; got != want { t.Fatalf("thinking event order = %s, want %s", got, want) } } func TestConvertCodexResponseToClaude_StreamSignatureOnlyReasoningEmitsThinkingSignature(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.output_item.added\",\"item\":{\"type\":\"reasoning\",\"encrypted_content\":\"enc_sig_initial\"}}"), []byte("data: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"reasoning\",\"encrypted_content\":\"enc_sig_only\"}}"), []byte("data: {\"type\":\"response.content_part.added\"}"), []byte("data: {\"type\":\"response.output_text.delta\",\"delta\":\"ok\"}"), } var outputs [][]byte for _, chunk := range chunks { outputs = append(outputs, ConvertCodexResponseToClaude(ctx, "", originalRequest, nil, chunk, ¶m)...) } thinkingStartFound := false thinkingDeltaFound := false signatureDeltaFound := false thinkingStopFound := false textStartIndex := int64(-1) events := []string{} 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" { events = append(events, "thinking_start") thinkingStartFound = true if got := data.Get("index").Int(); got != 0 { t.Fatalf("thinking block index = %d, want 0", got) } } if data.Get("content_block.type").String() == "text" { events = append(events, "text_start") textStartIndex = data.Get("index").Int() } case "content_block_delta": switch data.Get("delta.type").String() { case "thinking_delta": thinkingDeltaFound = true case "signature_delta": events = append(events, "signature_delta") signatureDeltaFound = true if got := data.Get("index").Int(); got != 0 { t.Fatalf("signature delta index = %d, want 0", got) } if got := data.Get("delta.signature").String(); got != "enc_sig_only" { t.Fatalf("unexpected signature delta: %q", got) } } case "content_block_stop": if data.Get("index").Int() == 0 { events = append(events, "thinking_stop") thinkingStopFound = true } } } } if !thinkingStartFound { t.Fatal("expected signature-only reasoning to start a thinking block") } if thinkingDeltaFound { t.Fatal("did not expect thinking_delta when upstream omitted summary text") } if !signatureDeltaFound { t.Fatal("expected signature_delta from encrypted_content-only reasoning") } if !thinkingStopFound { t.Fatal("expected signature-only thinking block to stop") } if textStartIndex != 1 { t.Fatalf("text block index = %d, want 1 after signature-only thinking block", textStartIndex) } if got, want := strings.Join(events, ","), "thinking_start,signature_delta,thinking_stop,text_start"; got != want { t.Fatalf("signature-only event order = %s, want %s", got, want) } } 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) } } func TestConvertCodexResponseToClaude_StreamEmptyOutputUsesOutputItemDoneMessageFallback(t *testing.T) { ctx := context.Background() originalRequest := []byte(`{"tools":[]}`) var param any chunks := [][]byte{ []byte("data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_1\",\"model\":\"gpt-5\"}}"), []byte("data: {\"type\":\"response.output_item.done\",\"item\":{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"ok\"}]},\"output_index\":0}"), []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)...) } foundText := 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_delta" && data.Get("delta.type").String() == "text_delta" && data.Get("delta.text").String() == "ok" { foundText = true break } } if foundText { break } } if !foundText { t.Fatalf("expected fallback content from response.output_item.done message; outputs=%q", outputs) } }