Files
ollama/internal/proxy/codex_desktop_test.go

2186 lines
81 KiB
Go

package proxy
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"slices"
"strings"
"testing"
"time"
"github.com/klauspost/compress/zstd"
"github.com/ollama/ollama/model/renderers"
"github.com/ollama/ollama/openai"
modelpkg "github.com/ollama/ollama/types/model"
)
func TestCodexDesktopRoutesCatalogModelToOllamaAndStripsCredentials(t *testing.T) {
var gotPath, gotQuery, gotAuthorization, gotEncoding, gotMetadata string
var gotBody []byte
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotQuery = r.URL.RawQuery
gotAuthorization = r.Header.Get("Authorization")
gotEncoding = r.Header.Get("Content-Encoding")
gotMetadata = r.Header.Get("X-Codex-Turn-Metadata")
gotBody, _ = io.ReadAll(r.Body)
w.Header().Set("Content-Type", "text/event-stream")
_, _ = io.WriteString(w, "data: routed\n\n")
}))
defer ollama.Close()
chatGPT := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("catalog model should not reach ChatGPT")
}))
defer chatGPT.Close()
catalogPath := writeCatalog(t, "glm-5.2:cloud")
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL+"/backend-api/codex", catalogPath)
proxy := httptest.NewServer(handler)
defer proxy.Close()
payload := []byte(`{"model":"glm-5.2:cloud","stream":true,"reasoning":{"effort":"high"},"tools":[{"type":"web_search","external_web_access":false}]}`)
encoder, err := zstd.NewWriter(nil)
if err != nil {
t.Fatal(err)
}
compressed := encoder.EncodeAll(payload, nil)
encoder.Close()
for _, authorization := range []string{"Bearer chatgpt-secret", "Bearer " + CodexDesktopManagedAPIKey} {
req, err := http.NewRequest(http.MethodPost, proxy.URL+CodexDesktopPathPrefix+"/v1/responses?trace=1", bytes.NewReader(compressed))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Content-Encoding", "zstd")
req.Header.Set("ChatGPT-Account-ID", "account-123")
req.Header.Set("Authorization", authorization)
req.Header.Set("X-Codex-Turn-Metadata", `{"thread":"secret"}`)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
responseBody, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK || string(responseBody) != "data: routed\n\n" {
t.Fatalf("response with %q = %d %q", authorization, resp.StatusCode, responseBody)
}
if gotPath != "/v1/responses" || gotQuery != "trace=1" {
t.Fatalf("Ollama target = %s?%s", gotPath, gotQuery)
}
if gotAuthorization != "" || gotEncoding != "" || gotMetadata != "" {
t.Fatalf("credentials leaked to Ollama: authorization=%q encoding=%q metadata=%q", gotAuthorization, gotEncoding, gotMetadata)
}
if string(gotBody) != string(payload) {
t.Fatalf("Ollama body = %q, want decompressed %q", gotBody, payload)
}
}
}
func TestCodexDesktopRoutesOllamaAndNativeModelsThroughOneEndpoint(t *testing.T) {
var ollamaCalls, chatGPTCalls int
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
ollamaCalls++
w.WriteHeader(http.StatusNoContent)
}))
defer ollama.Close()
chatGPT := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
chatGPTCalls++
w.WriteHeader(http.StatusNoContent)
}))
defer chatGPT.Close()
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL+"/backend-api/codex", writeCatalog(t, "glm-5.2:cloud"))
endpoint := httptest.NewServer(handler)
defer endpoint.Close()
for _, model := range []string{"glm-5.2:cloud", "gpt-5.6-sol"} {
req, err := http.NewRequest(
http.MethodPost,
endpoint.URL+CodexDesktopPathPrefix+"/v1/responses",
strings.NewReader(fmt.Sprintf(`{"model":%q}`, model)),
)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Authorization", "Bearer chatgpt-secret")
req.Header.Set("ChatGPT-Account-ID", "account-123")
resp, err := endpoint.Client().Do(req)
if err != nil {
t.Fatal(err)
}
_ = resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
t.Fatalf("model %q status = %d", model, resp.StatusCode)
}
}
if ollamaCalls != 1 || chatGPTCalls != 1 {
t.Fatalf("routes through one endpoint: Ollama calls=%d ChatGPT calls=%d", ollamaCalls, chatGPTCalls)
}
}
func TestNormalizeOllamaThinkingUsesRoutedModelContract(t *testing.T) {
tests := []struct {
name string
model routingModel
effort string
wantReasoning bool
wantEffort string
omitEffort bool
wantThink any
hasThink bool
}{
{
name: "legacy catalog preserves request",
effort: "high",
wantReasoning: true,
wantEffort: "high",
},
{
name: "non-thinking model drops stale reasoning",
model: routingModel{Thinking: &routingThinkingMetadata{}},
effort: "high",
wantReasoning: false,
},
{
name: "binary thinking stays off",
model: routingModel{Thinking: &routingThinkingMetadata{
Supported: true,
Levels: []string{"none", "medium"},
Values: map[string]json.RawMessage{"none": json.RawMessage("false"), "medium": json.RawMessage("true")},
}},
effort: "none",
wantReasoning: true,
wantEffort: "none",
wantThink: false,
hasThink: true,
},
{
name: "binary thinking uses medium for its enabled choice",
model: routingModel{Thinking: &routingThinkingMetadata{
Supported: true,
Levels: []string{"none", "medium"},
Values: map[string]json.RawMessage{"none": json.RawMessage("false"), "medium": json.RawMessage("true")},
}},
effort: "medium",
wantReasoning: true,
wantEffort: "medium",
wantThink: true,
hasThink: true,
},
{
name: "binary thinking maps a stale enabled level to medium",
model: routingModel{Thinking: &routingThinkingMetadata{
Supported: true,
Levels: []string{"none", "medium"},
Values: map[string]json.RawMessage{"none": json.RawMessage("false"), "medium": json.RawMessage("true")},
}},
effort: "high",
wantReasoning: true,
wantEffort: "medium",
wantThink: true,
hasThink: true,
},
{
name: "exact GLM ladder clamps xhigh to max",
model: routingModel{Thinking: &routingThinkingMetadata{
Supported: true,
Levels: []string{"low", "high", "max"},
Values: map[string]json.RawMessage{"low": json.RawMessage(`"low"`), "high": json.RawMessage(`"high"`), "max": json.RawMessage(`"max"`)},
}},
effort: "xhigh",
wantReasoning: true,
wantEffort: "max",
wantThink: "max",
hasThink: true,
},
{
name: "exact GLM ladder omits unsupported medium",
model: routingModel{Thinking: &routingThinkingMetadata{
Supported: true,
Levels: []string{"low", "high", "max"},
}},
effort: "medium",
wantReasoning: true,
omitEffort: true,
},
{
name: "always-thinking GLM omits stale off",
model: routingModel{Thinking: &routingThinkingMetadata{
Supported: true,
Levels: []string{"low", "high", "max"},
}},
effort: "none",
wantReasoning: true,
omitEffort: true,
},
{
name: "minimal maps to Ollama low",
model: routingModel{Thinking: &routingThinkingMetadata{
Supported: true,
Levels: []string{"low", "medium", "high"},
Values: map[string]json.RawMessage{"low": json.RawMessage(`"low"`), "medium": json.RawMessage(`"medium"`), "high": json.RawMessage(`"high"`)},
}},
effort: "minimal",
wantReasoning: true,
wantEffort: "low",
wantThink: "low",
hasThink: true,
},
{
name: "stale xhigh clamps to the strongest supported effort",
model: routingModel{Thinking: &routingThinkingMetadata{
Supported: true,
Levels: []string{"low", "medium", "high"},
Values: map[string]json.RawMessage{"low": json.RawMessage(`"low"`), "medium": json.RawMessage(`"medium"`), "high": json.RawMessage(`"high"`)},
}},
effort: "xhigh",
wantReasoning: true,
wantEffort: "high",
wantThink: "high",
hasThink: true,
},
{
name: "unknown effort is omitted instead of enabling binary thinking",
model: routingModel{Thinking: &routingThinkingMetadata{
Supported: true,
Levels: []string{"none", "medium"},
}},
effort: "unexpected",
wantReasoning: true,
omitEffort: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body, err := json.Marshal(map[string]any{
"model": "test-model",
"reasoning": map[string]any{
"effort": tt.effort,
"summary": "auto",
},
})
if err != nil {
t.Fatal(err)
}
normalized, err := normalizeOllamaRequestBody(body, tt.model)
if err != nil {
t.Fatal(err)
}
var payload map[string]any
if err := json.Unmarshal(normalized, &payload); err != nil {
t.Fatal(err)
}
reasoning, ok := payload["reasoning"].(map[string]any)
if ok != tt.wantReasoning {
t.Fatalf("reasoning present = %v, want %v in %s", ok, tt.wantReasoning, normalized)
}
gotThink, hasThink := payload["think"]
if hasThink != tt.hasThink || (hasThink && gotThink != tt.wantThink) {
t.Fatalf("think = %#v, %v; want %#v, %v in %s", gotThink, hasThink, tt.wantThink, tt.hasThink, normalized)
}
if !tt.wantReasoning {
return
}
gotEffort, hasEffort := reasoning["effort"].(string)
if tt.omitEffort {
if hasEffort {
t.Fatalf("reasoning effort = %q, want model default with no explicit effort in %s", gotEffort, normalized)
}
} else if !hasEffort || gotEffort != tt.wantEffort {
t.Fatalf("reasoning effort = %q, %v; want %q in %s", gotEffort, hasEffort, tt.wantEffort, normalized)
}
if got, _ := reasoning["summary"].(string); got != "auto" {
t.Fatalf("reasoning summary = %q, want preserved", got)
}
})
}
}
func TestNormalizeOllamaDiscoveredThinking(t *testing.T) {
for _, tc := range []struct {
name string
controls *modelpkg.Thinking
values map[string]any
effort string
want any
}{
{"exact xhigh", &modelpkg.Thinking{Values: []any{false, "low", "medium", "xhigh"}, Default: "medium"}, map[string]any{"none": false, "low": "low", "medium": "medium", "xhigh": "xhigh"}, "xhigh", "xhigh"},
{"exact minimal", &modelpkg.Thinking{Values: []any{"minimal", "high"}, Default: "high"}, map[string]any{"minimal": "minimal", "high": "high"}, "minimal", "minimal"},
{"unsupported xhigh defaults", &modelpkg.Thinking{Values: []any{false, "high", "max"}, Default: "high"}, map[string]any{"none": false, "high": "high", "max": "max"}, "xhigh", "high"},
{"saved Boolean medium", &modelpkg.Thinking{Values: []any{false, true}, Default: false}, map[string]any{"none": false, "high": true}, "medium", true},
{"Boolean effort alias", &modelpkg.Thinking{Values: []any{false, true}, Default: false}, map[string]any{"none": false, "high": true}, "minimal", true},
{"unknown Boolean effort defaults", &modelpkg.Thinking{Values: []any{false, true}, Default: false}, map[string]any{"none": false, "high": true}, "future", false},
{"mixed named medium", &modelpkg.Thinking{Values: []any{false, true, "medium"}, Default: false}, map[string]any{"none": false, "high": true, "medium": "medium"}, "medium", "medium"},
{"hidden named control prevents Boolean alias", &modelpkg.Thinking{Values: []any{false, true, "turbo"}, Default: false}, map[string]any{"none": false, "high": true}, "medium", false},
{"hidden default", &modelpkg.Thinking{Values: []any{"low", "high", "turbo"}, Default: "turbo"}, map[string]any{"low": "low", "high": "high"}, "medium", "turbo"},
} {
t.Run(tc.name, func(t *testing.T) {
metadata := routingThinkingMetadata{Supported: true, Controls: tc.controls, Values: make(map[string]json.RawMessage)}
for level, value := range tc.values {
metadata.Levels = append(metadata.Levels, level)
encoded, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
metadata.Values[level] = encoded
}
body := []byte(fmt.Sprintf(`{"model":"test","input":"hi","reasoning":{"effort":%q,"summary":"auto"}}`, tc.effort))
normalized, err := normalizeOllamaThinking(body, metadata)
if err != nil {
t.Fatal(err)
}
var request openai.ResponsesRequest
if err := json.Unmarshal(normalized, &request); err != nil {
t.Fatal(err)
}
converted, err := openai.FromResponsesRequest(request, tc.controls)
if err != nil {
t.Fatal(err)
}
resolved := renderers.ResolveThinking(converted.Think, tc.controls)
if resolved == nil || resolved.Value != tc.want {
t.Fatalf("resolved=%v, want %#v; request=%s", resolved, tc.want, normalized)
}
})
}
legacy := routingThinkingMetadata{Supported: true, Levels: []string{"none", "high"}, Values: map[string]json.RawMessage{"none": json.RawMessage("false"), "high": json.RawMessage("true")}}
if got := normalizeThinkingEffort("medium", legacy); got != "high" {
t.Fatalf("saved Boolean effort=%q, want high", got)
}
}
func TestNormalizeOllamaThinkingRejectsMalformedReasoning(t *testing.T) {
model := routingModel{Thinking: &routingThinkingMetadata{
Supported: true,
Levels: []string{"none", "medium"},
}}
_, err := normalizeOllamaRequestBody([]byte(`{"model":"test-model","reasoning":"high"}`), model)
if err == nil || !strings.Contains(err.Error(), "decode reasoning") {
t.Fatalf("normalize error = %v, want malformed reasoning error", err)
}
}
func TestNormalizeFullAccessExecToolRemovesEscalationArguments(t *testing.T) {
turnMetadata := `{"sandbox_mode":"danger-full-access"}`
body := []byte(fmt.Sprintf(`{
"model":"glm-5.3-flash:cloud",
"client_metadata":{"x-codex-turn-metadata":%q},
"tools":[
{"type":"function","name":"exec_command","parameters":{"type":"object","properties":{"cmd":{"type":"string"},"sandbox_permissions":{"type":"string"},"justification":{"type":"string"},"prefix_rule":{"type":"array"}},"required":["cmd","sandbox_permissions","justification","prefix_rule"],"additionalProperties":false}},
{"type":"function","name":"other_tool","parameters":{"type":"object","properties":{"sandbox_permissions":{"type":"string"}}}}
]
}`, turnMetadata))
normalized, err := normalizeFullAccessExecTool(body)
if err != nil {
t.Fatal(err)
}
var payload struct {
Tools []struct {
Name string `json:"name"`
Parameters struct {
Properties map[string]json.RawMessage `json:"properties"`
Required []string `json:"required"`
} `json:"parameters"`
} `json:"tools"`
}
if err := json.Unmarshal(normalized, &payload); err != nil {
t.Fatal(err)
}
if len(payload.Tools) != 2 {
t.Fatalf("tools = %d, want 2", len(payload.Tools))
}
execTool := payload.Tools[0]
if _, ok := execTool.Parameters.Properties["cmd"]; !ok {
t.Fatal("exec_command cmd property was removed")
}
for _, property := range []string{"sandbox_permissions", "justification", "prefix_rule"} {
if _, ok := execTool.Parameters.Properties[property]; ok {
t.Fatalf("exec_command retained %q property", property)
}
if slices.Contains(execTool.Parameters.Required, property) {
t.Fatalf("exec_command retained required %q", property)
}
}
if !slices.Equal(execTool.Parameters.Required, []string{"cmd"}) {
t.Fatalf("exec_command required = %v, want [cmd]", execTool.Parameters.Required)
}
if _, ok := payload.Tools[1].Parameters.Properties["sandbox_permissions"]; !ok {
t.Fatal("non-exec tool was changed")
}
}
func TestNormalizeFullAccessExecToolLeavesSandboxedTurnUnchanged(t *testing.T) {
body := []byte(`{"client_metadata":{"x-codex-turn-metadata":"{\"sandbox_mode\":\"workspace-write\"}"},"tools":[{"name":"exec_command","parameters":{"properties":{"sandbox_permissions":{"type":"string"}}}}]}`)
normalized, err := normalizeFullAccessExecTool(body)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(normalized, body) {
t.Fatalf("sandboxed request changed:\n%s", normalized)
}
}
func TestNormalizeFullAccessNamespacedExecTool(t *testing.T) {
const tools = `[{"type":"namespace","name":"functions","description":"Command tools","tools":[
{"type":"function","name":"exec_command","strict":false,"parameters":{"type":"object","properties":{"cmd":{"type":"string"},"sandbox_permissions":{"type":"string","enum":["use_default","require_escalated"]},"justification":{"type":"string"},"prefix_rule":{"type":"array","items":{"type":"string"}}},"required":["cmd","sandbox_permissions","justification","prefix_rule"],"additionalProperties":false}},
{"type":"function","name":"other_tool","parameters":{"type":"object","properties":{"sandbox_permissions":{"type":"string"}}}}
]}]`
for _, mode := range []string{"danger-full-access", "workspace-write", "read-only", ""} {
t.Run(mode, func(t *testing.T) {
metadata, err := json.Marshal(map[string]string{"sandbox_mode": mode})
if err != nil {
t.Fatal(err)
}
body := []byte(fmt.Sprintf(`{"model":"glm-5.3-flash:cloud","client_metadata":{"x-codex-turn-metadata":%q},"tools":%s}`, metadata, tools))
normalized, err := normalizeFullAccessExecTool(body)
if err != nil {
t.Fatal(err)
}
if mode != "danger-full-access" {
if !bytes.Equal(normalized, body) {
t.Fatalf("sandboxed request changed: %s", normalized)
}
return
}
var want, got map[string]any
if err := json.Unmarshal(body, &want); err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(normalized, &got); err != nil {
t.Fatal(err)
}
namespace := want["tools"].([]any)[0].(map[string]any)
exec := namespace["tools"].([]any)[0].(map[string]any)
parameters := exec["parameters"].(map[string]any)
properties := parameters["properties"].(map[string]any)
for _, key := range []string{"sandbox_permissions", "justification", "prefix_rule"} {
delete(properties, key)
}
parameters["required"] = []any{"cmd"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("namespace tool normalization did not preserve the expected request: %s", normalized)
}
})
}
}
func TestNormalizeFullAccessNamespaceBoundaries(t *testing.T) {
for _, tt := range []struct {
name string
tools string
wantErr bool
}{
{"missing members", `[{"type":"namespace","name":"functions"}]`, false},
{"null members", `[{"type":"namespace","name":"functions","tools":null}]`, false},
{"empty members", `[{"type":"namespace","name":"functions","tools":[]}]`, false},
{"no escalation arguments", `[{"type":"namespace","name":"functions","tools":[{"type":"function","name":"exec_command","parameters":{"properties":{"cmd":{"type":"string"}},"required":["cmd"]}}]}]`, false},
{"malformed members", `[{"type":"namespace","name":"functions","tools":{}}]`, true},
{"malformed parameters", `[{"type":"namespace","name":"functions","tools":[{"type":"function","name":"exec_command","parameters":42}]}]`, true},
} {
t.Run(tt.name, func(t *testing.T) {
body := []byte(`{"client_metadata":{"x-codex-turn-metadata":{"sandbox_mode":"danger-full-access"}},"tools":` + tt.tools + `}`)
normalized, err := normalizeFullAccessExecTool(body)
if (err != nil) != tt.wantErr {
t.Fatalf("normalization error = %v, want error = %v", err, tt.wantErr)
}
if !tt.wantErr && !bytes.Equal(normalized, body) {
t.Fatalf("unchanged namespace was rewritten: %s", normalized)
}
})
}
}
func TestLoadCatalogModelsReadsOptionalThinkingMetadata(t *testing.T) {
path := filepath.Join(t.TempDir(), CodexDesktopModelCatalogFilename)
data := []byte(`{"models":[{"slug":"legacy"},{"slug":"binary","thinking":{"supported":true,"levels":["none","medium"],"values":{"none":false,"medium":true}}}]}`)
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatal(err)
}
catalog, err := loadRoutingCatalog(path)
if err != nil {
t.Fatal(err)
}
models := catalog.models
if models["legacy"].Thinking != nil {
t.Fatalf("legacy model thinking metadata = %+v, want absent", models["legacy"].Thinking)
}
binary := models["binary"].Thinking
if binary == nil || !binary.Supported || strings.Join(binary.Levels, ",") != "none,medium" {
t.Fatalf("binary model thinking metadata = %+v, want off/medium contract", binary)
}
if string(binary.Values["none"]) != "false" || string(binary.Values["medium"]) != "true" {
t.Fatalf("binary model thinking values = %#v, want exact false/true values", binary.Values)
}
}
func TestCodexDesktopRoutesAutoReviewToSelectedNativeModel(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("native selected model should not reach Ollama")
}))
defer ollama.Close()
var models []string
var encodings []string
var guardianBody map[string]json.RawMessage
chatGPT := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var payload map[string]json.RawMessage
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
var model string
if err := json.Unmarshal(payload["model"], &model); err != nil {
t.Fatal(err)
}
models = append(models, model)
encodings = append(encodings, r.Header.Get("Content-Encoding"))
if len(models) == 2 {
guardianBody = payload
}
w.WriteHeader(http.StatusNoContent)
}))
defer chatGPT.Close()
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL, writeCatalogWithSelectedAutoReview(t, "glm-5.3-flash:cloud", "glm-5.3-flash:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
postCodexRequest(t, proxy.URL, `{"model":"gpt-5.6-sol","client_metadata":{"turn_id":"turn-native"}}`, true)
postCompressedCodexRequest(t, proxy.URL, `{"model":"codex-auto-review","client_metadata":{"parent_turn_id":"turn-native"},"text":{"format":{"type":"json_schema"}}}`)
if strings.Join(models, ",") != "gpt-5.6-sol,gpt-5.6-sol" {
t.Fatalf("forwarded models = %q, want selected native model for Guardian", models)
}
if _, ok := guardianBody["text"]; !ok {
t.Fatalf("native Guardian structured output was removed: %#v", guardianBody)
}
if _, ok := guardianBody["tools"]; ok {
t.Fatalf("proxy decision tool was added to native Guardian: %#v", guardianBody)
}
if encodings[1] != "" {
t.Fatalf("rewritten native Guardian retained content encoding %q", encodings[1])
}
}
func TestCodexDesktopRoutesAutoReviewToSelectedOllamaModel(t *testing.T) {
var models []string
var guardianBody map[string]json.RawMessage
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var payload map[string]json.RawMessage
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
var model string
if err := json.Unmarshal(payload["model"], &model); err != nil {
t.Fatal(err)
}
models = append(models, model)
if len(models) == 2 {
guardianBody = payload
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, autoReviewJSONResponse(`{"risk_level":"low","user_authorization":"high","outcome":"allow","rationale":"Allowed."}`))
return
}
w.WriteHeader(http.StatusNoContent)
}))
defer ollama.Close()
chatGPT := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("Ollama selected model should not reach ChatGPT")
}))
defer chatGPT.Close()
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL, writeCatalogWithSelectedAutoReview(t, "glm-5.3-flash:cloud", "glm-5.3-flash:cloud", "deepseek-v3.1:671b-cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
postCodexRequest(t, proxy.URL, `{"model":"deepseek-v3.1:671b-cloud","client_metadata":{"turn_id":"turn-ollama"}}`, false)
postCodexRequest(t, proxy.URL, `{"model":"codex-auto-review","client_metadata":{"parent_turn_id":"turn-ollama"},"input":[{"role":"user","content":"review"}],"text":{"format":{"type":"json_schema"}}}`, false)
if strings.Join(models, ",") != "deepseek-v3.1:671b-cloud,deepseek-v3.1:671b-cloud" {
t.Fatalf("forwarded models = %q, want selected Ollama model for Guardian", models)
}
if _, ok := guardianBody["text"]; ok {
t.Fatalf("Ollama Guardian retained competing structured output: %#v", guardianBody)
}
if _, ok := guardianBody["tools"]; !ok {
t.Fatalf("Ollama Guardian decision tool is missing: %#v", guardianBody)
}
}
func TestCodexDesktopSelectedAutoReviewFallsBackWithoutParentTurn(t *testing.T) {
var gotModel string
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var payload struct {
Model string `json:"model"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
gotModel = payload.Model
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, autoReviewJSONResponse(`{"risk_level":"low","user_authorization":"high","outcome":"allow","rationale":"Allowed."}`))
}))
defer ollama.Close()
chatGPT := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("selected Auto-review fallback should not reach ChatGPT")
}))
defer chatGPT.Close()
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL, writeCatalogWithSelectedAutoReview(t, "glm-5.3-flash:cloud", "glm-5.3-flash:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
postCodexRequest(t, proxy.URL, `{"model":"codex-auto-review","input":[{"role":"user","content":"review"}]}`, false)
if gotModel != "glm-5.3-flash:cloud" {
t.Fatalf("fallback model = %q, want launch primary", gotModel)
}
}
func TestTurnModelCacheIsBounded(t *testing.T) {
var cache turnModelCache
for i := 0; i <= maxTrackedTurnModels; i++ {
cache.remember(fmt.Sprintf("turn-%d", i), fmt.Sprintf("model-%d", i))
}
if _, ok := cache.lookup("turn-0"); ok {
t.Fatal("oldest turn was not evicted")
}
if model, ok := cache.lookup(fmt.Sprintf("turn-%d", maxTrackedTurnModels)); !ok || model != fmt.Sprintf("model-%d", maxTrackedTurnModels) {
t.Fatalf("newest turn = %q, %v", model, ok)
}
}
func TestCodexDesktopRoutesAutoReviewToConfiguredOllamaModel(t *testing.T) {
var gotBody []byte
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotBody, _ = io.ReadAll(r.Body)
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, autoReviewJSONResponse(`{"risk_level":"low","user_authorization":"high","outcome":"allow","rationale":"The user requested this action."}`))
}))
defer ollama.Close()
chatGPT := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("configured Auto-review request should not reach ChatGPT")
}))
defer chatGPT.Close()
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL, writeCatalogWithAutoReview(t, "glm-5.3-flash:cloud", "glm-5.3-flash:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
resp, err := http.Post(
proxy.URL+CodexDesktopPathPrefix+"/v1/responses",
"application/json",
strings.NewReader(`{"model":"codex-auto-review","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"review"}]}],"text":{"format":{"type":"json_schema"}},"response_format":{"type":"json_object"}}`),
)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
var forwarded struct {
Model string `json:"model"`
Tools []struct {
Type string `json:"type"`
Name string `json:"name"`
Strict bool `json:"strict"`
Parameters struct {
Required []string `json:"required"`
AdditionalProperties bool `json:"additionalProperties"`
} `json:"parameters"`
} `json:"tools"`
Input []struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"content"`
} `json:"input"`
}
if err := json.Unmarshal(gotBody, &forwarded); err != nil {
t.Fatal(err)
}
if forwarded.Model != "glm-5.3-flash:cloud" {
t.Fatalf("forwarded model = %q, want selected Ollama model", forwarded.Model)
}
if len(forwarded.Input) != 1 || len(forwarded.Input[0].Content) != 2 {
t.Fatalf("forwarded input = %#v", forwarded.Input)
}
instruction := forwarded.Input[0].Content[1]
if instruction.Type != "input_text" || !strings.Contains(instruction.Text, "call submit_guardian_decision exactly once") {
t.Fatalf("Auto-review decision instruction = %#v", instruction)
}
if len(forwarded.Tools) != 1 || forwarded.Tools[0].Type != "function" || forwarded.Tools[0].Name != guardianDecisionToolName || !forwarded.Tools[0].Strict {
t.Fatalf("Auto-review tools = %#v", forwarded.Tools)
}
if len(forwarded.Tools[0].Parameters.Required) != 4 || forwarded.Tools[0].Parameters.AdditionalProperties {
t.Fatalf("Auto-review tool schema = %#v", forwarded.Tools[0].Parameters)
}
var raw map[string]json.RawMessage
if err := json.Unmarshal(gotBody, &raw); err != nil {
t.Fatal(err)
}
if _, ok := raw["response_format"]; ok {
t.Fatalf("Auto-review request unexpectedly used structured outputs: %s", gotBody)
}
if _, ok := raw["text"]; ok {
t.Fatalf("Auto-review request unexpectedly added a structured text format: %s", gotBody)
}
var response struct {
Output []struct {
Type string `json:"type"`
Content []struct {
Text string `json:"text"`
} `json:"content"`
} `json:"output"`
}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
t.Fatal(err)
}
if len(response.Output) != 1 || response.Output[0].Type != "message" || len(response.Output[0].Content) != 1 || response.Output[0].Content[0].Text != `{"risk_level":"low","user_authorization":"high","outcome":"allow","rationale":"The user requested this action."}` {
t.Fatalf("translated Auto-review response = %#v", response.Output)
}
}
func TestPrepareAutoReviewRequestSupportsStringContentAndPreservesTools(t *testing.T) {
body := []byte(`{"model":"codex-auto-review","tools":[{"type":"function","name":"inspect","description":"Inspect","strict":true,"parameters":{"type":"object"}}],"input":[{"role":"user","content":"review"}]}`)
updated, err := prepareAutoReviewRequest(body)
if err != nil {
t.Fatal(err)
}
var payload struct {
Tools []struct {
Name string `json:"name"`
} `json:"tools"`
Input []struct {
Content string `json:"content"`
} `json:"input"`
}
if err := json.Unmarshal(updated, &payload); err != nil {
t.Fatal(err)
}
if len(payload.Input) != 1 || !strings.HasPrefix(payload.Input[0].Content, "review") || !strings.Contains(payload.Input[0].Content, guardianDecisionToolName) {
t.Fatalf("updated input = %#v", payload.Input)
}
if len(payload.Tools) != 2 || payload.Tools[0].Name != "inspect" || payload.Tools[1].Name != guardianDecisionToolName {
t.Fatalf("updated tools = %#v", payload.Tools)
}
}
func TestPrepareAutoReviewRequestRejectsRequestsWithoutUserMessages(t *testing.T) {
body := []byte(`{"model":"codex-auto-review","input":[{"role":"developer","content":"policy"}]}`)
if _, err := prepareAutoReviewRequest(body); err == nil || !strings.Contains(err.Error(), "no user message") {
t.Fatalf("error = %v, want missing user message", err)
}
}
func TestPrepareAutoReviewRequestUsesLastUserMessage(t *testing.T) {
body := []byte(`{"input":[{"role":"user","content":"earlier"},{"role":"assistant","content":"reviewed"},{"role":"user","content":"latest"}]}`)
updated, err := prepareAutoReviewRequest(body)
if err != nil {
t.Fatal(err)
}
var payload struct {
Input []struct {
Content string `json:"content"`
} `json:"input"`
}
if err := json.Unmarshal(updated, &payload); err != nil {
t.Fatal(err)
}
if payload.Input[0].Content != "earlier" {
t.Fatalf("earlier user message was changed: %q", payload.Input[0].Content)
}
if !strings.HasPrefix(payload.Input[2].Content, "latest") || !strings.Contains(payload.Input[2].Content, guardianDecisionToolName) {
t.Fatalf("latest user message = %q", payload.Input[2].Content)
}
}
func TestTransformAutoReviewEventStreamConvertsDecisionToolCall(t *testing.T) {
arguments := `{"risk_level":"medium","user_authorization":"low","outcome":"deny","rationale":"The action exceeds the user's authorization."}`
body := autoReviewEventStream(t, guardianDecisionToolName, arguments)
transformed, changed, err := transformAutoReviewResponse(body, "text/event-stream; charset=utf-8")
if err != nil {
t.Fatal(err)
}
if !changed {
t.Fatal("decision tool call was not transformed")
}
text := string(transformed)
if strings.Contains(text, "response.function_call_arguments") || strings.Contains(text, `"type":"function_call"`) {
t.Fatalf("transformed stream still contains the decision tool call:\n%s", text)
}
for _, want := range []string{
"event: response.output_text.delta",
"event: response.output_text.done",
`\"outcome\":\"deny\"`,
`"type":"message"`,
} {
if !strings.Contains(text, want) {
t.Fatalf("transformed stream missing %q:\n%s", want, text)
}
}
events, err := parseServerSentEvents(transformed)
if err != nil {
t.Fatal(err)
}
for i, event := range events {
if bytes.Equal(bytes.TrimSpace(event.data), []byte("[DONE]")) {
continue
}
var payload struct {
Sequence int `json:"sequence_number"`
}
if err := json.Unmarshal(event.data, &payload); err != nil {
t.Fatal(err)
}
if payload.Sequence != i {
t.Fatalf("event %d sequence_number = %d", i, payload.Sequence)
}
}
}
func TestTransformAutoReviewResponsePassesThroughInvestigativeToolCall(t *testing.T) {
body := []byte(autoReviewJSONResponseForTool("inspect_command", `{"command":"rm -rf tmp"}`))
transformed, changed, err := transformAutoReviewResponse(body, "application/json")
if err != nil {
t.Fatal(err)
}
if changed || !bytes.Equal(transformed, body) {
t.Fatalf("investigative tool call changed: %s", transformed)
}
}
func TestTransformAutoReviewResponseRejectsInvalidDecision(t *testing.T) {
body := []byte(autoReviewJSONResponse(`{"risk_level":"low","user_authorization":"high","outcome":"maybe","rationale":"Invalid outcome."}`))
if _, _, err := transformAutoReviewResponse(body, "application/json"); err == nil || !strings.Contains(err.Error(), "invalid Guardian outcome") {
t.Fatalf("error = %v, want invalid outcome", err)
}
}
func TestTransformAutoReviewResponseRejectsTextDecision(t *testing.T) {
body := []byte(`{"id":"resp_1","status":"completed","output":[{"id":"msg_1","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"{\\\"outcome\\\":\\\"deny\\\"}"}]}]}`)
if _, _, err := transformAutoReviewResponse(body, "application/json"); err == nil || !strings.Contains(err.Error(), "did not call") {
t.Fatalf("error = %v, want missing decision tool call", err)
}
}
func TestTransformAutoReviewResponseAcceptsValidatedTextFallback(t *testing.T) {
decision := `{"risk_level":"low","user_authorization":"high","outcome":"allow","rationale":"The user requested this benign local action."}`
jsonBody := []byte(fmt.Sprintf(`{"id":"resp_1","status":"completed","output":[{"id":"msg_1","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":%q}]}]}`, decision))
for _, test := range []struct {
name string
contentType string
body []byte
}{
{name: "json", contentType: "application/json", body: jsonBody},
{name: "event stream", contentType: "text/event-stream", body: autoReviewTextEventStream(t, decision)},
} {
t.Run(test.name, func(t *testing.T) {
transformed, changed, err := transformAutoReviewResponse(test.body, test.contentType)
if err != nil {
t.Fatal(err)
}
if changed || !bytes.Equal(transformed, test.body) {
t.Fatalf("validated text fallback changed:\n%s", transformed)
}
})
}
}
func TestTransformAutoReviewResponseAcceptsCompactAllowTextFallback(t *testing.T) {
decision := `{"outcome":"allow"}`
jsonBody := []byte(fmt.Sprintf(`{"id":"resp_1","status":"completed","output":[{"id":"msg_1","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":%q}]}]}`, decision))
for _, test := range []struct {
name string
contentType string
body []byte
}{
{name: "json", contentType: "application/json", body: jsonBody},
{name: "event stream", contentType: "text/event-stream", body: autoReviewTextEventStream(t, decision)},
} {
t.Run(test.name, func(t *testing.T) {
transformed, changed, err := transformAutoReviewResponse(test.body, test.contentType)
if err != nil {
t.Fatal(err)
}
if changed || !bytes.Equal(transformed, test.body) {
t.Fatalf("compact allow fallback changed:\n%s", transformed)
}
})
}
}
func TestTransformAutoReviewJSONDiscardsProseWithValidDecision(t *testing.T) {
arguments := `{"risk_level":"low","user_authorization":"high","outcome":"allow","rationale":"The command is read-only and explicitly requested."}`
body := []byte(fmt.Sprintf(`{"id":"resp_1","status":"completed","output":[{"id":"fc_1","type":"function_call","status":"completed","call_id":"call_1","name":%q,"arguments":%q},{"id":"msg_1","type":"message","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Submitting approval."}]}]}`, guardianDecisionToolName, arguments))
transformed, changed, err := transformAutoReviewResponse(body, "application/json")
if err != nil {
t.Fatal(err)
}
if !changed {
t.Fatal("mixed decision response was not transformed")
}
var response struct {
Output []struct {
ID string `json:"id"`
Type string `json:"type"`
Content []struct {
Text string `json:"text"`
} `json:"content"`
} `json:"output"`
}
if err := json.Unmarshal(transformed, &response); err != nil {
t.Fatal(err)
}
if len(response.Output) != 1 || response.Output[0].ID != "fc_1" || response.Output[0].Type != "message" || len(response.Output[0].Content) != 1 || response.Output[0].Content[0].Text != arguments {
t.Fatalf("transformed output = %#v", response.Output)
}
}
func TestTransformAutoReviewEventStreamDiscardsProseWithValidDecision(t *testing.T) {
arguments := `{"risk_level":"low","user_authorization":"high","outcome":"allow","rationale":"The command is read-only and explicitly requested."}`
extraMessage := map[string]any{
"id": "msg_1", "type": "message", "status": "completed", "role": "assistant",
"content": []any{map[string]any{"type": "output_text", "text": "Submitting approval.", "annotations": []any{}, "logprobs": []any{}}},
}
body := autoReviewEventStream(t, guardianDecisionToolName, arguments, extraMessage)
transformed, changed, err := transformAutoReviewResponse(body, "text/event-stream")
if err != nil {
t.Fatal(err)
}
if !changed {
t.Fatal("mixed decision stream was not transformed")
}
text := string(transformed)
if strings.Contains(text, "Submitting approval") || strings.Contains(text, "msg_1") {
t.Fatalf("transformed stream retained assistant prose:\n%s", text)
}
events, err := parseServerSentEvents(transformed)
if err != nil {
t.Fatal(err)
}
var completed map[string]any
for _, event := range events {
if event.event == "response.completed" {
if err := json.Unmarshal(event.data, &completed); err != nil {
t.Fatal(err)
}
}
}
response := completed["response"].(map[string]any)
output := response["output"].([]any)
if len(output) != 1 || itemString(output[0].(map[string]any), "id") != "fc_1" || itemString(output[0].(map[string]any), "type") != "message" {
t.Fatalf("completed output = %#v", output)
}
}
func TestTransformAutoReviewResponseRejectsDecisionWithAnotherToolCall(t *testing.T) {
decision := `{"risk_level":"low","user_authorization":"high","outcome":"allow","rationale":"Allowed."}`
body := []byte(fmt.Sprintf(`{"id":"resp_1","status":"completed","output":[{"id":"fc_1","type":"function_call","name":%q,"arguments":%q},{"id":"fc_2","type":"function_call","name":"inspect_command","arguments":"{}"}]}`, guardianDecisionToolName, decision))
if _, _, err := transformAutoReviewResponse(body, "application/json"); err == nil || !strings.Contains(err.Error(), "other terminal output") {
t.Fatalf("error = %v, want mixed terminal output", err)
}
}
func TestTransformAutoReviewResponsePreservesProviderFailure(t *testing.T) {
body := []byte(`{"id":"resp_1","status":"failed","output":[],"error":{"code":"provider_error","message":"unavailable"}}`)
transformed, changed, err := transformAutoReviewResponse(body, "application/json")
if err != nil {
t.Fatal(err)
}
if changed || !bytes.Equal(transformed, body) {
t.Fatalf("provider failure changed: %s", transformed)
}
}
func TestCodexDesktopKeepsAutoReviewOnChatGPTByDefault(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("native Auto-review request should not reach Ollama")
}))
defer ollama.Close()
var gotModel string
chatGPT := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var payload struct {
Model string `json:"model"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
gotModel = payload.Model
w.WriteHeader(http.StatusOK)
}))
defer chatGPT.Close()
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL, writeCatalog(t, "glm-5.3-flash:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
req, err := http.NewRequest(http.MethodPost, proxy.URL+CodexDesktopPathPrefix+"/v1/responses", strings.NewReader(`{"model":"codex-auto-review"}`))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("ChatGPT-Account-ID", "account-123")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
if gotModel != autoReviewModel {
t.Fatalf("forwarded model = %q, want native reviewer alias", gotModel)
}
}
func TestCodexDesktopRejectsAutoReviewModelOutsideRoutingCatalog(t *testing.T) {
called := false
upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
called = true
}))
defer upstream.Close()
handler := newTestCodexDesktop(t, upstream.URL, upstream.URL, writeCatalogWithAutoReview(t, "qwen3:8b", "glm-5.3-flash:cloud"))
req := httptest.NewRequest(http.MethodPost, "http://localhost"+CodexDesktopPathPrefix+"/v1/responses", strings.NewReader(`{"model":"codex-auto-review"}`))
req.RemoteAddr = "127.0.0.1:1234"
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
if recorder.Code != http.StatusServiceUnavailable {
t.Fatalf("status = %d, want 503: %s", recorder.Code, recorder.Body.String())
}
if called {
t.Fatal("invalid Auto-review configuration reached an upstream")
}
}
func TestCodexDesktopNormalizesCodexOnlyHistoryForOllama(t *testing.T) {
var gotBody []byte
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotBody, _ = io.ReadAll(r.Body)
w.WriteHeader(http.StatusOK)
}))
defer ollama.Close()
chatGPT := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("Ollama model should not reach ChatGPT")
}))
defer chatGPT.Close()
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL, writeCatalog(t, "glm-5.2:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
payload := `{
"model":"glm-5.2:cloud",
"stream":true,
"input":[
{"type":"compaction","encrypted_content":"opaque"},
{"type":"message","role":"developer","content":[{"type":"input_text","text":"<collaboration_mode>Plan Mode</collaboration_mode>"}]},
{"type":"custom_tool_call","id":"ctc_1","status":"completed","call_id":"call_1","name":"apply_patch","input":"*** Begin Patch"},
{"type":"custom_tool_call_output","call_id":"call_1","output":"Success"},
{"type":"message","role":"user","content":[{"type":"input_text","text":"continue"}]},
{"type":"future_codex_item","secret":"ignored"}
]
}`
resp, err := http.Post(proxy.URL+CodexDesktopPathPrefix+"/v1/responses", "application/json", strings.NewReader(payload))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
var forwarded struct {
Input []map[string]any `json:"input"`
}
if err := json.Unmarshal(gotBody, &forwarded); err != nil {
t.Fatal(err)
}
if len(forwarded.Input) != 4 {
t.Fatalf("forwarded input = %#v", forwarded.Input)
}
developer := forwarded.Input[0]
if developer["type"] != "message" || developer["role"] != "system" {
t.Fatalf("developer instructions were not promoted for Ollama: %#v", developer)
}
call := forwarded.Input[1]
if call["type"] != "function_call" || call["call_id"] != "call_1" || call["name"] != "apply_patch" {
t.Fatalf("converted call = %#v", call)
}
if call["arguments"] != `{"input":"*** Begin Patch"}` {
t.Fatalf("converted arguments = %#v", call["arguments"])
}
output := forwarded.Input[2]
if output["type"] != "function_call_output" || output["call_id"] != "call_1" || output["output"] != "Success" {
t.Fatalf("converted output = %#v", output)
}
if forwarded.Input[3]["type"] != "message" || forwarded.Input[3]["role"] != "user" {
t.Fatalf("user message was not preserved: %#v", forwarded.Input[3])
}
}
func TestCodexDesktopPreservesToolSearchAndOllamaCompactionForOllama(t *testing.T) {
var gotBody []byte
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotBody, _ = io.ReadAll(r.Body)
w.WriteHeader(http.StatusOK)
}))
defer ollama.Close()
chatGPT := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("Ollama model should not reach ChatGPT")
}))
defer chatGPT.Close()
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL, writeCatalog(t, "glm-5.3-flash:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
payload := `{
"model":"glm-5.3-flash:cloud",
"input":[
{"type":"compaction","encrypted_content":"native-opaque-state"},
{"type":"compaction","encrypted_content":"{\"type\":\"ollama_compaction\",\"version\":1,\"summary\":\"summary\",\"retained\":[]}"},
{"type":"tool_search_call","id":"tsc_1","call_id":"call_search","execution":"client","status":"completed","arguments":{"query":"notion"}},
{"type":"tool_search_output","id":"tso_1","call_id":"call_search","execution":"client","status":"completed","tools":[{"type":"function","name":"notion.search"}]},
{"type":"message","role":"user","content":"continue"},
{"type":"compaction_trigger"}
]
}`
resp, err := http.Post(proxy.URL+CodexDesktopPathPrefix+"/v1/responses", "application/json", strings.NewReader(payload))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
var forwarded struct {
Input []map[string]any `json:"input"`
}
if err := json.Unmarshal(gotBody, &forwarded); err != nil {
t.Fatal(err)
}
if len(forwarded.Input) != 5 {
t.Fatalf("forwarded input = %#v", forwarded.Input)
}
wantTypes := []string{"compaction", "tool_search_call", "tool_search_output", "message", "compaction_trigger"}
for i, want := range wantTypes {
if got := forwarded.Input[i]["type"]; got != want {
t.Fatalf("forwarded input[%d] type = %#v, want %q", i, got, want)
}
}
}
func TestCodexDesktopFiltersNativeReasoningWhenSwitchingToOllama(t *testing.T) {
var gotBody []byte
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotBody, _ = io.ReadAll(r.Body)
w.WriteHeader(http.StatusOK)
}))
defer ollama.Close()
chatGPT := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("Ollama model should not reach ChatGPT")
}))
defer chatGPT.Close()
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL, writeCatalog(t, "glm-5.3-flash:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
payload := `{
"model":"glm-5.3-flash:cloud",
"input":[
{"type":"reasoning","id":"rs_098c6fb068ce51bf016a9709ab7dcc87d185ecc21991f0f39c","encrypted_content":"gAAAAAB-native"},
{"type":"reasoning","id":"rs_713083","encrypted_content":"Ollama plaintext thinking"},
{"type":"reasoning","id":"rs_resp_123456","encrypted_content":"More Ollama thinking"},
{"type":"message","role":"user","content":[{"type":"input_text","text":"continue"}]}
]
}`
resp, err := http.Post(proxy.URL+CodexDesktopPathPrefix+"/v1/responses", "application/json", strings.NewReader(payload))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
var forwarded struct {
Input []map[string]any `json:"input"`
}
if err := json.Unmarshal(gotBody, &forwarded); err != nil {
t.Fatal(err)
}
if len(forwarded.Input) != 3 {
t.Fatalf("forwarded input = %#v", forwarded.Input)
}
if forwarded.Input[0]["id"] != "rs_713083" || forwarded.Input[1]["id"] != "rs_resp_123456" {
t.Fatalf("Ollama reasoning was not preserved: %#v", forwarded.Input)
}
if forwarded.Input[2]["type"] != "message" {
t.Fatalf("message was not preserved: %#v", forwarded.Input[2])
}
}
func TestCodexDesktopPassesNativeModelToChatGPT(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("native model should not reach Ollama")
}))
defer ollama.Close()
var gotPath, gotAuthorization, gotMetadata string
var gotBody []byte
chatGPT := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotAuthorization = r.Header.Get("Authorization")
gotMetadata = r.Header.Get("X-Codex-Turn-Metadata")
gotBody, _ = io.ReadAll(r.Body)
w.WriteHeader(http.StatusAccepted)
_, _ = io.WriteString(w, "native")
}))
defer chatGPT.Close()
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL+"/backend-api/codex", writeCatalog(t, "glm-5.2:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
payload := []byte(`{"model":"gpt-5.6-sol","stream":true,"input":[{"type":"message","role":"developer","content":[{"type":"input_text","text":"native instructions"}]}]}`)
req, err := http.NewRequest(http.MethodPost, proxy.URL+CodexDesktopPathPrefix+"/v1/responses", bytes.NewReader(payload))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Authorization", "Bearer chatgpt-secret")
req.Header.Set("ChatGPT-Account-ID", "account-123")
req.Header.Set("X-Codex-Turn-Metadata", `{"thread":"kept"}`)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusAccepted {
t.Fatalf("status = %d", resp.StatusCode)
}
if gotPath != "/backend-api/codex/responses" {
t.Fatalf("ChatGPT path = %q", gotPath)
}
if gotAuthorization != "Bearer chatgpt-secret" || gotMetadata != `{"thread":"kept"}` {
t.Fatalf("native headers were not preserved: authorization=%q metadata=%q", gotAuthorization, gotMetadata)
}
if string(gotBody) != string(payload) {
t.Fatalf("ChatGPT body = %q, want %q", gotBody, payload)
}
}
func TestCodexDesktopPassesNativeModelToOpenAIAPIWithAPIKey(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("native model should not reach Ollama")
}))
defer ollama.Close()
chatGPT := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("API-key request should not reach the ChatGPT subscription endpoint")
}))
defer chatGPT.Close()
var gotPath, gotAuthorization, gotOrganization string
openAI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotAuthorization = r.Header.Get("Authorization")
gotOrganization = r.Header.Get("OpenAI-Organization")
w.WriteHeader(http.StatusAccepted)
}))
defer openAI.Close()
handler, err := NewCodexDesktop(CodexDesktopConfig{
OllamaURL: ollama.URL,
ChatGPTURL: chatGPT.URL + "/backend-api/codex",
OpenAIURL: openAI.URL + "/v1",
RoutingCatalogPath: writeCatalog(t, "glm-5.2:cloud"),
})
if err != nil {
t.Fatal(err)
}
proxy := httptest.NewServer(handler)
defer proxy.Close()
req, err := http.NewRequest(
http.MethodPost,
proxy.URL+CodexDesktopPathPrefix+"/v1/responses",
strings.NewReader(`{"model":"gpt-5.6-sol"}`),
)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Authorization", "Bearer sk-test")
req.Header.Set("OpenAI-Organization", "org-test")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusAccepted {
t.Fatalf("status = %d", resp.StatusCode)
}
if gotPath != "/v1/responses" {
t.Fatalf("OpenAI API path = %q", gotPath)
}
if gotAuthorization != "Bearer sk-test" || gotOrganization != "org-test" {
t.Fatalf("OpenAI API headers were not preserved: authorization=%q organization=%q", gotAuthorization, gotOrganization)
}
}
func TestCodexDesktopRejectsManagedAPIKeyForNativeModel(t *testing.T) {
ollamaCalled := false
ollama := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
ollamaCalled = true
}))
defer ollama.Close()
chatGPTCalled := false
chatGPT := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
chatGPTCalled = true
}))
defer chatGPT.Close()
openAICalled := false
openAI := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
openAICalled = true
}))
defer openAI.Close()
handler, err := NewCodexDesktop(CodexDesktopConfig{
OllamaURL: ollama.URL,
ChatGPTURL: chatGPT.URL + "/backend-api/codex",
OpenAIURL: openAI.URL + "/v1",
RoutingCatalogPath: writeCatalog(t, "glm-5.2:cloud"),
})
if err != nil {
t.Fatal(err)
}
proxy := httptest.NewServer(handler)
defer proxy.Close()
req, err := http.NewRequest(
http.MethodPost,
proxy.URL+CodexDesktopPathPrefix+"/v1/responses",
strings.NewReader(`{"model":"gpt-5.6-sol"}`),
)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Authorization", "Bearer "+CodexDesktopManagedAPIKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusUnauthorized {
t.Fatalf("status = %d, want 401: %s", resp.StatusCode, body)
}
if !strings.Contains(string(body), "OpenAI models require signing in") {
t.Fatalf("body = %q, want sign-in recovery", body)
}
if ollamaCalled || chatGPTCalled || openAICalled {
t.Fatalf("managed API key escaped local rejection: ollama=%v chatgpt=%v openai=%v", ollamaCalled, chatGPTCalled, openAICalled)
}
}
func TestCodexDesktopPreservesCompressedNativeRequestWithoutOllamaReasoning(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("native model should not reach Ollama")
}))
defer ollama.Close()
var gotEncoding string
var gotBody []byte
chatGPT := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotEncoding = r.Header.Get("Content-Encoding")
gotBody, _ = io.ReadAll(r.Body)
w.WriteHeader(http.StatusOK)
}))
defer chatGPT.Close()
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL+"/backend-api/codex", writeCatalog(t, "glm-5.3-flash:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
payload := []byte(`{"model":"gpt-5.6-sol","input":[{"type":"reasoning","id":"rs_098c6fb068ce51bf016a9709ab7dcc87d185ecc21991f0f39c","encrypted_content":"gAAAAAB-native"}]}`)
encoder, err := zstd.NewWriter(nil)
if err != nil {
t.Fatal(err)
}
compressed := encoder.EncodeAll(payload, nil)
encoder.Close()
req, err := http.NewRequest(http.MethodPost, proxy.URL+CodexDesktopPathPrefix+"/v1/responses", bytes.NewReader(compressed))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Content-Encoding", "zstd")
req.Header.Set("ChatGPT-Account-ID", "account-123")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
if gotEncoding != "zstd" {
t.Fatalf("content encoding = %q, want zstd", gotEncoding)
}
if !bytes.Equal(gotBody, compressed) {
t.Fatal("native request body was rewritten")
}
}
func TestCodexDesktopFiltersOllamaProviderStateWhenSwitchingToNativeModel(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("native model should not reach Ollama")
}))
defer ollama.Close()
var gotAuthorization, gotEncoding string
var gotBody []byte
chatGPT := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuthorization = r.Header.Get("Authorization")
gotEncoding = r.Header.Get("Content-Encoding")
gotBody, _ = io.ReadAll(r.Body)
w.WriteHeader(http.StatusOK)
}))
defer chatGPT.Close()
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL+"/backend-api/codex", writeCatalog(t, "glm-5.3-flash:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
payload := []byte(`{
"model":"gpt-5.6-sol",
"input":[
{"type":"reasoning","id":"rs_713083","encrypted_content":"The user wants info about the repo"},
{"type":"reasoning","id":"rs_resp_123456","encrypted_content":"More plaintext thinking"},
{"type":"compaction","encrypted_content":"{\"type\":\"ollama_compaction\",\"version\":1,\"summary\":\"local summary\",\"retained\":[]}"},
{"type":"compaction","encrypted_content":"gAAAAAB-native-compaction"},
{"type":"reasoning","id":"rs_098c6fb068ce51bf016a9709ab7dcc87d185ecc21991f0f39c","encrypted_content":"gAAAAAB-native"},
{"type":"message","role":"user","content":[{"type":"input_text","text":"continue"}]}
]
}`)
encoder, err := zstd.NewWriter(nil)
if err != nil {
t.Fatal(err)
}
compressed := encoder.EncodeAll(payload, nil)
encoder.Close()
req, err := http.NewRequest(http.MethodPost, proxy.URL+CodexDesktopPathPrefix+"/v1/responses", bytes.NewReader(compressed))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Content-Encoding", "zstd")
req.Header.Set("Authorization", "Bearer chatgpt-secret")
req.Header.Set("ChatGPT-Account-ID", "account-123")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
if gotAuthorization != "Bearer chatgpt-secret" {
t.Fatalf("authorization = %q", gotAuthorization)
}
if gotEncoding != "" {
t.Fatalf("normalized body retained content encoding %q", gotEncoding)
}
var forwarded struct {
Input []map[string]any `json:"input"`
}
if err := json.Unmarshal(gotBody, &forwarded); err != nil {
t.Fatal(err)
}
if len(forwarded.Input) != 3 {
t.Fatalf("forwarded input = %#v", forwarded.Input)
}
if forwarded.Input[0]["type"] != "compaction" ||
forwarded.Input[0]["encrypted_content"] != "gAAAAAB-native-compaction" {
t.Fatalf("native encrypted compaction was not preserved: %#v", forwarded.Input[0])
}
if forwarded.Input[1]["id"] != "rs_098c6fb068ce51bf016a9709ab7dcc87d185ecc21991f0f39c" ||
forwarded.Input[1]["encrypted_content"] != "gAAAAAB-native" {
t.Fatalf("native encrypted reasoning was not preserved: %#v", forwarded.Input[1])
}
if forwarded.Input[2]["type"] != "message" {
t.Fatalf("message was not preserved: %#v", forwarded.Input[2])
}
}
func TestIsOllamaReasoningItemID(t *testing.T) {
for _, test := range []struct {
id string
want bool
}{
{id: "rs_0", want: true},
{id: "rs_713083", want: true},
{id: "rs_resp_123456", want: true},
{id: "rs_1234567", want: false},
{id: "rs_resp_1234567", want: false},
{id: "rs_098c6fb068ce51bf016a9709ab7dcc87d185ecc21991f0f39c", want: false},
{id: "reasoning_123", want: false},
} {
if got := isOllamaReasoningItemID(test.id); got != test.want {
t.Errorf("isOllamaReasoningItemID(%q) = %v, want %v", test.id, got, test.want)
}
}
}
func TestCodexDesktopRequestsHTTPFallbackForWebSocketUpgrade(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("WebSocket fallback should not reach Ollama")
}))
defer ollama.Close()
chatGPT := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("WebSocket fallback should not reach ChatGPT")
}))
defer chatGPT.Close()
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL+"/backend-api/codex", writeCatalog(t, "glm-5.2:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
req, err := http.NewRequest(http.MethodGet, proxy.URL+CodexDesktopPathPrefix+"/v1/responses", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Connection", "keep-alive, Upgrade")
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
req.Header.Set("Sec-WebSocket-Version", "13")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusUpgradeRequired {
t.Fatalf("status = %d, want 426", resp.StatusCode)
}
}
func TestCodexDesktopStatusReportsObservedRoutes(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer ollama.Close()
chatGPT := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusAccepted)
}))
defer chatGPT.Close()
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL+"/backend-api/codex", writeCatalog(t, "glm-5.2:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
for _, payload := range []string{
`{"model":"glm-5.2:cloud"}`,
`{"model":"gpt-5.6-sol"}`,
} {
req, err := http.NewRequest(http.MethodPost, proxy.URL+CodexDesktopPathPrefix+"/v1/responses", strings.NewReader(payload))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("ChatGPT-Account-ID", "account-123")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
_ = resp.Body.Close()
}
resp, err := http.Get(proxy.URL + CodexDesktopPathPrefix + "/_status")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
var status statusResponse
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
t.Fatal(err)
}
if !status.OK || status.OllamaRequests != 1 || status.ChatGPTRequests != 1 || status.UpstreamErrors != 0 {
t.Fatalf("status = %+v", status)
}
if status.LastModel != "gpt-5.6-sol" || status.LastRoute != "chatgpt" || status.LastUpstreamStatus != http.StatusAccepted {
t.Fatalf("last route = %+v", status)
}
}
func TestCodexDesktopCountersExcludeProbesAndFailedRetries(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer ollama.Close()
chatGPT := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusMethodNotAllowed)
}))
defer chatGPT.Close()
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL+"/backend-api/codex", writeCatalog(t, "glm-5.2:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
probeReq, err := http.NewRequest(http.MethodGet, proxy.URL+CodexDesktopPathPrefix+"/v1/responses", nil)
if err != nil {
t.Fatal(err)
}
probeReq.Header.Set("ChatGPT-Account-ID", "account-123")
probeResp, err := http.DefaultClient.Do(probeReq)
if err != nil {
t.Fatal(err)
}
_ = probeResp.Body.Close()
failedResp, err := http.Post(
proxy.URL+CodexDesktopPathPrefix+"/v1/responses",
"application/json",
strings.NewReader(`{"model":"glm-5.2:cloud"}`),
)
if err != nil {
t.Fatal(err)
}
_ = failedResp.Body.Close()
statusResp, err := http.Get(proxy.URL + CodexDesktopPathPrefix + "/_status")
if err != nil {
t.Fatal(err)
}
defer statusResp.Body.Close()
var status statusResponse
if err := json.NewDecoder(statusResp.Body).Decode(&status); err != nil {
t.Fatal(err)
}
if status.OllamaRequests != 0 || status.ChatGPTRequests != 0 {
t.Fatalf("failed requests were counted: %+v", status)
}
if status.UpstreamErrors != 1 {
t.Fatalf("upstream errors = %d, want 1", status.UpstreamErrors)
}
}
func TestCodexDesktopWritesSafeActivityLog(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer ollama.Close()
activityLogPath := filepath.Join(t.TempDir(), "logs", "codex-proxy.log")
handler, err := NewCodexDesktop(CodexDesktopConfig{
OllamaURL: ollama.URL,
ChatGPTURL: "https://chatgpt.com/backend-api/codex",
RoutingCatalogPath: writeCatalog(t, "glm-5.2:cloud"),
ActivityLogPath: activityLogPath,
})
if err != nil {
t.Fatal(err)
}
proxy := httptest.NewServer(handler)
defer proxy.Close()
prompt := "do-not-record-this-prompt"
req, err := http.NewRequest(
http.MethodPost,
proxy.URL+CodexDesktopPathPrefix+"/v1/responses",
strings.NewReader(`{"model":"glm-5.2:cloud","input":"`+prompt+`"}`),
)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Authorization", "Bearer do-not-record-this-token")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
_ = resp.Body.Close()
// logActivity runs when the server finishes the request, which can be a
// scheduler beat after the client has received the full response. Poll
// for the log line instead of assuming it is already on disk.
var logText string
deadline := time.Now().Add(2 * time.Second)
for {
data, readErr := os.ReadFile(activityLogPath)
if readErr == nil {
logText = string(data)
if strings.Contains(logText, `route=ollama model="glm-5.2:cloud"`) {
break
}
}
if time.Now().After(deadline) {
if readErr != nil {
t.Fatal(readErr)
}
t.Fatalf("activity log missing %q:\n%s", `route=ollama model="glm-5.2:cloud"`, logText)
}
time.Sleep(time.Millisecond)
}
for _, want := range []string{
`route=ollama model="glm-5.2:cloud"`,
"method=POST path=/v1/responses status=200",
"result=ok",
} {
if !strings.Contains(logText, want) {
t.Fatalf("activity log missing %q:\n%s", want, logText)
}
}
for _, secret := range []string{prompt, "do-not-record-this-token", "Authorization"} {
if strings.Contains(logText, secret) {
t.Fatalf("activity log recorded sensitive value %q:\n%s", secret, logText)
}
}
}
func TestCodexDesktopRecordsMidstreamAbortWithoutPanicking(t *testing.T) {
activityLogPath := filepath.Join(t.TempDir(), "codex-proxy.log")
streamErr := errors.New("upstream stream failed")
handler, err := NewCodexDesktop(CodexDesktopConfig{
OllamaURL: "http://127.0.0.1:11434",
ChatGPTURL: "https://chatgpt.com/backend-api/codex",
RoutingCatalogPath: writeCatalog(t, "glm-5.3-flash:cloud"),
ActivityLogPath: activityLogPath,
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
return &http.Response{
Status: "200 OK",
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
Body: io.NopCloser(io.MultiReader(
strings.NewReader("data: partial\n\n"),
errorReader{err: streamErr},
)),
Request: req,
}, nil
}),
})
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(
http.MethodPost,
"http://localhost"+CodexDesktopPathPrefix+"/v1/responses",
strings.NewReader(`{"model":"glm-5.3-flash:cloud"}`),
)
req.RemoteAddr = "127.0.0.1:1234"
req = req.WithContext(context.WithValue(req.Context(), http.ServerContextKey, &http.Server{}))
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", recorder.Code)
}
if got := recorder.Body.String(); got != "data: partial\n\n" {
t.Fatalf("body = %q, want partial event", got)
}
if got := handler.upstreamErrors.Load(); got != 1 {
t.Fatalf("upstream errors = %d, want 1", got)
}
data, err := os.ReadFile(activityLogPath)
if err != nil {
t.Fatal(err)
}
logText := string(data)
for _, want := range []string{
`route=ollama model="glm-5.3-flash:cloud"`,
"status=200",
"result=stream_error",
} {
if !strings.Contains(logText, want) {
t.Fatalf("activity log missing %q:\n%s", want, logText)
}
}
}
func TestCodexDesktopRecordsClientCanceledStream(t *testing.T) {
activityLogPath := filepath.Join(t.TempDir(), "codex-proxy.log")
handler, err := NewCodexDesktop(CodexDesktopConfig{
OllamaURL: "http://127.0.0.1:11434",
ChatGPTURL: "https://chatgpt.com/backend-api/codex",
RoutingCatalogPath: writeCatalog(t, "glm-5.3-flash:cloud"),
ActivityLogPath: activityLogPath,
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
return &http.Response{
Status: "200 OK",
StatusCode: http.StatusOK,
Header: http.Header{"Content-Type": []string{"text/event-stream"}},
Body: io.NopCloser(strings.NewReader("data: ignored\n\n")),
Request: req,
}, nil
}),
})
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(
http.MethodPost,
"http://localhost"+CodexDesktopPathPrefix+"/v1/responses",
strings.NewReader(`{"model":"glm-5.3-flash:cloud"}`),
)
req.RemoteAddr = "127.0.0.1:1234"
ctx, cancel := context.WithCancel(req.Context())
cancel()
req = req.WithContext(ctx)
recorder := &writeErrorResponseWriter{err: context.Canceled}
handler.ServeHTTP(recorder, req)
if recorder.status != http.StatusOK {
t.Fatalf("status = %d, want 200", recorder.status)
}
if got := handler.upstreamErrors.Load(); got != 0 {
t.Fatalf("upstream errors = %d, want 0", got)
}
data, err := os.ReadFile(activityLogPath)
if err != nil {
t.Fatal(err)
}
logText := string(data)
if !strings.Contains(logText, "status=200") || !strings.Contains(logText, "result=canceled") {
t.Fatalf("activity log did not record cancellation:\n%s", logText)
}
}
func TestCodexDesktopRejectsNonLoopbackClients(t *testing.T) {
handler := newTestCodexDesktop(t, "http://127.0.0.1:11434", "https://chatgpt.com/backend-api/codex", writeCatalog(t, "glm"))
req := httptest.NewRequest(http.MethodGet, "http://example.test"+CodexDesktopPathPrefix+"/_health", nil)
req.RemoteAddr = "192.0.2.10:1234"
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
if recorder.Code != http.StatusForbidden {
t.Fatalf("status = %d, want 403", recorder.Code)
}
}
func TestCodexDesktopFailsClosedWhenCatalogIsMissing(t *testing.T) {
handler := newTestCodexDesktop(t, "http://127.0.0.1:11434", "https://chatgpt.com/backend-api/codex", filepath.Join(t.TempDir(), "missing.json"))
req := httptest.NewRequest(http.MethodPost, "http://localhost"+CodexDesktopPathPrefix+"/v1/responses", strings.NewReader(`{"model":"glm"}`))
req.RemoteAddr = "127.0.0.1:1234"
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
if recorder.Code != http.StatusServiceUnavailable {
t.Fatalf("status = %d, want 503: %s", recorder.Code, recorder.Body.String())
}
}
func postCodexRequest(t *testing.T, proxyURL, body string, chatGPT bool) {
t.Helper()
req, err := http.NewRequest(http.MethodPost, proxyURL+CodexDesktopPathPrefix+"/v1/responses", strings.NewReader(body))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
if chatGPT {
req.Header.Set("Authorization", "Bearer chatgpt-secret")
req.Header.Set("ChatGPT-Account-ID", "account-123")
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
responseBody, _ := io.ReadAll(resp.Body)
t.Fatalf("status = %d: %s", resp.StatusCode, responseBody)
}
}
func postCompressedCodexRequest(t *testing.T, proxyURL, body string) {
t.Helper()
encoder, err := zstd.NewWriter(nil)
if err != nil {
t.Fatal(err)
}
compressed := encoder.EncodeAll([]byte(body), nil)
encoder.Close()
req, err := http.NewRequest(http.MethodPost, proxyURL+CodexDesktopPathPrefix+"/v1/responses", bytes.NewReader(compressed))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Content-Encoding", "zstd")
req.Header.Set("Authorization", "Bearer chatgpt-secret")
req.Header.Set("ChatGPT-Account-ID", "account-123")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
responseBody, _ := io.ReadAll(resp.Body)
t.Fatalf("status = %d: %s", resp.StatusCode, responseBody)
}
}
func newTestCodexDesktop(t *testing.T, ollamaURL, chatGPTURL, catalogPath string) *CodexDesktop {
t.Helper()
handler, err := NewCodexDesktop(CodexDesktopConfig{
OllamaURL: ollamaURL,
ChatGPTURL: chatGPTURL,
RoutingCatalogPath: catalogPath,
})
if err != nil {
t.Fatal(err)
}
return handler
}
func writeCatalog(t *testing.T, models ...string) string {
return writeCatalogWithAutoReview(t, "", models...)
}
func writeCatalogWithAutoReview(t *testing.T, autoReviewModel string, models ...string) string {
return writeRoutingCatalog(t, autoReviewModel, "", models...)
}
func writeCatalogWithSelectedAutoReview(t *testing.T, fallbackModel string, models ...string) string {
return writeRoutingCatalog(t, autoReviewSelectedModel, fallbackModel, models...)
}
func writeRoutingCatalog(t *testing.T, autoReviewModel, fallbackModel string, models ...string) string {
t.Helper()
entries := make([]map[string]string, 0, len(models))
for _, model := range models {
entries = append(entries, map[string]string{"slug": model})
}
data, err := json.Marshal(map[string]any{
"models": entries,
"auto_review_model": autoReviewModel,
"auto_review_fallback_model": fallbackModel,
})
if err != nil {
t.Fatal(err)
}
path := filepath.Join(t.TempDir(), CodexDesktopModelCatalogFilename)
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatal(err)
}
return path
}
func autoReviewJSONResponse(arguments string) string {
return autoReviewJSONResponseForTool(guardianDecisionToolName, arguments)
}
type errorReader struct {
err error
}
func (r errorReader) Read([]byte) (int, error) {
return 0, r.err
}
type writeErrorResponseWriter struct {
header http.Header
status int
err error
}
func (w *writeErrorResponseWriter) Header() http.Header {
if w.header == nil {
w.header = make(http.Header)
}
return w.header
}
func (w *writeErrorResponseWriter) WriteHeader(status int) {
w.status = status
}
func (w *writeErrorResponseWriter) Write([]byte) (int, error) {
return 0, w.err
}
func autoReviewJSONResponseForTool(name, arguments string) string {
return fmt.Sprintf(`{"id":"resp_1","status":"completed","output":[{"id":"fc_1","type":"function_call","status":"completed","call_id":"call_1","name":%q,"arguments":%q}]}`, name, arguments)
}
func autoReviewEventStream(t *testing.T, name, arguments string, extraOutput ...map[string]any) []byte {
t.Helper()
inProgressItem := map[string]any{
"id": "fc_1", "type": "function_call", "status": "in_progress", "call_id": "call_1", "name": name, "arguments": "",
}
completedItem := map[string]any{
"id": "fc_1", "type": "function_call", "status": "completed", "call_id": "call_1", "name": name, "arguments": arguments,
}
events := []serverSentEvent{
newServerSentEvent("response.created", map[string]any{"response": map[string]any{"id": "resp_1", "status": "in_progress", "output": []any{}}}),
newServerSentEvent("response.output_item.added", map[string]any{"output_index": 0, "item": inProgressItem}),
newServerSentEvent("response.function_call_arguments.delta", map[string]any{"item_id": "fc_1", "output_index": 0, "delta": arguments}),
newServerSentEvent("response.function_call_arguments.done", map[string]any{"item_id": "fc_1", "output_index": 0, "arguments": arguments}),
newServerSentEvent("response.output_item.done", map[string]any{"output_index": 0, "item": completedItem}),
}
output := []any{completedItem}
for i, item := range extraOutput {
outputIndex := i + 1
itemID := itemString(item, "id")
content := item["content"].([]any)[0].(map[string]any)
text := itemString(content, "text")
events = append(events,
newServerSentEvent("response.output_item.added", map[string]any{"output_index": outputIndex, "item": map[string]any{"id": itemID, "type": "message", "status": "in_progress", "role": "assistant", "content": []any{}}}),
newServerSentEvent("response.content_part.added", map[string]any{"item_id": itemID, "output_index": outputIndex, "content_index": 0, "part": map[string]any{"type": "output_text", "text": "", "annotations": []any{}, "logprobs": []any{}}}),
newServerSentEvent("response.output_text.delta", map[string]any{"item_id": itemID, "output_index": outputIndex, "content_index": 0, "delta": text, "logprobs": []any{}}),
newServerSentEvent("response.output_text.done", map[string]any{"item_id": itemID, "output_index": outputIndex, "content_index": 0, "text": text, "logprobs": []any{}}),
newServerSentEvent("response.content_part.done", map[string]any{"item_id": itemID, "output_index": outputIndex, "content_index": 0, "part": content}),
newServerSentEvent("response.output_item.done", map[string]any{"output_index": outputIndex, "item": item}),
)
output = append(output, item)
}
events = append(events, newServerSentEvent("response.completed", map[string]any{"response": map[string]any{"id": "resp_1", "status": "completed", "output": output}}))
encoded, err := encodeServerSentEvents(events)
if err != nil {
t.Fatal(err)
}
return encoded
}
func autoReviewTextEventStream(t *testing.T, text string) []byte {
t.Helper()
inProgressItem := map[string]any{
"id": "msg_1", "type": "message", "status": "in_progress", "role": "assistant", "content": []any{},
}
content := map[string]any{"type": "output_text", "text": text, "annotations": []any{}, "logprobs": []any{}}
completedItem := map[string]any{
"id": "msg_1", "type": "message", "status": "completed", "role": "assistant", "content": []any{content},
}
events := []serverSentEvent{
newServerSentEvent("response.created", map[string]any{"response": map[string]any{"id": "resp_1", "status": "in_progress", "output": []any{}}}),
newServerSentEvent("response.output_item.added", map[string]any{"output_index": 0, "item": inProgressItem}),
newServerSentEvent("response.content_part.added", map[string]any{"item_id": "msg_1", "output_index": 0, "content_index": 0, "part": map[string]any{"type": "output_text", "text": "", "annotations": []any{}, "logprobs": []any{}}}),
newServerSentEvent("response.output_text.delta", map[string]any{"item_id": "msg_1", "output_index": 0, "content_index": 0, "delta": text, "logprobs": []any{}}),
newServerSentEvent("response.output_text.done", map[string]any{"item_id": "msg_1", "output_index": 0, "content_index": 0, "text": text, "logprobs": []any{}}),
newServerSentEvent("response.content_part.done", map[string]any{"item_id": "msg_1", "output_index": 0, "content_index": 0, "part": content}),
newServerSentEvent("response.output_item.done", map[string]any{"output_index": 0, "item": completedItem}),
newServerSentEvent("response.completed", map[string]any{"response": map[string]any{"id": "resp_1", "status": "completed", "output": []any{completedItem}}}),
}
encoded, err := encodeServerSentEvents(events)
if err != nil {
t.Fatal(err)
}
return encoded
}
func TestCodexDesktopDoesNotAddForwardedHeaders(t *testing.T) {
forwardedHeaders := func(r *http.Request) []string {
var forwarded []string
for _, key := range []string{"X-Forwarded-For", "X-Forwarded-Host", "X-Forwarded-Proto"} {
forwarded = append(forwarded, r.Header.Values(key)...)
}
return forwarded
}
var ollamaForwarded, nativeForwarded []string
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ollamaForwarded = forwardedHeaders(r)
w.WriteHeader(http.StatusOK)
}))
defer ollama.Close()
native := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nativeForwarded = forwardedHeaders(r)
w.WriteHeader(http.StatusOK)
}))
defer native.Close()
catalogPath := writeCatalog(t, "glm-5.2:cloud")
handler := newTestCodexDesktop(t, ollama.URL, native.URL, catalogPath)
proxy := httptest.NewServer(handler)
defer proxy.Close()
for _, model := range []string{"glm-5.2:cloud", "gpt-native"} {
req, err := http.NewRequest(http.MethodPost, proxy.URL+CodexDesktopPathPrefix+"/v1/responses", strings.NewReader(`{"model":"`+model+`"}`))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("ChatGPT-Account-ID", "account-123")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
}
if len(ollamaForwarded) != 0 {
t.Fatalf("Ollama received forwarding headers: %q", ollamaForwarded)
}
if len(nativeForwarded) != 0 {
t.Fatalf("native upstream received forwarding headers: %q", nativeForwarded)
}
}
func TestCodexDesktopPassesThroughRedirectResponse(t *testing.T) {
native := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "https://example.com/moved")
w.WriteHeader(http.StatusFound)
_, _ = io.WriteString(w, "moved")
}))
defer native.Close()
ollama := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("native model should not reach Ollama")
}))
defer ollama.Close()
catalogPath := writeCatalog(t, "glm-5.2:cloud")
handler := newTestCodexDesktop(t, ollama.URL, native.URL, catalogPath)
proxy := httptest.NewServer(handler)
defer proxy.Close()
req, err := http.NewRequest(http.MethodPost, proxy.URL+CodexDesktopPathPrefix+"/v1/responses", strings.NewReader(`{"model":"gpt-native"}`))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("ChatGPT-Account-ID", "account-123")
client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error {
return http.ErrUseLastResponse
}}
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusFound || resp.Header.Get("Location") != "https://example.com/moved" || string(body) != "moved" {
t.Fatalf("redirect passthrough = %d %q %q", resp.StatusCode, resp.Header.Get("Location"), body)
}
}