Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)}
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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",
Expand All @@ -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 ""
}
Expand Down Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package responses
import (
"bytes"
"context"
"encoding/base64"
"strings"
"testing"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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, &param)...)
}

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)
Expand Down
Loading