Merge pull request #3088 from sususu98/codex-claude-reasoning-signature
Preserve Codex reasoning signatures for Claude
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
package claude
|
package claude
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -125,21 +126,14 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
thinkingText := thinking.GetThinkingText(part)
|
|
||||||
signature := part.Get("signature").String()
|
signature := part.Get("signature").String()
|
||||||
if strings.TrimSpace(thinkingText) == "" && signature == "" {
|
if !isFernetLikeReasoningSignature(signature) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flushMessage()
|
||||||
reasoningItem := []byte(`{"type":"reasoning","summary":[],"content":null}`)
|
reasoningItem := []byte(`{"type":"reasoning","summary":[],"content":null}`)
|
||||||
if signature != "" {
|
|
||||||
reasoningItem, _ = sjson.SetBytes(reasoningItem, "encrypted_content", signature)
|
reasoningItem, _ = sjson.SetBytes(reasoningItem, "encrypted_content", signature)
|
||||||
}
|
|
||||||
if strings.TrimSpace(thinkingText) != "" {
|
|
||||||
summary := []byte(`{"type":"summary_text","text":""}`)
|
|
||||||
summary, _ = sjson.SetBytes(summary, "text", thinkingText)
|
|
||||||
reasoningItem, _ = sjson.SetRawBytes(reasoningItem, "summary.-1", summary)
|
|
||||||
}
|
|
||||||
template, _ = sjson.SetRawBytes(template, "input.-1", reasoningItem)
|
template, _ = sjson.SetRawBytes(template, "input.-1", reasoningItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +148,6 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
case "text":
|
case "text":
|
||||||
appendTextContent(messageContentResult.Get("text").String())
|
appendTextContent(messageContentResult.Get("text").String())
|
||||||
case "thinking":
|
case "thinking":
|
||||||
flushMessage()
|
|
||||||
appendReasoningContent(messageContentResult)
|
appendReasoningContent(messageContentResult)
|
||||||
case "image":
|
case "image":
|
||||||
sourceResult := messageContentResult.Get("source")
|
sourceResult := messageContentResult.Get("source")
|
||||||
@@ -344,6 +337,39 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
return template
|
return template
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isFernetLikeReasoningSignature checks only the encrypted_content envelope shape
|
||||||
|
// observed in OpenAI reasoning signatures. It does not authenticate source or payload type.
|
||||||
|
func isFernetLikeReasoningSignature(signature string) bool {
|
||||||
|
const (
|
||||||
|
fernetVersionLen = 1
|
||||||
|
fernetTimestamp = 8
|
||||||
|
fernetIV = 16
|
||||||
|
fernetHMAC = 32
|
||||||
|
aesBlockSize = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
signature = strings.TrimSpace(signature)
|
||||||
|
if !strings.HasPrefix(signature, "gAAAA") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := base64.URLEncoding.DecodeString(signature)
|
||||||
|
if err != nil {
|
||||||
|
decoded, err = base64.RawURLEncoding.DecodeString(signature)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
minLen := fernetVersionLen + fernetTimestamp + fernetIV + aesBlockSize + fernetHMAC
|
||||||
|
if len(decoded) < minLen || decoded[0] != 0x80 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertextLen := len(decoded) - fernetVersionLen - fernetTimestamp - fernetIV - fernetHMAC
|
||||||
|
return ciphertextLen > 0 && ciphertextLen%aesBlockSize == 0
|
||||||
|
}
|
||||||
|
|
||||||
// shortenNameIfNeeded applies a simple shortening rule for a single name.
|
// shortenNameIfNeeded applies a simple shortening rule for a single name.
|
||||||
func shortenNameIfNeeded(name string) string {
|
func shortenNameIfNeeded(name string) string {
|
||||||
const limit = 64
|
const limit = 64
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package claude
|
package claude
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
@@ -134,74 +136,143 @@ func TestConvertClaudeRequestToCodex_ParallelToolCalls(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvertClaudeRequestToCodex_ThinkingSignatureToEncryptedContent(t *testing.T) {
|
func TestConvertClaudeRequestToCodex_AssistantThinkingSignatureToReasoningItem(t *testing.T) {
|
||||||
result := ConvertClaudeRequestToCodex("test-model", []byte(`{
|
signature := validCodexReasoningSignature()
|
||||||
|
inputJSON := `{
|
||||||
"model": "claude-3-opus",
|
"model": "claude-3-opus",
|
||||||
"messages": [{
|
"messages": [
|
||||||
|
{
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": [
|
"content": [
|
||||||
{"type": "thinking", "thinking": "Internal reasoning.", "signature": "sig_123"},
|
{
|
||||||
{"type": "text", "text": "Visible answer."}
|
"type": "thinking",
|
||||||
|
"thinking": "visible summary must not be replayed",
|
||||||
|
"signature": "` + signature + `"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "visible answer"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}]
|
},
|
||||||
}`), false)
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "continue"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
result := ConvertClaudeRequestToCodex("test-model", []byte(inputJSON), false)
|
||||||
resultJSON := gjson.ParseBytes(result)
|
resultJSON := gjson.ParseBytes(result)
|
||||||
inputs := resultJSON.Get("input").Array()
|
inputs := resultJSON.Get("input").Array()
|
||||||
|
if len(inputs) != 3 {
|
||||||
if len(inputs) != 2 {
|
t.Fatalf("got %d input items, want 3. Output: %s", len(inputs), string(result))
|
||||||
t.Fatalf("got %d input items, want 2. Output: %s", len(inputs), string(result))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reasoning := inputs[0]
|
reasoning := inputs[0]
|
||||||
if got := reasoning.Get("type").String(); got != "reasoning" {
|
if got := reasoning.Get("type").String(); got != "reasoning" {
|
||||||
t.Fatalf("input[0].type = %q, want %q. Output: %s", got, "reasoning", string(result))
|
t.Fatalf("first input type = %q, want reasoning. Output: %s", got, string(result))
|
||||||
}
|
}
|
||||||
if got := reasoning.Get("encrypted_content").String(); got != "sig_123" {
|
if got := reasoning.Get("encrypted_content").String(); got != signature {
|
||||||
t.Fatalf("encrypted_content = %q, want %q. Output: %s", got, "sig_123", string(result))
|
t.Fatalf("encrypted_content = %q, want %q", got, signature)
|
||||||
}
|
}
|
||||||
if got := reasoning.Get("summary.0.type").String(); got != "summary_text" {
|
if got := reasoning.Get("summary").Raw; got != "[]" {
|
||||||
t.Fatalf("summary.0.type = %q, want %q. Output: %s", got, "summary_text", string(result))
|
t.Fatalf("summary = %s, want []", got)
|
||||||
}
|
}
|
||||||
if got := reasoning.Get("summary.0.text").String(); got != "Internal reasoning." {
|
if got := reasoning.Get("content").Raw; got != "null" {
|
||||||
t.Fatalf("summary.0.text = %q, want %q. Output: %s", got, "Internal reasoning.", string(result))
|
t.Fatalf("content = %s, want null", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
message := inputs[1]
|
assistantMessage := inputs[1]
|
||||||
if got := message.Get("type").String(); got != "message" {
|
if got := assistantMessage.Get("role").String(); got != "assistant" {
|
||||||
t.Fatalf("input[1].type = %q, want %q. Output: %s", got, "message", string(result))
|
t.Fatalf("second input role = %q, want assistant. Output: %s", got, string(result))
|
||||||
}
|
}
|
||||||
if got := message.Get("role").String(); got != "assistant" {
|
if got := assistantMessage.Get("content.0.type").String(); got != "output_text" {
|
||||||
t.Fatalf("input[1].role = %q, want %q. Output: %s", got, "assistant", string(result))
|
t.Fatalf("assistant content type = %q, want output_text", got)
|
||||||
}
|
}
|
||||||
if got := message.Get("content.0.type").String(); got != "output_text" {
|
if got := assistantMessage.Get("content.0.text").String(); got != "visible answer" {
|
||||||
t.Fatalf("content.0.type = %q, want %q. Output: %s", got, "output_text", string(result))
|
t.Fatalf("assistant text = %q, want visible answer", got)
|
||||||
}
|
}
|
||||||
if got := message.Get("content.0.text").String(); got != "Visible answer." {
|
if strings.Contains(string(result), "visible summary must not be replayed") {
|
||||||
t.Fatalf("content.0.text = %q, want %q. Output: %s", got, "Visible answer.", string(result))
|
t.Fatalf("thinking text should not be replayed into Codex input. Output: %s", string(result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvertClaudeRequestToCodex_ThinkingSignatureWithoutText(t *testing.T) {
|
func TestConvertClaudeRequestToCodex_IgnoresNonCodexThinkingSignatures(t *testing.T) {
|
||||||
result := ConvertClaudeRequestToCodex("test-model", []byte(`{
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
inputJSON string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Ignore user thinking even with Codex-shaped signature",
|
||||||
|
inputJSON: `{
|
||||||
"model": "claude-3-opus",
|
"model": "claude-3-opus",
|
||||||
"messages": [{
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "thinking",
|
||||||
|
"thinking": "user supplied thinking",
|
||||||
|
"signature": "` + validCodexReasoningSignature() + `"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "hello"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignore Anthropic native signature",
|
||||||
|
inputJSON: `{
|
||||||
|
"model": "claude-3-opus",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": [{"type": "thinking", "thinking": "", "signature": "sig_empty_text"}]
|
"content": [
|
||||||
}]
|
{
|
||||||
}`), false)
|
"type": "thinking",
|
||||||
resultJSON := gjson.ParseBytes(result)
|
"thinking": "anthropic thinking",
|
||||||
inputs := resultJSON.Get("input").Array()
|
"signature": "Eo8Canthropic-state"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "visible answer"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
if len(inputs) != 1 {
|
for _, tt := range tests {
|
||||||
t.Fatalf("got %d input items, want 1. Output: %s", len(inputs), string(result))
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := ConvertClaudeRequestToCodex("test-model", []byte(tt.inputJSON), false)
|
||||||
|
if got := countRequestInputItemsByType(result, "reasoning"); got != 0 {
|
||||||
|
t.Fatalf("got %d reasoning items, want 0. Output: %s", got, string(result))
|
||||||
}
|
}
|
||||||
if got := inputs[0].Get("type").String(); got != "reasoning" {
|
})
|
||||||
t.Fatalf("input[0].type = %q, want %q. Output: %s", got, "reasoning", string(result))
|
|
||||||
}
|
|
||||||
if got := inputs[0].Get("encrypted_content").String(); got != "sig_empty_text" {
|
|
||||||
t.Fatalf("encrypted_content = %q, want %q. Output: %s", got, "sig_empty_text", string(result))
|
|
||||||
}
|
|
||||||
if got := len(inputs[0].Get("summary").Array()); got != 0 {
|
|
||||||
t.Fatalf("summary length = %d, want 0. Output: %s", got, string(result))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func countRequestInputItemsByType(result []byte, itemType string) int {
|
||||||
|
count := 0
|
||||||
|
gjson.GetBytes(result, "input").ForEach(func(_, item gjson.Result) bool {
|
||||||
|
if item.Get("type").String() == itemType {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func validCodexReasoningSignature() string {
|
||||||
|
raw := make([]byte, 1+8+16+16+32)
|
||||||
|
raw[0] = 0x80
|
||||||
|
raw[8] = 1
|
||||||
|
return base64.URLEncoding.EncodeToString(raw)
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type ConvertCodexResponseToClaudeParams struct {
|
|||||||
ThinkingBlockOpen bool
|
ThinkingBlockOpen bool
|
||||||
ThinkingStopPending bool
|
ThinkingStopPending bool
|
||||||
ThinkingSignature string
|
ThinkingSignature string
|
||||||
|
ThinkingSummarySeen bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertCodexResponseToClaude performs sophisticated streaming response format conversion.
|
// ConvertCodexResponseToClaude performs sophisticated streaming response format conversion.
|
||||||
@@ -86,12 +87,8 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa
|
|||||||
if params.ThinkingBlockOpen && params.ThinkingStopPending {
|
if params.ThinkingBlockOpen && params.ThinkingStopPending {
|
||||||
output = append(output, finalizeCodexThinkingBlock(params)...)
|
output = append(output, finalizeCodexThinkingBlock(params)...)
|
||||||
}
|
}
|
||||||
template = []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`)
|
params.ThinkingSummarySeen = true
|
||||||
template, _ = sjson.SetBytes(template, "index", params.BlockIndex)
|
output = append(output, startCodexThinkingBlock(params)...)
|
||||||
params.ThinkingBlockOpen = true
|
|
||||||
params.ThinkingStopPending = false
|
|
||||||
|
|
||||||
output = translatorcommon.AppendSSEEventBytes(output, "content_block_start", template, 2)
|
|
||||||
} else if typeStr == "response.reasoning_summary_text.delta" {
|
} else if typeStr == "response.reasoning_summary_text.delta" {
|
||||||
template = []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`)
|
template = []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`)
|
||||||
template, _ = sjson.SetBytes(template, "index", params.BlockIndex)
|
template, _ = sjson.SetBytes(template, "index", params.BlockIndex)
|
||||||
@@ -100,9 +97,6 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa
|
|||||||
output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2)
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2)
|
||||||
} else if typeStr == "response.reasoning_summary_part.done" {
|
} else if typeStr == "response.reasoning_summary_part.done" {
|
||||||
params.ThinkingStopPending = true
|
params.ThinkingStopPending = true
|
||||||
if params.ThinkingSignature != "" {
|
|
||||||
output = append(output, finalizeCodexThinkingBlock(params)...)
|
|
||||||
}
|
|
||||||
} else if typeStr == "response.content_part.added" {
|
} else if typeStr == "response.content_part.added" {
|
||||||
template = []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`)
|
template = []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`)
|
||||||
template, _ = sjson.SetBytes(template, "index", params.BlockIndex)
|
template, _ = sjson.SetBytes(template, "index", params.BlockIndex)
|
||||||
@@ -169,10 +163,8 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa
|
|||||||
|
|
||||||
output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2)
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2)
|
||||||
} else if itemType == "reasoning" {
|
} else if itemType == "reasoning" {
|
||||||
|
params.ThinkingSummarySeen = false
|
||||||
params.ThinkingSignature = itemResult.Get("encrypted_content").String()
|
params.ThinkingSignature = itemResult.Get("encrypted_content").String()
|
||||||
if params.ThinkingStopPending {
|
|
||||||
output = append(output, finalizeCodexThinkingBlock(params)...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if typeStr == "response.output_item.done" {
|
} else if typeStr == "response.output_item.done" {
|
||||||
itemResult := rootResult.Get("item")
|
itemResult := rootResult.Get("item")
|
||||||
@@ -229,8 +221,13 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa
|
|||||||
if signature := itemResult.Get("encrypted_content").String(); signature != "" {
|
if signature := itemResult.Get("encrypted_content").String(); signature != "" {
|
||||||
params.ThinkingSignature = signature
|
params.ThinkingSignature = signature
|
||||||
}
|
}
|
||||||
|
if params.ThinkingSummarySeen {
|
||||||
output = append(output, finalizeCodexThinkingBlock(params)...)
|
output = append(output, finalizeCodexThinkingBlock(params)...)
|
||||||
|
} else {
|
||||||
|
output = append(output, finalizeCodexSignatureOnlyThinkingBlock(params)...)
|
||||||
|
}
|
||||||
params.ThinkingSignature = ""
|
params.ThinkingSignature = ""
|
||||||
|
params.ThinkingSummarySeen = false
|
||||||
}
|
}
|
||||||
} else if typeStr == "response.function_call_arguments.delta" {
|
} else if typeStr == "response.function_call_arguments.delta" {
|
||||||
params.HasReceivedArgumentsDelta = true
|
params.HasReceivedArgumentsDelta = true
|
||||||
@@ -437,6 +434,29 @@ func ClaudeTokenCount(_ context.Context, count int64) []byte {
|
|||||||
return translatorcommon.ClaudeInputTokensJSON(count)
|
return translatorcommon.ClaudeInputTokensJSON(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startCodexThinkingBlock(params *ConvertCodexResponseToClaudeParams) []byte {
|
||||||
|
if params.ThinkingBlockOpen {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
template := []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`)
|
||||||
|
template, _ = sjson.SetBytes(template, "index", params.BlockIndex)
|
||||||
|
params.ThinkingBlockOpen = true
|
||||||
|
params.ThinkingStopPending = false
|
||||||
|
|
||||||
|
return translatorcommon.AppendSSEEventBytes(nil, "content_block_start", template, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func finalizeCodexSignatureOnlyThinkingBlock(params *ConvertCodexResponseToClaudeParams) []byte {
|
||||||
|
if params.ThinkingSignature == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
output := startCodexThinkingBlock(params)
|
||||||
|
output = append(output, finalizeCodexThinkingBlock(params)...)
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
func finalizeCodexThinkingBlock(params *ConvertCodexResponseToClaudeParams) []byte {
|
func finalizeCodexThinkingBlock(params *ConvertCodexResponseToClaudeParams) []byte {
|
||||||
if !params.ThinkingBlockOpen {
|
if !params.ThinkingBlockOpen {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -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, ¶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) {
|
func TestConvertCodexResponseToClaudeNonStream_ThinkingIncludesSignature(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
originalRequest := []byte(`{"messages":[]}`)
|
originalRequest := []byte(`{"messages":[]}`)
|
||||||
|
|||||||
Reference in New Issue
Block a user