diff --git a/internal/translator/openai/interactions/responses/interactions_openai_responses_response.go b/internal/translator/openai/interactions/responses/interactions_openai_responses_response.go index 47b9b321..5a9f7808 100644 --- a/internal/translator/openai/interactions/responses/interactions_openai_responses_response.go +++ b/internal/translator/openai/interactions/responses/interactions_openai_responses_response.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/router-for-me/CLIProxyAPI/v7/internal/signature" translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -237,7 +238,7 @@ func interactionsStepStartToResponses(root gjson.Result, st *interactionsToRespo added, _ = sjson.SetBytes(added, "sequence_number", nextResponsesSeq(st)) added, _ = sjson.SetBytes(added, "output_index", index) added, _ = sjson.SetBytes(added, "item.id", itemID) - if signature := st.ReasoningEncrypted[index]; signature != "" { + if signature := interactionsReasoningEncryptedContent(st.ReasoningEncrypted[index]); signature != "" { added, _ = sjson.SetBytes(added, "item.encrypted_content", signature) } return [][]byte{emitResponsesEvent("response.output_item.added", added)} @@ -282,7 +283,7 @@ func interactionsStepDeltaToResponses(root gjson.Result, st *interactionsToRespo payload, _ = sjson.SetBytes(payload, "delta", text) return [][]byte{emitResponsesEvent("response.reasoning_summary_text.delta", payload)} case "thought_signature": - if signature := delta.Get("signature").String(); signature != "" { + if signature := interactionsReasoningEncryptedContent(delta.Get("signature").String()); signature != "" { st.ReasoningEncrypted[index] = signature } return nil @@ -411,7 +412,7 @@ func interactionsThoughtSignature(step gjson.Result) string { "thoughtSignature", "extra_content.google.thought_signature", } { - if signature := step.Get(path).String(); signature != "" { + if signature := interactionsReasoningEncryptedContent(step.Get(path).String()); signature != "" { return signature } } @@ -419,19 +420,34 @@ func interactionsThoughtSignature(step gjson.Result) string { if content.IsArray() { var signature string content.ForEach(func(_, part gjson.Result) bool { - signature = firstNonEmpty( + candidate := firstNonEmpty( part.Get("signature").String(), part.Get("thought_signature").String(), part.Get("thoughtSignature").String(), part.Get("extra_content.google.thought_signature").String(), ) - return signature == "" + if valid := interactionsReasoningEncryptedContent(candidate); valid != "" { + signature = valid + return false + } + return true }) return signature } return "" } +func interactionsReasoningEncryptedContent(rawSignature string) string { + candidate := strings.TrimSpace(rawSignature) + if candidate == "" { + return "" + } + if _, err := signature.InspectGPTReasoningSignature(candidate); err != nil { + return "" + } + return candidate +} + func recordResponsesReasoningSummary(st *interactionsToResponsesStreamState, index int, text string) { if text == "" { return @@ -510,7 +526,7 @@ func responsesCompletedOutputItem(index int, itemType string, st *interactionsTo func responsesReasoningItem(index int, st *interactionsToResponsesStreamState) []byte { item := []byte(`{"id":"","type":"reasoning","encrypted_content":"","summary":[]}`) item, _ = sjson.SetBytes(item, "id", st.ItemIDs[index]) - if signature := st.ReasoningEncrypted[index]; signature != "" { + if signature := interactionsReasoningEncryptedContent(st.ReasoningEncrypted[index]); signature != "" { item, _ = sjson.SetBytes(item, "encrypted_content", signature) } summaries := st.ReasoningSummaries[index] diff --git a/internal/translator/openai/interactions/responses/interactions_openai_responses_response_test.go b/internal/translator/openai/interactions/responses/interactions_openai_responses_response_test.go index e79c11cb..5e6946fe 100644 --- a/internal/translator/openai/interactions/responses/interactions_openai_responses_response_test.go +++ b/internal/translator/openai/interactions/responses/interactions_openai_responses_response_test.go @@ -3,6 +3,7 @@ package responses import ( "bytes" "context" + "encoding/base64" "strings" "testing" @@ -223,9 +224,19 @@ data: {"index":0,"event_type":"step.stop"} } } +func testGPTResponsesReasoningSignature() string { + payload := make([]byte, 1+8+16+16+32) + payload[0] = 0x80 + payload[8] = 1 + for i := 9; i < len(payload); i++ { + payload[i] = byte(i) + } + return base64.URLEncoding.EncodeToString(payload) +} + func TestConvertInteractionsResponseToOpenAIResponsesStreamPreservesThoughtSignature(t *testing.T) { var param any - signature := "EtoRtestThoughtSignature" + signature := testGPTResponsesReasoningSignature() var out [][]byte for _, raw := range [][]byte{ []byte(`event: step.start @@ -272,6 +283,69 @@ data: {"interaction":{"id":"interaction_1","status":"completed","object":"intera } } +func TestConvertInteractionsResponseToOpenAIResponsesStreamDropsForeignThoughtSignature(t *testing.T) { + var param any + foreignSignature := "foreign-gemini-signature" + var out [][]byte + for _, raw := range [][]byte{ + []byte(`event: step.start +data: {"index":0,"step":{"type":"thought"},"event_type":"step.start"} + +`), + []byte(`event: step.delta +data: {"index":0,"delta":{"content":{"text":"thinking","type":"text"},"type":"thought_summary"},"event_type":"step.delta"} + +`), + []byte(`event: step.delta +data: {"index":0,"delta":{"signature":"` + foreignSignature + `","type":"thought_signature"},"event_type":"step.delta"} + +`), + []byte(`event: step.stop +data: {"index":0,"event_type":"step.stop"} + +`), + []byte(`event: interaction.completed +data: {"interaction":{"id":"interaction_1","status":"completed","object":"interaction","model":"gpt-test"},"event_type":"interaction.completed"} + +`), + } { + out = append(out, ConvertInteractionsResponseToOpenAIResponses(context.Background(), "gpt-test", []byte(`{"model":"gpt-test"}`), nil, raw, ¶m)...) + } + + donePayload := findResponsesEventPayload(out, "response.output_item.done") + if got := gjson.GetBytes(donePayload, "item.encrypted_content").String(); got != "" { + t.Fatalf("done encrypted_content = %q, want empty for foreign signature. Payload: %s", got, string(donePayload)) + } + if got := gjson.GetBytes(donePayload, "item.summary.0.text").String(); got != "thinking" { + t.Fatalf("done summary = %q, want thinking. Payload: %s", got, string(donePayload)) + } + completedPayload := findResponsesEventPayload(out, "response.completed") + if got := gjson.GetBytes(completedPayload, "response.output.0.encrypted_content").String(); got != "" { + t.Fatalf("completed encrypted_content = %q, want empty for foreign signature. Payload: %s", got, string(completedPayload)) + } +} + +func TestConvertInteractionsResponseToOpenAIResponsesNonStreamThoughtSignature(t *testing.T) { + validSig := testGPTResponsesReasoningSignature() + rawValid := []byte(`{"id":"interaction_1","object":"interaction","status":"completed","steps":[{"type":"thought","signature":"` + validSig + `","content":[{"type":"text","text":"thinking"}]}],"usage":{"total_tokens":1}}`) + outValid := ConvertInteractionsResponseToOpenAIResponsesNonStream(context.Background(), "gpt-test", []byte(`{"model":"gpt-test"}`), nil, rawValid, nil) + if got := gjson.GetBytes(outValid, "output.0.encrypted_content").String(); got != validSig { + t.Fatalf("valid encrypted_content = %q, want %q. Output: %s", got, validSig, string(outValid)) + } + if got := gjson.GetBytes(outValid, "output.0.summary.0.text").String(); got != "thinking" { + t.Fatalf("summary = %q, want thinking. Output: %s", got, string(outValid)) + } + + rawForeign := []byte(`{"id":"interaction_1","object":"interaction","status":"completed","steps":[{"type":"thought","thought_signature":"foreign-gemini-signature","content":[{"type":"text","text":"thinking"}]}],"usage":{"total_tokens":1}}`) + outForeign := ConvertInteractionsResponseToOpenAIResponsesNonStream(context.Background(), "gpt-test", []byte(`{"model":"gpt-test"}`), nil, rawForeign, nil) + if got := gjson.GetBytes(outForeign, "output.0.encrypted_content").String(); got != "" { + t.Fatalf("foreign encrypted_content = %q, want empty. Output: %s", got, string(outForeign)) + } + if got := gjson.GetBytes(outForeign, "output.0.summary.0.text").String(); got != "thinking" { + t.Fatalf("summary = %q, want thinking. Output: %s", got, string(outForeign)) + } +} + func TestConvertOpenAIResponsesResponseToInteractionsNonStreamFunctionCall(t *testing.T) { raw := []byte(`{"id":"resp_1","output":[{"type":"function_call","name":"lookup","call_id":"call_1","arguments":{"q":"x"}}],"usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}`) out := ConvertOpenAIResponsesResponseToInteractionsNonStream(context.Background(), "gpt-test", nil, nil, raw, nil)