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 47b9b321c..c70451e4a 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)} @@ -283,7 +284,7 @@ func interactionsStepDeltaToResponses(root gjson.Result, st *interactionsToRespo return [][]byte{emitResponsesEvent("response.reasoning_summary_text.delta", payload)} case "thought_signature": if signature := delta.Get("signature").String(); signature != "" { - st.ReasoningEncrypted[index] = signature + st.ReasoningEncrypted[index] = interactionsReasoningEncryptedContent(signature) } return nil case "arguments_delta": @@ -403,6 +404,16 @@ func responsesCompletedEvent(modelName string, root gjson.Result, st *interactio return emitResponsesEvent("response.completed", payload) } +func interactionsReasoningEncryptedContent(raw string) string { + if raw == "" { + return "" + } + if _, err := signature.InspectGPTReasoningSignature(raw); err != nil { + return "" + } + return raw +} + func interactionsThoughtSignature(step gjson.Result) string { for _, path := range []string{ "encrypted_content", @@ -411,23 +422,25 @@ func interactionsThoughtSignature(step gjson.Result) string { "thoughtSignature", "extra_content.google.thought_signature", } { - if signature := step.Get(path).String(); signature != "" { - return signature + if raw := step.Get(path).String(); raw != "" { + if enc := interactionsReasoningEncryptedContent(raw); enc != "" { + return enc + } } } content := step.Get("content") if content.IsArray() { - var signature string + var raw string content.ForEach(func(_, part gjson.Result) bool { - signature = firstNonEmpty( + raw = firstNonEmpty( part.Get("signature").String(), part.Get("thought_signature").String(), part.Get("thoughtSignature").String(), part.Get("extra_content.google.thought_signature").String(), ) - return signature == "" + return raw == "" }) - return signature + return interactionsReasoningEncryptedContent(raw) } return "" } @@ -510,7 +523,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 e79c11cbc..8352bb7a7 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,18 @@ data: {"index":0,"event_type":"step.stop"} } } +func testGPTReasoningSignatureForInteractions() string { + payload := make([]byte, 1+8+16+16+32) + payload[0] = 0x80 + for i := 9; i < len(payload); i++ { + payload[i] = byte(i) + } + return base64.RawURLEncoding.EncodeToString(payload) +} + func TestConvertInteractionsResponseToOpenAIResponsesStreamPreservesThoughtSignature(t *testing.T) { var param any - signature := "EtoRtestThoughtSignature" + signature := testGPTReasoningSignatureForInteractions() var out [][]byte for _, raw := range [][]byte{ []byte(`event: step.start @@ -272,6 +282,48 @@ data: {"interaction":{"id":"interaction_1","status":"completed","object":"intera } } +func TestConvertInteractionsResponseToOpenAIResponsesStreamDropsInvalidThoughtSignature(t *testing.T) { + var param any + signature := "foreign-thought-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":"` + signature + `","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 gjson.GetBytes(donePayload, "item.encrypted_content").Exists() && gjson.GetBytes(donePayload, "item.encrypted_content").String() != "" { + t.Fatalf("invalid encrypted_content should be dropped, got %s", 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 gjson.GetBytes(completedPayload, "response.output.0.encrypted_content").Exists() && gjson.GetBytes(completedPayload, "response.output.0.encrypted_content").String() != "" { + t.Fatalf("completed invalid encrypted_content should be dropped, got %s", string(completedPayload)) + } +} + 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)