iop/apps/edge/internal/openai/provider_model_rewrite_test.go

544 lines
26 KiB
Go

package openai
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
edgeservice "iop/apps/edge/internal/service"
"iop/packages/go/config"
)
func TestGeminiChatProviderThoughtSignatureRoundTrip(t *testing.T) {
response := []byte(`{"id":"chat-1","model":"served","choices":[{"message":{"role":"assistant","content":null,"tool_calls":[{"id":"call-1","type":"function","function":{"name":"glob","arguments":"{\"pattern\":\"*\"}"},"extra_content":{"google":{"thought_signature":"opaque-signature"}}}]},"finish_reason":"tool_calls"}]}`)
rewritten := rewriteProviderJSONResponse(response, "public-gemini", config.ProtocolToolCallWireGeminiChat)
var decoded map[string]any
if err := json.Unmarshal(rewritten, &decoded); err != nil {
t.Fatal(err)
}
choice := anySlice(decoded["choices"])[0].(map[string]any)
message := choice["message"].(map[string]any)
call := anySlice(message["tool_calls"])[0].(map[string]any)
encodedID := call["id"].(string)
if !strings.HasPrefix(encodedID, geminiThoughtSignatureToolIDPrefix) {
t.Fatalf("thought signature was not encoded in tool id: %+v", call)
}
if _, exists := call["extra_content"]; exists {
t.Fatalf("Gemini extension leaked after normalization: %+v", call)
}
if decoded["model"] != "public-gemini" {
t.Fatalf("model echo was not preserved through normalization: %+v", decoded)
}
request, err := normalizeGeminiChatProviderRequest([]byte(`{"model":"served","messages":[{"role":"assistant","content":null,"tool_calls":[{"id":"` + encodedID + `","type":"function","function":{"name":"glob","arguments":"{\"pattern\":\"*\"}"}}]},{"role":"tool","tool_call_id":"` + encodedID + `","content":"[]"}]}`))
if err != nil {
t.Fatal(err)
}
var replay map[string]any
if err := json.Unmarshal(request, &replay); err != nil {
t.Fatal(err)
}
messages := anySlice(replay["messages"])
replayedCall := anySlice(messages[0].(map[string]any)["tool_calls"])[0].(map[string]any)
if replayedCall["id"] != "call-1" || messages[1].(map[string]any)["tool_call_id"] != "call-1" {
t.Fatalf("original tool ids were not restored: %+v", messages)
}
extra := replayedCall["extra_content"].(map[string]any)
google := extra["google"].(map[string]any)
if google["thought_signature"] != "opaque-signature" {
t.Fatalf("thought signature was not restored: %+v", replayedCall)
}
}
func TestProviderThoughtSignatureNormalizationIsGeminiProfileOnly(t *testing.T) {
response := []byte(`{"model":"served","choices":[{"message":{"tool_calls":[{"id":"call-1","extra_content":{"google":{"thought_signature":"opaque"}}}]}}]}`)
if got := rewriteProviderJSONResponse(response, "public", ""); !strings.Contains(string(got), `"id":"call-1"`) || !strings.Contains(string(got), `"thought_signature":"opaque"`) {
t.Fatalf("non-Gemini response was normalized: %s", got)
}
profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
if err != nil {
t.Fatal(err)
}
baseBody := []byte(`{"model":"served","messages":[{"role":"user","content":"hi"}]}`)
tunnel := edgeservice.SubmitProviderTunnelRequest{BuildBody: func(string) ([]byte, error) { return baseBody, nil }}
prepared, err := prepareProviderChatToolCallNormalization(tunnel, edgeservice.ProviderPoolCandidate{ProtocolProfile: &profile})
if err != nil {
t.Fatal(err)
}
got, err := prepared.BuildBody("served")
if err != nil || string(got) != string(baseBody) {
t.Fatalf("OpenAI profile body changed: %s err=%v", got, err)
}
}
func TestGeminiChatProviderResponseAddsMissingParallelToolIndices(t *testing.T) {
response := []byte(`{"id":"chat-1","choices":[{"delta":{"tool_calls":[{"id":"call-1","function":{"name":"read_file","arguments":"{\"path\":\"plan.md\"}"}},{"id":"call-2","function":{"name":"read_file","arguments":"{\"path\":\"review.md\"}"}},{"id":"call-3","function":{"name":"bash","arguments":"{\"command\":\"test -f index.html\"}"}}]}}]}`)
normalized := normalizeGeminiChatProviderResponse(response)
var body map[string]any
if err := json.Unmarshal(normalized, &body); err != nil {
t.Fatal(err)
}
choice := anySlice(body["choices"])[0].(map[string]any)
delta := choice["delta"].(map[string]any)
calls := anySlice(delta["tool_calls"])
for index, raw := range calls {
call := raw.(map[string]any)
if got := int(call["index"].(float64)); got != index {
t.Fatalf("tool call %d index = %d", index, got)
}
}
stage, err := decodeOpenAIPresetSSE([]byte("data: " + string(normalized) + "\n\ndata: {\"id\":\"chat-1\",\"choices\":[{\"delta\":{},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":1,\"total_tokens\":2}}\n\ndata: [DONE]\n\n"))
if err != nil {
t.Fatal(err)
}
if len(stage.ToolCalls) != 3 {
t.Fatalf("decoded tool calls = %+v", stage.ToolCalls)
}
}
func TestOpenAIPresetSSESeparatesNewToolIDsWithoutIndices(t *testing.T) {
body := []byte("data: {\"id\":\"chat-1\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"id\":\"call-1\",\"function\":{\"name\":\"read_file\",\"arguments\":\"{\\\"path\\\":\\\"plan.md\\\"}\"}}]}}]}\n\n" +
"data: {\"id\":\"chat-1\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"id\":\"call-2\",\"function\":{\"name\":\"read_file\",\"arguments\":\"{\\\"path\\\":\\\"review.md\\\"}\"}}]}}]}\n\n" +
"data: {\"id\":\"chat-1\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"id\":\"call-3\",\"function\":{\"name\":\"bash\",\"arguments\":\"{\\\"command\\\":\\\"test -f index.html\\\"}\"}}]}}]}\n\n" +
"data: {\"id\":\"chat-1\",\"choices\":[{\"delta\":{},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":1,\"total_tokens\":2}}\n\ndata: [DONE]\n\n")
stage, err := decodeOpenAIPresetSSE(body)
if err != nil {
t.Fatal(err)
}
if len(stage.ToolCalls) != 3 {
t.Fatalf("decoded tool calls = %+v", stage.ToolCalls)
}
}
func TestOpenAIPresetSSECarriesLateGeminiThoughtSignature(t *testing.T) {
body := []byte("data: {\"id\":\"chat-1\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"call-1\",\"function\":{\"name\":\"read_file\",\"arguments\":\"{\\\"path\\\":\\\"plan.md\\\"}\"}}]}}]}\n\n" +
"data: {\"id\":\"chat-1\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"extra_content\":{\"google\":{\"thought_signature\":\"late-signature\"}},\"function\":{}}]}}]}\n\n" +
"data: {\"id\":\"chat-1\",\"choices\":[{\"delta\":{},\"finish_reason\":\"tool_calls\"}],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":1,\"total_tokens\":2}}\n\ndata: [DONE]\n\n")
stage, err := decodeOpenAIPresetSSE(body)
if err != nil {
t.Fatal(err)
}
if len(stage.ToolCalls) != 1 || !strings.HasPrefix(stage.ToolCalls[0].ProviderCallID, geminiThoughtSignatureToolIDPrefix) {
t.Fatalf("late Gemini signature was not retained: %+v", stage.ToolCalls)
}
}
func TestProviderChatTokenLimitNormalizationUsesSelectedProfile(t *testing.T) {
tests := []struct {
name string
profileID string
body string
wantField string
wantAbsent string
want float64
}{
{name: "generic max tokens to OpenAI completion field", profileID: "openai", body: `{"model":"served","max_tokens":32000,"future":{"keep":true}}`, wantField: "max_completion_tokens", wantAbsent: "max_tokens", want: 32000},
{name: "OpenAI native field wins", profileID: "openai", body: `{"model":"served","max_tokens":8,"max_completion_tokens":16}`, wantField: "max_completion_tokens", wantAbsent: "max_tokens", want: 16},
{name: "completion field to Gemini legacy field", profileID: "gemini", body: `{"model":"served","max_completion_tokens":2048}`, wantField: "max_tokens", wantAbsent: "max_completion_tokens", want: 2048},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
profile, err := config.ResolveProtocolProfile(tc.profileID, "", config.BuiltInProtocolProfileCatalog())
if err != nil {
t.Fatal(err)
}
tunnel := edgeservice.SubmitProviderTunnelRequest{
Operation: string(config.OperationChatCompletions),
BuildBody: func(string) ([]byte, error) { return []byte(tc.body), nil },
}
prepared, err := prepareProviderChatRequestNormalization(tunnel, edgeservice.ProviderPoolCandidate{ProtocolProfile: &profile})
if err != nil {
t.Fatal(err)
}
body, err := prepared.BuildBody("served")
if err != nil {
t.Fatal(err)
}
var got map[string]any
if err := json.Unmarshal(body, &got); err != nil {
t.Fatal(err)
}
if got[tc.wantField] != tc.want {
t.Fatalf("%s=%v, want %v; body=%s", tc.wantField, got[tc.wantField], tc.want, body)
}
if _, ok := got[tc.wantAbsent]; ok {
t.Fatalf("%s survived normalization: %s", tc.wantAbsent, body)
}
if strings.Contains(tc.body, `"future"`) && !strings.Contains(string(body), `"future":{"keep":true}`) {
t.Fatalf("unknown provider field changed: %s", body)
}
})
}
}
func TestGeminiChatProviderStreamingThoughtSignatureNormalization(t *testing.T) {
line := []byte("data: {\"model\":\"served\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"id\":\"call-1\",\"type\":\"function\",\"function\":{\"name\":\"glob\",\"arguments\":\"{}\"},\"extra_content\":{\"google\":{\"thought_signature\":\"opaque\"}}}]}}]}\n\n")
rewriter := newProviderModelRewriterWithToolCallWire(true, "public", config.ProtocolToolCallWireGeminiChat)
got := append(rewriter.AppendStream(line), rewriter.FlushStream()...)
if strings.Contains(string(got), "thought_signature") || !strings.Contains(string(got), geminiThoughtSignatureToolIDPrefix) {
t.Fatalf("streaming signature was not normalized: %s", got)
}
}
func TestGeminiChatProviderRejectsMalformedOpaqueToolID(t *testing.T) {
_, err := normalizeGeminiChatProviderRequest([]byte(`{"messages":[{"role":"assistant","tool_calls":[{"id":"iop_gts_not-base64"}]}]}`))
if err == nil {
t.Fatal("malformed IOP Gemini tool id must fail closed")
}
}
func TestOpenAIChatProviderHTTPNormalizesGenericTokenLimit(t *testing.T) {
var providerRequest map[string]any
provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
if err := json.Unmarshal(body, &providerRequest); err != nil {
t.Errorf("decode provider request: %v", err)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"chat-openai","model":"served-openai","choices":[{"index":0,"message":{"role":"assistant","content":"done"},"finish_reason":"stop"}]}`))
}))
defer provider.Close()
profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
if err != nil {
t.Fatal(err)
}
fake := &providerFakeRunService{
poolDispatchPath: string(edgeservice.ProviderPoolPathTunnel),
tunnelProviderURL: provider.URL,
tunnelServedTarget: "served-openai",
poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
ActualModel: "served-openai", ProviderID: "openai-provider",
ExecutionPath: string(edgeservice.ProviderPoolPathTunnel), ProfileID: profile.ID,
ProfileDriver: string(profile.Driver), ProfileCapabilities: append([]string(nil), profile.Capabilities...),
ProtocolProfile: &profile,
},
}
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "openai-route", Providers: map[string]string{"openai-provider": "served-openai"}}})
request := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{"model":"openai-route","messages":[{"role":"user","content":"hello"}],"max_tokens":32000,"future":{"keep":true}}`))
response := httptest.NewRecorder()
srv.handleChatCompletions(response, request)
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"content":"done"`) {
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
}
if providerRequest["max_completion_tokens"] != float64(32000) {
t.Fatalf("max_completion_tokens=%v, request=%+v", providerRequest["max_completion_tokens"], providerRequest)
}
if _, ok := providerRequest["max_tokens"]; ok {
t.Fatalf("legacy max_tokens reached OpenAI provider: %+v", providerRequest)
}
if future, ok := providerRequest["future"].(map[string]any); !ok || future["keep"] != true {
t.Fatalf("unknown provider field changed: %+v", providerRequest)
}
}
func TestHotPathOpenAIToolsAndEffortUseResponsesOperation(t *testing.T) {
profile, err := config.ResolveProtocolProfile("openai", "", config.BuiltInProtocolProfileCatalog())
if err != nil {
t.Fatal(err)
}
tunnel := edgeservice.SubmitProviderTunnelRequest{
Operation: string(config.OperationChatCompletions),
Path: "/v1/chat/completions",
BuildBody: func(target string) ([]byte, error) {
return []byte(`{"model":"` + target + `","messages":[{"role":"system","content":"inspect"},{"role":"user","content":"work"}],"tools":[{"type":"function","function":{"name":"bash","parameters":{"type":"object"}}}],"max_tokens":32000,"reasoning_effort":"high","temperature":0,"top_p":1,"stream":true}`), nil
},
}
prepared, err := prepareHotPathChatProviderOperation(tunnel, edgeservice.ProviderPoolCandidate{ProtocolProfile: &profile}, providerRequestRequirements{
HasTools: true, Stream: true, Effort: "high",
})
if err != nil {
t.Fatal(err)
}
if prepared.Operation != string(config.OperationResponses) || prepared.Path != "/v1/responses" {
t.Fatalf("operation=%q path=%q", prepared.Operation, prepared.Path)
}
if prepared.Stream {
t.Fatal("buffered internal Responses operation retained streaming tunnel metadata")
}
body, err := prepared.BuildBody("gpt-5.6-terra")
if err != nil {
t.Fatal(err)
}
var request map[string]any
if err := json.Unmarshal(body, &request); err != nil {
t.Fatal(err)
}
if request["max_output_tokens"] != float64(32000) || request["messages"] != nil || request["max_tokens"] != nil {
t.Fatalf("Responses token/message conversion mismatch: %+v", request)
}
reasoning, _ := request["reasoning"].(map[string]any)
if reasoning["effort"] != "high" || len(anySlice(request["tools"])) != 1 {
t.Fatalf("Responses effort/tools conversion mismatch: %+v", request)
}
if request["temperature"] != nil || request["top_p"] != nil {
t.Fatalf("reasoning Responses request retained incompatible sampling controls: %+v", request)
}
}
func TestChatProviderRequirementsAllowPresetOwnedEffort(t *testing.T) {
requirements := chatProviderRequirements(chatCompletionRequest{Tools: []any{map[string]any{"type": "function"}}})
if requirements.Effort != "" || !requirements.HasTools {
t.Fatalf("caller requirements=%+v", requirements)
}
presetOptions := map[string]any{"reasoning_effort": "high"}
if effort, ok := presetOptions["reasoning_effort"].(string); ok {
requirements.Effort = strings.TrimSpace(effort)
}
if requirements.Effort != "high" || !requirements.HasTools {
t.Fatalf("effective selector requirements=%+v", requirements)
}
}
func TestHotPathSelectorOutputLimitIsBounded(t *testing.T) {
tunnel := edgeservice.SubmitProviderTunnelRequest{
BuildBody: func(string) ([]byte, error) {
return []byte(`{"model":"selector","max_completion_tokens":32000,"future":true}`), nil
},
}
prepared, err := prepareHotPathSelectorOutputLimit(tunnel)
if err != nil {
t.Fatal(err)
}
body, err := prepared.BuildBody("selector")
if err != nil {
t.Fatal(err)
}
var request map[string]any
if err := json.Unmarshal(body, &request); err != nil {
t.Fatal(err)
}
if request["max_tokens"] != float64(maxHotPathSelectorOutputTokens) || request["max_completion_tokens"] != nil || request["future"] != true {
t.Fatalf("bounded selector request=%+v", request)
}
}
func TestHotPathSelectorPairInstructionCarriesCompactPlanContract(t *testing.T) {
instruction, err := buildHotPathSelectorProviderInstruction("req_compact_plan", selectorInstructionPairWrite)
if err != nil {
t.Fatal(err)
}
for _, fragment := range []string{
"Analyze the immutable user task",
"Keep every explicit requirement, exact literal, filename, command, output string, and acceptance condition.",
"1-5 executable worker steps",
"1-3 observable verification checks",
"Do not write markdown; IOP renders the fixed Plan and pending Review templates.",
"Task outputs belong under the caller workspace current working directory",
"IOP will append the final Review handoff step and matching pending status itself.",
} {
if !strings.Contains(instruction, fragment) {
t.Fatalf("selector instruction omitted compact Plan contract %q: %s", fragment, instruction)
}
}
if strings.Contains(instruction, "deterministic seed") || strings.Contains(instruction, "exactly following two-step") {
t.Fatalf("selector instruction retained seed-oriented Plan guidance: %s", instruction)
}
}
func TestOpenAIHotPathResponsesOperationUsesResponsesDecoder(t *testing.T) {
body := []byte(`{"id":"resp-selector","model":"gpt-5.6-terra","status":"completed","output":[{"type":"function_call","id":"fc-1","call_id":"call-1","name":"bash","arguments":"{\"command\":\"true\"}"}],"usage":{"input_tokens":10,"output_tokens":3}}`)
stage, err := decodePresetTunnelBody(body, "application/json", "openai", string(config.OperationResponses), string(config.ProtocolDriverOpenAIChat))
if err != nil {
t.Fatal(err)
}
if stage.ResponseID != "resp-selector" || len(stage.ToolCalls) != 1 || stage.ToolCalls[0].Name != "bash" {
t.Fatalf("Responses stage=%+v", stage)
}
}
func TestHotPathSelectorCanonicalWriteToolReplacesCallerCommandSchema(t *testing.T) {
preset := config.ExecutionPreset{WorkspaceTools: []config.ExecutionWorkspaceToolAlternative{{
Operations: map[string]config.ExecutionWorkspaceOperation{
"write": {ToolName: "bash"},
},
}}}
tunnel := edgeservice.SubmitProviderTunnelRequest{BuildBody: func(string) ([]byte, error) {
return []byte(`{"model":"selector","tools":[{"type":"function","function":{"name":"bash","description":"shell","parameters":{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}}},{"type":"function","function":{"name":"webfetch","parameters":{"type":"object"}}}]}`), nil
}}
prepared, err := prepareHotPathSelectorCanonicalTools(tunnel, "Operation: pair-write", preset)
if err != nil {
t.Fatal(err)
}
body, err := prepared.BuildBody("selector")
if err != nil {
t.Fatal(err)
}
var request map[string]any
if err := json.Unmarshal(body, &request); err != nil {
t.Fatal(err)
}
tools := anySlice(request["tools"])
if len(tools) != 1 {
t.Fatalf("canonical tools=%+v", tools)
}
function := tools[0].(map[string]any)["function"].(map[string]any)
parameters := function["parameters"].(map[string]any)
properties := parameters["properties"].(map[string]any)
if function["name"] != hotPathArtifactPairToolName || properties["goal"] == nil || properties["steps"] == nil || properties["verification"] == nil || properties["command"] != nil {
t.Fatalf("canonical write function=%+v", function)
}
if request["tool_choice"] != "required" || request["parallel_tool_calls"] != nil {
t.Fatalf("pair-write must force one atomic tool call: %+v", request)
}
}
func TestAnthropicCallerWorkspaceSelectorUsesCanonicalProviderOperation(t *testing.T) {
preset := config.ExecutionPreset{
Selector: config.ExecutionModelBinding{Options: map[string]any{"reasoning_effort": "high"}},
WorkspaceTools: []config.ExecutionWorkspaceToolAlternative{{
Name: "claude-code-bash",
Operations: map[string]config.ExecutionWorkspaceOperation{
"write": {ToolName: "Bash"},
},
}},
}
instruction, err := buildHotPathSelectorProviderInstruction("req_anthropic_selector", selectorInstructionPairWrite)
if err != nil {
t.Fatal(err)
}
callerBody := []byte(`{"model":"gpt-hybrid","max_tokens":32000,"messages":[{"role":"user","content":"build it"}],"tools":[{"name":"Bash","description":"shell","input_schema":{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}}],"stream":true}`)
for _, profileID := range []string{"gemini", "openai"} {
t.Run(profileID, func(t *testing.T) {
profile, err := config.ResolveProtocolProfile(profileID, "", config.BuiltInProtocolProfileCatalog())
if err != nil {
t.Fatal(err)
}
selected := edgeservice.ProviderPoolCandidate{ProtocolProfile: &profile}
tunnel := edgeservice.SubmitProviderTunnelRequest{Stream: true}
prepared, err := prepareAnthropicCallerWorkspaceSelectorTunnel(
tunnel, selected, callerBody, profile, instruction, preset,
providerRequestRequirements{HasTools: true, Stream: true, Effort: "high"},
)
if err != nil {
t.Fatal(err)
}
body, err := prepared.BuildBody("served-selector")
if err != nil {
t.Fatal(err)
}
var request map[string]any
if err := json.Unmarshal(body, &request); err != nil {
t.Fatal(err)
}
tools := anySlice(request["tools"])
if len(tools) != 1 {
t.Fatalf("canonical tools=%+v", tools)
}
if profileID == "openai" {
if prepared.Operation != string(config.OperationResponses) || prepared.Path != "/v1/responses" || prepared.Stream {
t.Fatalf("OpenAI selector tunnel=%+v", prepared)
}
tool := tools[0].(map[string]any)
if tool["name"] != hotPathArtifactPairToolName || request["tool_choice"] != "required" || request["max_output_tokens"] != float64(maxHotPathSelectorOutputTokens) {
t.Fatalf("OpenAI selector request=%+v", request)
}
if !strings.Contains(request["instructions"].(string), "Operation: pair-write") {
t.Fatalf("OpenAI selector instruction=%v", request["instructions"])
}
return
}
if prepared.Operation != string(config.OperationChatCompletions) || prepared.Path != "/v1/chat/completions" {
t.Fatalf("Gemini selector tunnel=%+v", prepared)
}
function := tools[0].(map[string]any)["function"].(map[string]any)
if function["name"] != hotPathArtifactPairToolName || request["tool_choice"] != "required" || request["max_tokens"] != float64(maxHotPathSelectorOutputTokens) {
t.Fatalf("Gemini selector request=%+v", request)
}
messages := anySlice(request["messages"])
if len(messages) < 2 || !strings.Contains(messages[0].(map[string]any)["content"].(string), "Operation: pair-write") {
t.Fatalf("Gemini selector messages=%+v", messages)
}
})
}
}
func TestGeminiChatProviderHTTPToolContinuationRoundTrip(t *testing.T) {
var providerRequests []map[string]any
provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var request map[string]any
if err := json.Unmarshal(body, &request); err != nil {
t.Errorf("decode provider request: %v", err)
w.WriteHeader(http.StatusBadRequest)
return
}
providerRequests = append(providerRequests, request)
w.Header().Set("Content-Type", "application/json")
if len(providerRequests) == 1 {
_, _ = w.Write([]byte(`{"id":"chat-1","model":"served-gemini","choices":[{"index":0,"message":{"role":"assistant","content":null,"tool_calls":[{"id":"call-1","type":"function","function":{"name":"glob","arguments":"{\"pattern\":\"*\"}"},"extra_content":{"google":{"thought_signature":"opaque-signature"}}}]},"finish_reason":"tool_calls"}]}`))
return
}
_, _ = w.Write([]byte(`{"id":"chat-2","model":"served-gemini","choices":[{"index":0,"message":{"role":"assistant","content":"done"},"finish_reason":"stop"}]}`))
}))
defer provider.Close()
profile, err := config.ResolveProtocolProfile("gemini", "", config.BuiltInProtocolProfileCatalog())
if err != nil {
t.Fatal(err)
}
fake := &providerFakeRunService{
poolDispatchPath: string(edgeservice.ProviderPoolPathTunnel),
tunnelProviderURL: provider.URL,
tunnelServedTarget: "served-gemini",
poolSelectedCandidate: edgeservice.ProviderPoolCandidate{
ActualModel: "served-gemini", ProviderID: "gemini-provider",
ExecutionPath: string(edgeservice.ProviderPoolPathTunnel), ProfileID: profile.ID,
ProfileDriver: string(profile.Driver), ProfileCapabilities: append([]string(nil), profile.Capabilities...),
ProtocolProfile: &profile,
},
}
srv := NewServer(config.EdgeOpenAIConf{}, fake, nil)
srv.SetModelCatalog([]config.ModelCatalogEntry{{ID: "gemini-route", Providers: map[string]string{"gemini-provider": "served-gemini"}}})
first := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(`{"model":"gemini-route","messages":[{"role":"user","content":"list files"}],"tools":[{"type":"function","function":{"name":"glob","parameters":{"type":"object"}}}]}`))
firstResponse := httptest.NewRecorder()
srv.handleChatCompletions(firstResponse, first)
if firstResponse.Code != http.StatusOK {
t.Fatalf("first status=%d body=%s", firstResponse.Code, firstResponse.Body.String())
}
if got := fake.lastTunnelHandle.Dispatch().ProfileToolCallWire; got != config.ProtocolToolCallWireGeminiChat {
t.Fatalf("selected dispatch tool-call wire=%q", got)
}
var firstBody map[string]any
if err := json.Unmarshal(firstResponse.Body.Bytes(), &firstBody); err != nil {
t.Fatal(err)
}
firstChoice := anySlice(firstBody["choices"])[0].(map[string]any)
firstMessage := firstChoice["message"].(map[string]any)
encodedID := anySlice(firstMessage["tool_calls"])[0].(map[string]any)["id"].(string)
if !strings.HasPrefix(encodedID, geminiThoughtSignatureToolIDPrefix) {
t.Fatalf("caller did not receive opaque normalized id: %s", firstResponse.Body.String())
}
secondBody := `{"model":"gemini-route","messages":[{"role":"user","content":"list files"},{"role":"assistant","content":null,"tool_calls":[{"id":"` + encodedID + `","type":"function","function":{"name":"glob","arguments":"{\"pattern\":\"*\"}"}}]},{"role":"tool","tool_call_id":"` + encodedID + `","content":"[]"}]}`
second := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", strings.NewReader(secondBody))
secondResponse := httptest.NewRecorder()
srv.handleChatCompletions(secondResponse, second)
if secondResponse.Code != http.StatusOK || !strings.Contains(secondResponse.Body.String(), `"content":"done"`) {
t.Fatalf("second status=%d body=%s", secondResponse.Code, secondResponse.Body.String())
}
if len(providerRequests) != 2 {
t.Fatalf("provider requests=%d", len(providerRequests))
}
messages := anySlice(providerRequests[1]["messages"])
replayedCall := anySlice(messages[1].(map[string]any)["tool_calls"])[0].(map[string]any)
if replayedCall["id"] != "call-1" || messages[2].(map[string]any)["tool_call_id"] != "call-1" {
t.Fatalf("provider did not receive original tool ids: %+v", messages)
}
google := replayedCall["extra_content"].(map[string]any)["google"].(map[string]any)
if google["thought_signature"] != "opaque-signature" {
t.Fatalf("provider did not receive restored signature: %+v", replayedCall)
}
}