Preserve Codex reasoning signatures for Claude

This commit is contained in:
sususu
2026-04-27 16:46:00 +08:00
parent d85e13b044
commit c523101439
4 changed files with 332 additions and 74 deletions
@@ -243,6 +243,147 @@ func TestConvertCodexResponseToClaude_StreamThinkingUsesEarlyCapturedSignatureWh
}
}
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, &param)...)
}
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, &param)...)
}
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":[]}`)