Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
19
backend/internal/translator/gemini/openai/responses/init.go
Normal file
19
backend/internal/translator/gemini/openai/responses/init.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package responses
|
||||
|
||||
import (
|
||||
. "github.com/router-for-me/CLIProxyAPI/v7/internal/constant"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
translator.Register(
|
||||
OpenaiResponse,
|
||||
Gemini,
|
||||
ConvertOpenAIResponsesRequestToGemini,
|
||||
interfaces.TranslateResponse{
|
||||
Stream: ConvertGeminiResponseToOpenAIResponses,
|
||||
NonStream: ConvertGeminiResponseToOpenAIResponsesNonStream,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package responses
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestConvertOpenAIResponsesRequestToGeminiBuildsGenerationConfigWithoutIntermediateObject(t *testing.T) {
|
||||
input := []byte(`{"input":"hello","temperature":0.5,"top_p":0.9,"stop_sequences":["done"],"text":{"format":{"type":"json_schema","schema":{"type":"object"}}}}`)
|
||||
|
||||
output := ConvertOpenAIResponsesRequestToGemini("gemini-test", input, false)
|
||||
|
||||
if got := gjson.GetBytes(output, "generationConfig.temperature").Float(); got != 0.5 {
|
||||
t.Fatalf("temperature = %v, want 0.5", got)
|
||||
}
|
||||
if got := gjson.GetBytes(output, "generationConfig.topP").Float(); got != 0.9 {
|
||||
t.Fatalf("topP = %v, want 0.9", got)
|
||||
}
|
||||
if got := gjson.GetBytes(output, "generationConfig.stopSequences.0").String(); got != "done" {
|
||||
t.Fatalf("stop sequence = %q, want done", got)
|
||||
}
|
||||
if got := gjson.GetBytes(output, "generationConfig.responseMimeType").String(); got != "application/json" {
|
||||
t.Fatalf("responseMimeType = %q, want application/json", got)
|
||||
}
|
||||
if !gjson.GetBytes(output, "generationConfig.responseJsonSchema").Exists() {
|
||||
t.Fatal("responseJsonSchema should be present")
|
||||
}
|
||||
if gjson.GetBytes(output, "generationConfig.responseSchema").Exists() {
|
||||
t.Fatal("responseSchema should not be present")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
package responses
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
sigcompat "github.com/router-for-me/CLIProxyAPI/v7/internal/signature"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
const (
|
||||
geminiResponsesCarrierPrefix = "cpa-gemini-responses-carrier-v1:"
|
||||
geminiResponsesCarrierNext = "next"
|
||||
geminiResponsesCarrierPrevious = "previous"
|
||||
geminiResponsesCarrierStandalone = "standalone"
|
||||
geminiResponsesCarrierText = "text"
|
||||
geminiResponsesCarrierFunction = "function"
|
||||
geminiResponsesCarrierAny = "any"
|
||||
|
||||
geminiResponsesCarrierDirectionField = "_cpa_reasoning_direction"
|
||||
geminiResponsesCarrierTargetField = "_cpa_reasoning_target"
|
||||
geminiResponsesCarrierSignatureField = "_cpa_reasoning_signature"
|
||||
geminiResponsesCarrierSummaryField = "_cpa_reasoning_summary"
|
||||
)
|
||||
|
||||
func encodeGeminiResponsesCarrier(rawSignature, direction, targetKind string) string {
|
||||
rawSignature = strings.TrimSpace(rawSignature)
|
||||
if rawSignature == "" {
|
||||
return ""
|
||||
}
|
||||
return geminiResponsesCarrierPrefix + direction + ":" + targetKind + ":" + base64.RawStdEncoding.EncodeToString([]byte(rawSignature))
|
||||
}
|
||||
|
||||
func decodeGeminiResponsesCarrier(rawSignature string) (signatureValue, direction, targetKind string, marked, ok bool) {
|
||||
rawSignature = strings.TrimSpace(rawSignature)
|
||||
if !strings.HasPrefix(rawSignature, geminiResponsesCarrierPrefix) {
|
||||
return rawSignature, "", "", false, true
|
||||
}
|
||||
marked = true
|
||||
if len(rawSignature) > (sigcompat.MaxGeminiThoughtSignatureLen*4/3)+1024 {
|
||||
return "", "", "", true, false
|
||||
}
|
||||
fields := strings.SplitN(strings.TrimPrefix(rawSignature, geminiResponsesCarrierPrefix), ":", 3)
|
||||
if len(fields) != 3 {
|
||||
return "", "", "", true, false
|
||||
}
|
||||
direction, targetKind = fields[0], fields[1]
|
||||
switch direction {
|
||||
case geminiResponsesCarrierNext, geminiResponsesCarrierPrevious, geminiResponsesCarrierStandalone:
|
||||
default:
|
||||
return "", "", "", true, false
|
||||
}
|
||||
switch targetKind {
|
||||
case geminiResponsesCarrierText, geminiResponsesCarrierFunction, geminiResponsesCarrierAny:
|
||||
default:
|
||||
return "", "", "", true, false
|
||||
}
|
||||
decoded, errDecode := base64.RawStdEncoding.DecodeString(fields[2])
|
||||
if errDecode != nil || len(decoded) == 0 || strings.HasPrefix(string(decoded), geminiResponsesCarrierPrefix) {
|
||||
return "", "", "", true, false
|
||||
}
|
||||
return string(decoded), direction, targetKind, true, true
|
||||
}
|
||||
|
||||
func compatibleGeminiResponsesCarrierSignature(rawSignature, targetKind string) (string, bool) {
|
||||
blockKind := sigcompat.SignatureBlockKindGeminiModelPart
|
||||
if targetKind == geminiResponsesCarrierFunction {
|
||||
blockKind = sigcompat.SignatureBlockKindGeminiFunctionCall
|
||||
}
|
||||
normalized, compatible := sigcompat.CompatibleSignatureForProviderBlock(sigcompat.SignatureProviderGemini, rawSignature, blockKind)
|
||||
if !compatible || sigcompat.IsGeminiThoughtSignatureBypass(sigcompat.SignaturePayloadWithoutProviderPrefix(normalized)) {
|
||||
return "", false
|
||||
}
|
||||
return normalized, true
|
||||
}
|
||||
|
||||
func geminiResponsesCarrierSemanticTarget(item gjson.Result) string {
|
||||
switch item.Get("type").String() {
|
||||
case "function_call", "custom_tool_call":
|
||||
return geminiResponsesCarrierFunction
|
||||
case "reasoning":
|
||||
if strings.TrimSpace(item.Get("summary.0.text").String()) != "" {
|
||||
return geminiResponsesCarrierText
|
||||
}
|
||||
}
|
||||
if _, ok := openAIResponsesAssistantVisibleText(item); ok {
|
||||
return geminiResponsesCarrierText
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func geminiResponsesCarrierMatchesAdjacent(items []gjson.Result, index int, direction, targetKind string) bool {
|
||||
step := 1
|
||||
if direction == geminiResponsesCarrierPrevious {
|
||||
step = -1
|
||||
}
|
||||
for adjacent := index + step; adjacent >= 0 && adjacent < len(items); adjacent += step {
|
||||
if kind := geminiResponsesCarrierSemanticTarget(items[adjacent]); kind != "" {
|
||||
return targetKind == geminiResponsesCarrierAny || targetKind == kind
|
||||
}
|
||||
if !isOpenAIResponsesDetachedCarrier(items[adjacent]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasInternalCarrierFields(item gjson.Result) bool {
|
||||
return item.Get(geminiResponsesCarrierDirectionField).Exists() ||
|
||||
item.Get(geminiResponsesCarrierTargetField).Exists() ||
|
||||
item.Get(geminiResponsesCarrierSignatureField).Exists() ||
|
||||
item.Get(geminiResponsesCarrierSummaryField).Exists()
|
||||
}
|
||||
|
||||
func stripGeminiResponsesCarrierMetadata(rawJSON string) ([]byte, bool) {
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal([]byte(rawJSON), &fields); err != nil {
|
||||
return []byte(rawJSON), false
|
||||
}
|
||||
delete(fields, geminiResponsesCarrierDirectionField)
|
||||
delete(fields, geminiResponsesCarrierTargetField)
|
||||
delete(fields, geminiResponsesCarrierSignatureField)
|
||||
delete(fields, geminiResponsesCarrierSummaryField)
|
||||
stripped, errMarshal := json.Marshal(fields)
|
||||
if errMarshal != nil {
|
||||
return []byte(rawJSON), false
|
||||
}
|
||||
return stripped, true
|
||||
}
|
||||
|
||||
func normalizeGeminiResponsesCarriers(items []gjson.Result) ([]gjson.Result, bool) {
|
||||
normalized := make([]gjson.Result, 0, len(items))
|
||||
hasValidCarrier := false
|
||||
for itemIndex, originalItem := range items {
|
||||
item := originalItem
|
||||
var itemJSON []byte
|
||||
if hasInternalCarrierFields(originalItem) {
|
||||
stripped, ok := stripGeminiResponsesCarrierMetadata(originalItem.Raw)
|
||||
if ok {
|
||||
itemJSON = stripped
|
||||
item = gjson.ParseBytes(itemJSON)
|
||||
}
|
||||
}
|
||||
if item.Get("type").String() != "reasoning" {
|
||||
normalized = append(normalized, item)
|
||||
continue
|
||||
}
|
||||
if len(itemJSON) == 0 {
|
||||
itemJSON = []byte(item.Raw)
|
||||
}
|
||||
rawSignature := strings.TrimSpace(item.Get("encrypted_content").String())
|
||||
signature, direction, targetKind, marked, ok := decodeGeminiResponsesCarrier(rawSignature)
|
||||
if !marked {
|
||||
if rawSignature != "" {
|
||||
_, hasCompatibleRawCarrier := compatibleGeminiResponsesCarrierSignature(rawSignature, geminiResponsesCarrierAny)
|
||||
hasValidCarrier = hasValidCarrier || hasCompatibleRawCarrier
|
||||
}
|
||||
normalized = append(normalized, item)
|
||||
continue
|
||||
}
|
||||
if ok {
|
||||
signature, ok = compatibleGeminiResponsesCarrierSignature(signature, targetKind)
|
||||
}
|
||||
if ok && direction != geminiResponsesCarrierStandalone {
|
||||
ok = geminiResponsesCarrierMatchesAdjacent(items, itemIndex, direction, targetKind)
|
||||
}
|
||||
isDetached := isOpenAIResponsesDetachedCarrier(item)
|
||||
hasSummary := strings.TrimSpace(item.Get("summary.0.text").String()) != ""
|
||||
validSummaryCarrier := hasSummary && ((direction == geminiResponsesCarrierStandalone && (targetKind == geminiResponsesCarrierText || targetKind == geminiResponsesCarrierAny)) || direction == geminiResponsesCarrierNext)
|
||||
if !ok || (!isDetached && !validSummaryCarrier) {
|
||||
if strings.TrimSpace(item.Get("summary.0.text").String()) == "" {
|
||||
continue
|
||||
}
|
||||
itemJSON, _ = sjson.DeleteBytes(itemJSON, "encrypted_content")
|
||||
normalized = append(normalized, gjson.ParseBytes(itemJSON))
|
||||
continue
|
||||
}
|
||||
hasValidCarrier = true
|
||||
itemJSON, _ = sjson.SetBytes(itemJSON, "encrypted_content", signature)
|
||||
itemJSON, _ = sjson.SetBytes(itemJSON, geminiResponsesCarrierDirectionField, direction)
|
||||
itemJSON, _ = sjson.SetBytes(itemJSON, geminiResponsesCarrierTargetField, targetKind)
|
||||
normalized = append(normalized, gjson.ParseBytes(itemJSON))
|
||||
}
|
||||
return normalized, hasValidCarrier
|
||||
}
|
||||
|
||||
func geminiResponsesCarrierDirection(item gjson.Result) string {
|
||||
return item.Get(geminiResponsesCarrierDirectionField).String()
|
||||
}
|
||||
|
||||
func geminiResponsesCarrierTarget(item gjson.Result) string {
|
||||
return item.Get(geminiResponsesCarrierTargetField).String()
|
||||
}
|
||||
|
||||
func isOpenAIResponsesDetachedCarrier(item gjson.Result) bool {
|
||||
return item.Get("type").String() == "reasoning" && strings.TrimSpace(item.Get("encrypted_content").String()) != "" && strings.TrimSpace(item.Get("summary.0.text").String()) == ""
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
package responses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"google.golang.org/protobuf/encoding/protowire"
|
||||
)
|
||||
|
||||
func TestGeminiResponsesCarrierRoundTrip(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
direction string
|
||||
targetKind string
|
||||
}{
|
||||
{geminiResponsesCarrierNext, geminiResponsesCarrierText},
|
||||
{geminiResponsesCarrierPrevious, geminiResponsesCarrierFunction},
|
||||
{geminiResponsesCarrierStandalone, geminiResponsesCarrierAny},
|
||||
} {
|
||||
encoded := encodeGeminiResponsesCarrier(testResponsesGeminiThoughtSignature, testCase.direction, testCase.targetKind)
|
||||
signature, direction, targetKind, marked, ok := decodeGeminiResponsesCarrier(encoded)
|
||||
if !marked || !ok || signature != testResponsesGeminiThoughtSignature || direction != testCase.direction || targetKind != testCase.targetKind {
|
||||
t.Fatalf("carrier round-trip = %q/%q/%q marked=%v ok=%v", signature, direction, targetKind, marked, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeGeminiResponsesCarriersDropsMalformedEnvelope(t *testing.T) {
|
||||
items := gjson.Parse(`[{"type":"reasoning","encrypted_content":"` + geminiResponsesCarrierPrefix + `previous:text:not-base64!","summary":[]},{"type":"message","role":"assistant","content":[{"type":"output_text","text":"safe"}]}]`).Array()
|
||||
normalized, hasCarrier := normalizeGeminiResponsesCarriers(items)
|
||||
if hasCarrier || len(normalized) != 1 || normalized[0].Get("type").String() != "message" || strings.Contains(normalized[0].Raw, geminiResponsesCarrierPrefix) {
|
||||
t.Fatalf("malformed carrier was preserved: %v", normalized)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponsesRequestToGemini_DecodesCarrierForAliasModel(t *testing.T) {
|
||||
carrier := encodeGeminiResponsesCarrier(testResponsesGeminiThoughtSignature, geminiResponsesCarrierNext, geminiResponsesCarrierText)
|
||||
request := []byte(`{"model":"alias-without-provider-name","input":[{"type":"reasoning","encrypted_content":"` + carrier + `","summary":[]},{"type":"message","role":"assistant","content":[{"type":"output_text","text":"answer"}]}]}`)
|
||||
translated := ConvertOpenAIResponsesRequestToGemini("alias-without-provider-name", request, false)
|
||||
part := gjson.GetBytes(translated, "contents.0.parts.0")
|
||||
if part.Get("text").String() != "answer" || part.Get("thoughtSignature").String() != testResponsesGeminiThoughtSignature || strings.Contains(string(translated), geminiResponsesCarrierPrefix) {
|
||||
t.Fatalf("alias model did not decode carrier: %s", translated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeminiResponsesWrappedUUIDFunctionSignatureRoundTrip(t *testing.T) {
|
||||
const providerUUID = "e24830a7-5cd6-42fe-998b-ee539e72b9c3"
|
||||
inner := protowire.AppendTag(nil, 1, protowire.BytesType)
|
||||
inner = protowire.AppendBytes(inner, []byte(providerUUID))
|
||||
outer := protowire.AppendTag(nil, 2, protowire.BytesType)
|
||||
outer = protowire.AppendBytes(outer, inner)
|
||||
signature := base64.StdEncoding.EncodeToString(outer)
|
||||
|
||||
providerResponse := `data: {"response":{"candidates":[{"content":{"role":"model","parts":[{"thoughtSignature":"` + signature + `","functionCall":{"id":"native-call","name":"run","args":{"command":"true"}}}]},"finishReason":"STOP"}],"modelVersion":"gemini-3.6-flash","responseId":"wrapped-uuid"}}`
|
||||
var state any
|
||||
chunks := ConvertGeminiResponseToOpenAIResponses(context.Background(), "gemini-3.6-flash", []byte(`{"model":"alias-without-provider-name"}`), nil, []byte(providerResponse), &state)
|
||||
clientItems := make([]string, 0, 2)
|
||||
callID := ""
|
||||
for _, chunk := range chunks {
|
||||
event, data := parseSSEEvent(t, chunk)
|
||||
if event != "response.output_item.done" {
|
||||
continue
|
||||
}
|
||||
item := data.Get("item")
|
||||
switch item.Get("type").String() {
|
||||
case "reasoning":
|
||||
decoded, direction, targetKind, marked, ok := decodeGeminiResponsesCarrier(item.Get("encrypted_content").String())
|
||||
if !marked || !ok || decoded != signature || direction != geminiResponsesCarrierNext || targetKind != geminiResponsesCarrierFunction {
|
||||
t.Fatalf("provider signature carrier = marked:%v ok:%v direction:%q target:%q", marked, ok, direction, targetKind)
|
||||
}
|
||||
clientItems = append(clientItems, item.Raw)
|
||||
case "function_call":
|
||||
callID = item.Get("call_id").String()
|
||||
clientItems = append(clientItems, item.Raw)
|
||||
}
|
||||
}
|
||||
if len(clientItems) != 2 || callID == "" {
|
||||
t.Fatalf("Responses client items = %v, call ID present=%v", clientItems, callID != "")
|
||||
}
|
||||
clientItems = append(clientItems, `{"type":"function_call_output","call_id":`+strconv.Quote(callID)+`,"output":"ok"}`)
|
||||
request := []byte(`{"model":"alias-without-provider-name","input":[` + strings.Join(clientItems, ",") + `]}`)
|
||||
|
||||
translated := ConvertOpenAIResponsesRequestToGemini("alias-without-provider-name", request, false)
|
||||
var functionPart gjson.Result
|
||||
gjson.GetBytes(translated, "contents").ForEach(func(_, content gjson.Result) bool {
|
||||
content.Get("parts").ForEach(func(_, part gjson.Result) bool {
|
||||
if part.Get("functionCall").Exists() {
|
||||
functionPart = part
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return !functionPart.Exists()
|
||||
})
|
||||
if !functionPart.Exists() || functionPart.Get("functionCall.name").String() != "run" || functionPart.Get("functionCall.args.command").String() != "true" {
|
||||
t.Fatalf("function carrier did not bind to the native call: %s", translated)
|
||||
}
|
||||
if got := functionPart.Get("thoughtSignature").String(); got != signature || got == geminiResponsesThoughtSignature {
|
||||
t.Fatalf("function signature = %q, want provider-native wrapped UUID signature", got)
|
||||
}
|
||||
if strings.Contains(string(translated), geminiResponsesCarrierPrefix) {
|
||||
t.Fatalf("carrier envelope reached Gemini: %s", translated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponsesRequestToGemini_DecodesLegacyRawCarrierForAliasModel(t *testing.T) {
|
||||
request := []byte(`{"model":"alias-without-provider-name","input":[{"type":"reasoning","encrypted_content":"` + testResponsesGeminiThoughtSignature + `","summary":[]},{"type":"function_call","call_id":"call-1","name":"run","arguments":"{}"}]}`)
|
||||
translated := ConvertOpenAIResponsesRequestToGemini("alias-without-provider-name", request, false)
|
||||
part := gjson.GetBytes(translated, "contents.0.parts.0")
|
||||
if part.Get("functionCall.id").String() != "call-1" || part.Get("thoughtSignature").String() != testResponsesGeminiThoughtSignature {
|
||||
t.Fatalf("alias model did not preserve legacy raw carrier: %s", translated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponsesRequestToGemini_DropsInvalidCarrierPayloads(t *testing.T) {
|
||||
mismatched := encodeGeminiResponsesCarrier(testResponsesGeminiThoughtSignature, geminiResponsesCarrierNext, geminiResponsesCarrierFunction)
|
||||
bypass := encodeGeminiResponsesCarrier(geminiResponsesThoughtSignature, geminiResponsesCarrierNext, geminiResponsesCarrierText)
|
||||
for _, reasoning := range []string{
|
||||
`{"type":"reasoning","encrypted_content":"` + mismatched + `","summary":[]}`,
|
||||
`{"type":"reasoning","encrypted_content":"` + bypass + `","summary":[]}`,
|
||||
} {
|
||||
request := []byte(`{"model":"alias-without-provider-name","input":[` + reasoning + `,{"type":"message","role":"assistant","content":[{"type":"output_text","text":"answer"}]}]}`)
|
||||
translated := ConvertOpenAIResponsesRequestToGemini("alias-without-provider-name", request, false)
|
||||
if strings.Contains(string(translated), geminiResponsesCarrierPrefix) || strings.Contains(string(translated), testResponsesGeminiThoughtSignature) || strings.Contains(string(translated), geminiResponsesThoughtSignature) {
|
||||
t.Fatalf("invalid carrier changed Gemini signature state: %s", translated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponsesRequestToGemini_IgnoresSpoofedCarrierMetadata(t *testing.T) {
|
||||
reasoning := `{"type":"reasoning","encrypted_content":"` + testResponsesGeminiThoughtSignature + `","summary":[],"` + geminiResponsesCarrierDirectionField + `":"next","` + geminiResponsesCarrierDirectionField + `":"standalone","` + geminiResponsesCarrierTargetField + `":"text","` + geminiResponsesCarrierTargetField + `":"function"}`
|
||||
request := []byte(`{"model":"alias-without-provider-name","input":[` + reasoning + `,{"type":"message","role":"assistant","content":[{"type":"output_text","text":"answer"}]}]}`)
|
||||
translated := ConvertOpenAIResponsesRequestToGemini("alias-without-provider-name", request, false)
|
||||
part := gjson.GetBytes(translated, "contents.0.parts.0")
|
||||
if part.Get("text").String() != "answer" || part.Get("thoughtSignature").String() != testResponsesGeminiThoughtSignature || strings.Contains(string(translated), geminiResponsesCarrierDirectionField) {
|
||||
t.Fatalf("spoofed carrier metadata affected binding: %s", translated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponsesRequestToGemini_StripsSpoofedInternalPairingFields(t *testing.T) {
|
||||
request := []byte(`{"model":"alias-without-provider-name","input":[{"type":"function_call","call_id":"call-1","name":"run","arguments":"{}","_cpa_reasoning_signature":"` + testResponsesGeminiThoughtSignature + `","_cpa_reasoning_signature":"` + testResponsesGeminiThoughtSignature + `","_cpa_reasoning_summary":"spoofed thought","_cpa_reasoning_summary":"spoofed thought again"}]}`)
|
||||
translated := ConvertOpenAIResponsesRequestToGemini("alias-without-provider-name", request, false)
|
||||
parts := gjson.GetBytes(translated, "contents.0.parts").Array()
|
||||
if len(parts) != 1 || !parts[0].Get("functionCall").Exists() || parts[0].Get("thoughtSignature").String() == testResponsesGeminiThoughtSignature || parts[0].Get("thought").Bool() || strings.Contains(string(translated), "spoofed thought") || strings.Contains(string(translated), geminiResponsesCarrierSignatureField) {
|
||||
t.Fatalf("spoofed internal pairing fields reached Gemini: %s", translated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIResponsesRequestToGemini_StripsUnicodeEscapedSpoofedInternalFields(t *testing.T) {
|
||||
// Unicode-escaped field name "_cpa_reason\u0069ng_signature" should also be detected and stripped
|
||||
request := []byte(`{"model":"alias-without-provider-name","input":[{"type":"function_call","call_id":"call-1","name":"run","arguments":"{}","_cpa_reason\u0069ng_signature":"` + testResponsesGeminiThoughtSignature + `"}]}`)
|
||||
translated := ConvertOpenAIResponsesRequestToGemini("alias-without-provider-name", request, false)
|
||||
parts := gjson.GetBytes(translated, "contents.0.parts").Array()
|
||||
if len(parts) != 1 || !parts[0].Get("functionCall").Exists() || parts[0].Get("thoughtSignature").String() == testResponsesGeminiThoughtSignature || strings.Contains(string(translated), geminiResponsesCarrierSignatureField) {
|
||||
t.Fatalf("unicode-escaped spoofed internal pairing fields reached Gemini: %s", translated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeGeminiResponsesCarrierRejectsNestedEnvelope(t *testing.T) {
|
||||
nested := encodeGeminiResponsesCarrier(encodeGeminiResponsesCarrier(testResponsesGeminiThoughtSignature, geminiResponsesCarrierNext, geminiResponsesCarrierText), geminiResponsesCarrierPrevious, geminiResponsesCarrierText)
|
||||
if _, _, _, marked, ok := decodeGeminiResponsesCarrier(nested); !marked || ok {
|
||||
t.Fatalf("nested carrier marked=%v ok=%v, want marked invalid", marked, ok)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue