proxy: consolidate Codex desktop routing

This commit is contained in:
Eva Ho
2026-09-03 19:42:35 -04:00
parent 3ab06a9f00
commit 58850d25de
6 changed files with 161 additions and 130 deletions
+7 -7
View File
@@ -17,10 +17,10 @@ import (
"time"
"github.com/ollama/ollama/api"
"github.com/ollama/ollama/app/codexproxy"
"github.com/ollama/ollama/cmd/config"
"github.com/ollama/ollama/cmd/internal/fileutil"
"github.com/ollama/ollama/envconfig"
"github.com/ollama/ollama/internal/proxy"
modelpkg "github.com/ollama/ollama/types/model"
)
@@ -29,8 +29,8 @@ const (
codexAppIntegrationName = "codex-app"
codexAppProfileName = "ollama-launch-codex-app"
codexAppBundleID = "com.openai.codex"
codexAppModelCatalogFilename = codexproxy.ModelCatalogFilename
codexAppRoutingCatalogFilename = codexproxy.RoutingCatalogFilename
codexAppModelCatalogFilename = proxy.CodexDesktopModelCatalogFilename
codexAppRoutingCatalogFilename = proxy.CodexDesktopRoutingCatalogFilename
codexAppAutoReviewModelEnv = "OLLAMA_CODEX_AUTO_REVIEW_MODEL"
codexAppOllamaProfileDirName = "chatgpt-ollama"
codexAppOllamaUserDataName = "electron-data"
@@ -548,7 +548,7 @@ func codexAppReplacementLineEnding(replaced string) string {
}
func codexAppProxyBaseURL() string {
return strings.TrimRight(envconfig.ConnectableHost().String(), "/") + codexproxy.PathPrefix + "/v1"
return strings.TrimRight(envconfig.ConnectableHost().String(), "/") + proxy.CodexDesktopPathPrefix + "/v1"
}
type codexAppManagedAuth struct {
@@ -566,7 +566,7 @@ func ensureCodexAppManagedAuth(configPath string) (bool, error) {
}
data, err := json.MarshalIndent(codexAppManagedAuth{
OpenAIAPIKey: codexproxy.ManagedAPIKey,
OpenAIAPIKey: proxy.CodexDesktopManagedAPIKey,
AuthMode: "apikey",
}, "", " ")
if err != nil {
@@ -623,7 +623,7 @@ func isCodexAppManagedAuth(data []byte) bool {
if json.Unmarshal(data, &auth) != nil {
return false
}
return auth.AuthMode == "apikey" && auth.OpenAIAPIKey == codexproxy.ManagedAPIKey
return auth.AuthMode == "apikey" && auth.OpenAIAPIKey == proxy.CodexDesktopManagedAPIKey
}
func (c *CodexApp) Onboard() error {
@@ -726,7 +726,7 @@ func defaultCodexAppRouterHealth() error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
endpoint := strings.TrimRight(envconfig.ConnectableHost().String(), "/") + codexproxy.PathPrefix + "/_health"
endpoint := strings.TrimRight(envconfig.ConnectableHost().String(), "/") + proxy.CodexDesktopPathPrefix + "/_health"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return fmt.Errorf("create ChatGPT router health check: %w", err)
+3 -3
View File
@@ -15,8 +15,8 @@ import (
"time"
"github.com/ollama/ollama/api"
"github.com/ollama/ollama/app/codexproxy"
"github.com/ollama/ollama/cmd/internal/fileutil"
"github.com/ollama/ollama/internal/proxy"
"github.com/ollama/ollama/types/model"
)
@@ -238,7 +238,7 @@ func TestCodexAppConfigureMarksOnlyOllamaModelsForNonChatGPTSessions(t *testing.
func TestCodexAppRouterHealth(t *testing.T) {
t.Run("ready", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != codexproxy.PathPrefix+"/_health" {
if r.URL.Path != proxy.CodexDesktopPathPrefix+"/_health" {
t.Fatalf("health path = %q", r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
@@ -561,7 +561,7 @@ func TestCodexAppManagedAuthLifecycle(t *testing.T) {
if err := json.Unmarshal(data, &auth); err != nil {
t.Fatal(err)
}
if auth.AuthMode != "apikey" || auth.OpenAIAPIKey != codexproxy.ManagedAPIKey {
if auth.AuthMode != "apikey" || auth.OpenAIAPIKey != proxy.CodexDesktopManagedAPIKey {
t.Fatalf("managed auth = %+v", auth)
}
info, err := os.Stat(authPath)
@@ -1,4 +1,4 @@
package codexproxy
package proxy
import (
"bytes"
@@ -22,24 +22,24 @@ import (
)
const (
// PathPrefix is the loopback-only Ollama server route used as Codex's
// openai_base_url. Codex appends /v1/responses and related paths to it.
PathPrefix = "/api/codex"
// CodexDesktopPathPrefix is the loopback-only Ollama server route used as
// Codex's openai_base_url. Codex appends /v1/responses and related paths.
CodexDesktopPathPrefix = "/api/codex"
// ModelCatalogFilename is the combined native and Ollama catalog shown by
// the Codex model picker.
ModelCatalogFilename = "ollama-launch-models.json"
// CodexDesktopModelCatalogFilename is the combined native and Ollama catalog
// shown by the Codex model picker.
CodexDesktopModelCatalogFilename = "ollama-launch-models.json"
// RoutingCatalogFilename contains only Ollama model slugs. Keeping the
// router allow-list separate from the picker catalog ensures native Codex
// CodexDesktopRoutingCatalogFilename contains only Ollama model slugs.
// Keeping the router allow-list separate from the picker catalog ensures native Codex
// models continue to pass through to their normal OpenAI upstream.
RoutingCatalogFilename = "ollama-launch-codex-routing.json"
CodexDesktopRoutingCatalogFilename = "ollama-launch-codex-routing.json"
// ManagedAPIKey is a local sentinel used to let signed-out ChatGPT users
// open Codex directly with Ollama models. It is never valid for an OpenAI
// CodexDesktopManagedAPIKey is a local sentinel used to let signed-out
// ChatGPT users open Codex directly with Ollama models. It is never valid for an OpenAI
// upstream and the router must reject it before any native request leaves
// the machine.
ManagedAPIKey = "ollama-local-codex"
CodexDesktopManagedAPIKey = "ollama-local-codex"
// autoReviewModel is the native Codex reviewer alias. The routing catalog
// may map requests for this alias to a selected Ollama model.
@@ -96,9 +96,8 @@ var hopByHopHeaders = map[string]struct{}{
"upgrade": {},
}
// Config describes the upstreams and Ollama-only routing catalog.
type Config struct {
PathPrefix string
// CodexDesktopConfig describes the upstreams and Ollama-only routing catalog.
type CodexDesktopConfig struct {
OllamaURL string
ChatGPTURL string
OpenAIURL string
@@ -109,10 +108,11 @@ type Config struct {
Transport http.RoundTripper
}
// Handler routes catalog-listed model slugs to Ollama and passes every other
// request to the native upstream selected by Codex's authentication mode.
type Handler struct {
pathPrefix string
// CodexDesktop routes catalog-listed model slugs to Ollama and passes every
// other request to the native upstream selected by Codex's authentication mode.
// The Ollama server mounts this handler on its existing listener, so one
// loopback port serves both routes.
type CodexDesktop struct {
ollamaURL *url.URL
chatGPTURL *url.URL
openAIURL *url.URL
@@ -144,15 +144,7 @@ type statusResponse struct {
LastUpstreamStatus int `json:"last_upstream_status,omitempty"`
}
func New(config Config) (*Handler, error) {
pathPrefix := strings.TrimRight(strings.TrimSpace(config.PathPrefix), "/")
if pathPrefix == "" {
pathPrefix = PathPrefix
}
if !strings.HasPrefix(pathPrefix, "/") {
return nil, fmt.Errorf("Codex proxy path prefix must start with '/': %q", pathPrefix)
}
func NewCodexDesktop(config CodexDesktopConfig) (*CodexDesktop, error) {
ollamaURL, err := parseBaseURL("Ollama", config.OllamaURL)
if err != nil {
return nil, err
@@ -183,8 +175,7 @@ func New(config Config) (*Handler, error) {
transport = http.DefaultTransport.(*http.Transport).Clone()
}
handler := &Handler{
pathPrefix: pathPrefix,
handler := &CodexDesktop{
ollamaURL: ollamaURL,
chatGPTURL: chatGPTURL,
openAIURL: openAIURL,
@@ -213,13 +204,13 @@ func parseBaseURL(name, raw string) (*url.URL, error) {
return u, nil
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (h *CodexDesktop) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if !isLoopbackRequest(r) {
writeJSONError(w, http.StatusForbidden, "Codex proxy only accepts loopback requests")
return
}
suffix, ok := strings.CutPrefix(r.URL.Path, h.pathPrefix)
suffix, ok := strings.CutPrefix(r.URL.Path, CodexDesktopPathPrefix)
if !ok || suffix == "" {
http.NotFound(w, r)
return
@@ -412,7 +403,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.logActivity(started, r.Method, suffix, model, route, resp.StatusCode, result)
}
func (h *Handler) logActivity(started time.Time, method, path, model, route string, status int, result string) {
func (h *CodexDesktop) logActivity(started time.Time, method, path, model, route string, status int, result string) {
h.writeActivity(
"route=%s model=%q method=%s path=%s status=%d duration=%s result=%s",
route,
@@ -425,7 +416,7 @@ func (h *Handler) logActivity(started time.Time, method, path, model, route stri
)
}
func (h *Handler) writeActivity(format string, args ...any) {
func (h *CodexDesktop) writeActivity(format string, args ...any) {
if h.activityLogPath == "" {
return
}
@@ -449,7 +440,7 @@ func (h *Handler) writeActivity(format string, args ...any) {
}
}
func (h *Handler) writeStatus(w http.ResponseWriter) {
func (h *CodexDesktop) writeStatus(w http.ResponseWriter) {
status := statusResponse{
OK: true,
OllamaRequests: h.ollamaRequests.Load(),
@@ -475,7 +466,7 @@ func isAcceptedModelRequest(method, path string, hasModel bool, status int) bool
status < http.StatusMultipleChoices
}
func (h *Handler) readBodies(r *http.Request) ([]byte, []byte, error) {
func (h *CodexDesktop) readBodies(r *http.Request) ([]byte, []byte, error) {
if r.Body == nil {
return nil, nil, nil
}
@@ -1482,7 +1473,7 @@ func usesChatGPTBackend(header http.Header) bool {
func usesManagedAPIKey(header http.Header) bool {
parts := strings.Fields(header.Get("Authorization"))
return len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") && parts[1] == ManagedAPIKey
return len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") && parts[1] == CodexDesktopManagedAPIKey
}
func copyOllamaRequestHeaders(dst, src http.Header) {
@@ -1,4 +1,4 @@
package codexproxy
package proxy
import (
"bytes"
@@ -15,7 +15,7 @@ import (
"github.com/klauspost/compress/zstd"
)
func TestHandlerRoutesCatalogModelToOllamaAndStripsCredentials(t *testing.T) {
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) {
@@ -36,7 +36,7 @@ func TestHandlerRoutesCatalogModelToOllamaAndStripsCredentials(t *testing.T) {
defer chatGPT.Close()
catalogPath := writeCatalog(t, "glm-5.2:cloud")
handler := newTestHandler(t, ollama.URL, chatGPT.URL+"/backend-api/codex", catalogPath)
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL+"/backend-api/codex", catalogPath)
proxy := httptest.NewServer(handler)
defer proxy.Close()
@@ -48,8 +48,8 @@ func TestHandlerRoutesCatalogModelToOllamaAndStripsCredentials(t *testing.T) {
compressed := encoder.EncodeAll(payload, nil)
encoder.Close()
for _, authorization := range []string{"Bearer chatgpt-secret", "Bearer " + ManagedAPIKey} {
req, err := http.NewRequest(http.MethodPost, proxy.URL+PathPrefix+"/v1/responses?trace=1", bytes.NewReader(compressed))
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)
}
@@ -81,6 +81,49 @@ func TestHandlerRoutesCatalogModelToOllamaAndStripsCredentials(t *testing.T) {
}
}
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
@@ -272,7 +315,7 @@ func TestNormalizeOllamaThinkingRejectsMalformedReasoning(t *testing.T) {
}
func TestLoadCatalogModelsReadsOptionalThinkingMetadata(t *testing.T) {
path := filepath.Join(t.TempDir(), ModelCatalogFilename)
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)
@@ -294,7 +337,7 @@ func TestLoadCatalogModelsReadsOptionalThinkingMetadata(t *testing.T) {
}
}
func TestHandlerRoutesAutoReviewToConfiguredOllamaModel(t *testing.T) {
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)
@@ -308,12 +351,12 @@ func TestHandlerRoutesAutoReviewToConfiguredOllamaModel(t *testing.T) {
}))
defer chatGPT.Close()
handler := newTestHandler(t, ollama.URL, chatGPT.URL, writeCatalogWithAutoReview(t, "glm-5.3-flash:cloud", "glm-5.3-flash:cloud"))
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+PathPrefix+"/v1/responses",
proxy.URL+CodexDesktopPathPrefix+"/v1/responses",
"application/json",
strings.NewReader(`{"model":"codex-auto-review","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"review"}]}]}`),
)
@@ -597,7 +640,7 @@ func TestTransformAutoReviewResponsePreservesProviderFailure(t *testing.T) {
}
}
func TestHandlerKeepsAutoReviewOnChatGPTByDefault(t *testing.T) {
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")
}))
@@ -616,11 +659,11 @@ func TestHandlerKeepsAutoReviewOnChatGPTByDefault(t *testing.T) {
}))
defer chatGPT.Close()
handler := newTestHandler(t, ollama.URL, chatGPT.URL, writeCatalog(t, "glm-5.3-flash:cloud"))
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+PathPrefix+"/v1/responses", strings.NewReader(`{"model":"codex-auto-review"}`))
req, err := http.NewRequest(http.MethodPost, proxy.URL+CodexDesktopPathPrefix+"/v1/responses", strings.NewReader(`{"model":"codex-auto-review"}`))
if err != nil {
t.Fatal(err)
}
@@ -639,15 +682,15 @@ func TestHandlerKeepsAutoReviewOnChatGPTByDefault(t *testing.T) {
}
}
func TestHandlerRejectsAutoReviewModelOutsideRoutingCatalog(t *testing.T) {
func TestCodexDesktopRejectsAutoReviewModelOutsideRoutingCatalog(t *testing.T) {
called := false
upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
called = true
}))
defer upstream.Close()
handler := newTestHandler(t, upstream.URL, upstream.URL, writeCatalogWithAutoReview(t, "qwen3:8b", "glm-5.3-flash:cloud"))
req := httptest.NewRequest(http.MethodPost, "http://localhost"+PathPrefix+"/v1/responses", strings.NewReader(`{"model":"codex-auto-review"}`))
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()
@@ -660,7 +703,7 @@ func TestHandlerRejectsAutoReviewModelOutsideRoutingCatalog(t *testing.T) {
}
}
func TestHandlerNormalizesCodexOnlyHistoryForOllama(t *testing.T) {
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)
@@ -672,7 +715,7 @@ func TestHandlerNormalizesCodexOnlyHistoryForOllama(t *testing.T) {
}))
defer chatGPT.Close()
handler := newTestHandler(t, ollama.URL, chatGPT.URL, writeCatalog(t, "glm-5.2:cloud"))
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL, writeCatalog(t, "glm-5.2:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
@@ -688,7 +731,7 @@ func TestHandlerNormalizesCodexOnlyHistoryForOllama(t *testing.T) {
{"type":"future_codex_item","secret":"ignored"}
]
}`
resp, err := http.Post(proxy.URL+PathPrefix+"/v1/responses", "application/json", strings.NewReader(payload))
resp, err := http.Post(proxy.URL+CodexDesktopPathPrefix+"/v1/responses", "application/json", strings.NewReader(payload))
if err != nil {
t.Fatal(err)
}
@@ -726,7 +769,7 @@ func TestHandlerNormalizesCodexOnlyHistoryForOllama(t *testing.T) {
}
}
func TestHandlerPreservesToolSearchAndOllamaCompactionForOllama(t *testing.T) {
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)
@@ -738,7 +781,7 @@ func TestHandlerPreservesToolSearchAndOllamaCompactionForOllama(t *testing.T) {
}))
defer chatGPT.Close()
handler := newTestHandler(t, ollama.URL, chatGPT.URL, writeCatalog(t, "glm-5.3-flash:cloud"))
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL, writeCatalog(t, "glm-5.3-flash:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
@@ -753,7 +796,7 @@ func TestHandlerPreservesToolSearchAndOllamaCompactionForOllama(t *testing.T) {
{"type":"compaction_trigger"}
]
}`
resp, err := http.Post(proxy.URL+PathPrefix+"/v1/responses", "application/json", strings.NewReader(payload))
resp, err := http.Post(proxy.URL+CodexDesktopPathPrefix+"/v1/responses", "application/json", strings.NewReader(payload))
if err != nil {
t.Fatal(err)
}
@@ -779,7 +822,7 @@ func TestHandlerPreservesToolSearchAndOllamaCompactionForOllama(t *testing.T) {
}
}
func TestHandlerFiltersNativeReasoningWhenSwitchingToOllama(t *testing.T) {
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)
@@ -791,7 +834,7 @@ func TestHandlerFiltersNativeReasoningWhenSwitchingToOllama(t *testing.T) {
}))
defer chatGPT.Close()
handler := newTestHandler(t, ollama.URL, chatGPT.URL, writeCatalog(t, "glm-5.3-flash:cloud"))
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL, writeCatalog(t, "glm-5.3-flash:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
@@ -804,7 +847,7 @@ func TestHandlerFiltersNativeReasoningWhenSwitchingToOllama(t *testing.T) {
{"type":"message","role":"user","content":[{"type":"input_text","text":"continue"}]}
]
}`
resp, err := http.Post(proxy.URL+PathPrefix+"/v1/responses", "application/json", strings.NewReader(payload))
resp, err := http.Post(proxy.URL+CodexDesktopPathPrefix+"/v1/responses", "application/json", strings.NewReader(payload))
if err != nil {
t.Fatal(err)
}
@@ -830,7 +873,7 @@ func TestHandlerFiltersNativeReasoningWhenSwitchingToOllama(t *testing.T) {
}
}
func TestHandlerPassesNativeModelToChatGPT(t *testing.T) {
func TestCodexDesktopPassesNativeModelToChatGPT(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("native model should not reach Ollama")
}))
@@ -848,12 +891,12 @@ func TestHandlerPassesNativeModelToChatGPT(t *testing.T) {
}))
defer chatGPT.Close()
handler := newTestHandler(t, ollama.URL, chatGPT.URL+"/backend-api/codex", writeCatalog(t, "glm-5.2:cloud"))
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+PathPrefix+"/v1/responses", bytes.NewReader(payload))
req, err := http.NewRequest(http.MethodPost, proxy.URL+CodexDesktopPathPrefix+"/v1/responses", bytes.NewReader(payload))
if err != nil {
t.Fatal(err)
}
@@ -880,7 +923,7 @@ func TestHandlerPassesNativeModelToChatGPT(t *testing.T) {
}
}
func TestHandlerPassesNativeModelToOpenAIAPIWithAPIKey(t *testing.T) {
func TestCodexDesktopPassesNativeModelToOpenAIAPIWithAPIKey(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("native model should not reach Ollama")
}))
@@ -899,8 +942,7 @@ func TestHandlerPassesNativeModelToOpenAIAPIWithAPIKey(t *testing.T) {
}))
defer openAI.Close()
handler, err := New(Config{
PathPrefix: PathPrefix,
handler, err := NewCodexDesktop(CodexDesktopConfig{
OllamaURL: ollama.URL,
ChatGPTURL: chatGPT.URL + "/backend-api/codex",
OpenAIURL: openAI.URL + "/v1",
@@ -914,7 +956,7 @@ func TestHandlerPassesNativeModelToOpenAIAPIWithAPIKey(t *testing.T) {
req, err := http.NewRequest(
http.MethodPost,
proxy.URL+PathPrefix+"/v1/responses",
proxy.URL+CodexDesktopPathPrefix+"/v1/responses",
strings.NewReader(`{"model":"gpt-5.6-sol"}`),
)
if err != nil {
@@ -939,7 +981,7 @@ func TestHandlerPassesNativeModelToOpenAIAPIWithAPIKey(t *testing.T) {
}
}
func TestHandlerRejectsManagedAPIKeyForNativeModel(t *testing.T) {
func TestCodexDesktopRejectsManagedAPIKeyForNativeModel(t *testing.T) {
ollamaCalled := false
ollama := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
ollamaCalled = true
@@ -956,8 +998,7 @@ func TestHandlerRejectsManagedAPIKeyForNativeModel(t *testing.T) {
}))
defer openAI.Close()
handler, err := New(Config{
PathPrefix: PathPrefix,
handler, err := NewCodexDesktop(CodexDesktopConfig{
OllamaURL: ollama.URL,
ChatGPTURL: chatGPT.URL + "/backend-api/codex",
OpenAIURL: openAI.URL + "/v1",
@@ -971,13 +1012,13 @@ func TestHandlerRejectsManagedAPIKeyForNativeModel(t *testing.T) {
req, err := http.NewRequest(
http.MethodPost,
proxy.URL+PathPrefix+"/v1/responses",
proxy.URL+CodexDesktopPathPrefix+"/v1/responses",
strings.NewReader(`{"model":"gpt-5.6-sol"}`),
)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Authorization", "Bearer "+ManagedAPIKey)
req.Header.Set("Authorization", "Bearer "+CodexDesktopManagedAPIKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
@@ -996,7 +1037,7 @@ func TestHandlerRejectsManagedAPIKeyForNativeModel(t *testing.T) {
}
}
func TestHandlerPreservesCompressedNativeRequestWithoutOllamaReasoning(t *testing.T) {
func TestCodexDesktopPreservesCompressedNativeRequestWithoutOllamaReasoning(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("native model should not reach Ollama")
}))
@@ -1011,7 +1052,7 @@ func TestHandlerPreservesCompressedNativeRequestWithoutOllamaReasoning(t *testin
}))
defer chatGPT.Close()
handler := newTestHandler(t, ollama.URL, chatGPT.URL+"/backend-api/codex", writeCatalog(t, "glm-5.3-flash:cloud"))
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL+"/backend-api/codex", writeCatalog(t, "glm-5.3-flash:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
@@ -1023,7 +1064,7 @@ func TestHandlerPreservesCompressedNativeRequestWithoutOllamaReasoning(t *testin
compressed := encoder.EncodeAll(payload, nil)
encoder.Close()
req, err := http.NewRequest(http.MethodPost, proxy.URL+PathPrefix+"/v1/responses", bytes.NewReader(compressed))
req, err := http.NewRequest(http.MethodPost, proxy.URL+CodexDesktopPathPrefix+"/v1/responses", bytes.NewReader(compressed))
if err != nil {
t.Fatal(err)
}
@@ -1046,7 +1087,7 @@ func TestHandlerPreservesCompressedNativeRequestWithoutOllamaReasoning(t *testin
}
}
func TestHandlerFiltersOllamaProviderStateWhenSwitchingToNativeModel(t *testing.T) {
func TestCodexDesktopFiltersOllamaProviderStateWhenSwitchingToNativeModel(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("native model should not reach Ollama")
}))
@@ -1062,7 +1103,7 @@ func TestHandlerFiltersOllamaProviderStateWhenSwitchingToNativeModel(t *testing.
}))
defer chatGPT.Close()
handler := newTestHandler(t, ollama.URL, chatGPT.URL+"/backend-api/codex", writeCatalog(t, "glm-5.3-flash:cloud"))
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL+"/backend-api/codex", writeCatalog(t, "glm-5.3-flash:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
@@ -1084,7 +1125,7 @@ func TestHandlerFiltersOllamaProviderStateWhenSwitchingToNativeModel(t *testing.
compressed := encoder.EncodeAll(payload, nil)
encoder.Close()
req, err := http.NewRequest(http.MethodPost, proxy.URL+PathPrefix+"/v1/responses", bytes.NewReader(compressed))
req, err := http.NewRequest(http.MethodPost, proxy.URL+CodexDesktopPathPrefix+"/v1/responses", bytes.NewReader(compressed))
if err != nil {
t.Fatal(err)
}
@@ -1148,7 +1189,7 @@ func TestIsOllamaReasoningItemID(t *testing.T) {
}
}
func TestHandlerRequestsHTTPFallbackForWebSocketUpgrade(t *testing.T) {
func TestCodexDesktopRequestsHTTPFallbackForWebSocketUpgrade(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
t.Fatal("WebSocket fallback should not reach Ollama")
}))
@@ -1158,11 +1199,11 @@ func TestHandlerRequestsHTTPFallbackForWebSocketUpgrade(t *testing.T) {
}))
defer chatGPT.Close()
handler := newTestHandler(t, ollama.URL, chatGPT.URL+"/backend-api/codex", writeCatalog(t, "glm-5.2:cloud"))
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+PathPrefix+"/v1/responses", nil)
req, err := http.NewRequest(http.MethodGet, proxy.URL+CodexDesktopPathPrefix+"/v1/responses", nil)
if err != nil {
t.Fatal(err)
}
@@ -1181,7 +1222,7 @@ func TestHandlerRequestsHTTPFallbackForWebSocketUpgrade(t *testing.T) {
}
}
func TestHandlerStatusReportsObservedRoutes(t *testing.T) {
func TestCodexDesktopStatusReportsObservedRoutes(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
@@ -1192,7 +1233,7 @@ func TestHandlerStatusReportsObservedRoutes(t *testing.T) {
}))
defer chatGPT.Close()
handler := newTestHandler(t, ollama.URL, chatGPT.URL+"/backend-api/codex", writeCatalog(t, "glm-5.2:cloud"))
handler := newTestCodexDesktop(t, ollama.URL, chatGPT.URL+"/backend-api/codex", writeCatalog(t, "glm-5.2:cloud"))
proxy := httptest.NewServer(handler)
defer proxy.Close()
@@ -1200,7 +1241,7 @@ func TestHandlerStatusReportsObservedRoutes(t *testing.T) {
`{"model":"glm-5.2:cloud"}`,
`{"model":"gpt-5.6-sol"}`,
} {
req, err := http.NewRequest(http.MethodPost, proxy.URL+PathPrefix+"/v1/responses", strings.NewReader(payload))
req, err := http.NewRequest(http.MethodPost, proxy.URL+CodexDesktopPathPrefix+"/v1/responses", strings.NewReader(payload))
if err != nil {
t.Fatal(err)
}
@@ -1213,7 +1254,7 @@ func TestHandlerStatusReportsObservedRoutes(t *testing.T) {
_ = resp.Body.Close()
}
resp, err := http.Get(proxy.URL + PathPrefix + "/_status")
resp, err := http.Get(proxy.URL + CodexDesktopPathPrefix + "/_status")
if err != nil {
t.Fatal(err)
}
@@ -1230,7 +1271,7 @@ func TestHandlerStatusReportsObservedRoutes(t *testing.T) {
}
}
func TestHandlerCountersExcludeProbesAndFailedRetries(t *testing.T) {
func TestCodexDesktopCountersExcludeProbesAndFailedRetries(t *testing.T) {
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
@@ -1240,11 +1281,11 @@ func TestHandlerCountersExcludeProbesAndFailedRetries(t *testing.T) {
}))
defer chatGPT.Close()
handler := newTestHandler(t, ollama.URL, chatGPT.URL+"/backend-api/codex", writeCatalog(t, "glm-5.2:cloud"))
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+PathPrefix+"/v1/responses", nil)
probeReq, err := http.NewRequest(http.MethodGet, proxy.URL+CodexDesktopPathPrefix+"/v1/responses", nil)
if err != nil {
t.Fatal(err)
}
@@ -1255,7 +1296,7 @@ func TestHandlerCountersExcludeProbesAndFailedRetries(t *testing.T) {
}
_ = probeResp.Body.Close()
failedResp, err := http.Post(
proxy.URL+PathPrefix+"/v1/responses",
proxy.URL+CodexDesktopPathPrefix+"/v1/responses",
"application/json",
strings.NewReader(`{"model":"glm-5.2:cloud"}`),
)
@@ -1264,7 +1305,7 @@ func TestHandlerCountersExcludeProbesAndFailedRetries(t *testing.T) {
}
_ = failedResp.Body.Close()
statusResp, err := http.Get(proxy.URL + PathPrefix + "/_status")
statusResp, err := http.Get(proxy.URL + CodexDesktopPathPrefix + "/_status")
if err != nil {
t.Fatal(err)
}
@@ -1281,15 +1322,14 @@ func TestHandlerCountersExcludeProbesAndFailedRetries(t *testing.T) {
}
}
func TestHandlerWritesSafeActivityLog(t *testing.T) {
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 := New(Config{
PathPrefix: PathPrefix,
handler, err := NewCodexDesktop(CodexDesktopConfig{
OllamaURL: ollama.URL,
ChatGPTURL: "https://chatgpt.com/backend-api/codex",
RoutingCatalogPath: writeCatalog(t, "glm-5.2:cloud"),
@@ -1304,7 +1344,7 @@ func TestHandlerWritesSafeActivityLog(t *testing.T) {
prompt := "do-not-record-this-prompt"
req, err := http.NewRequest(
http.MethodPost,
proxy.URL+PathPrefix+"/v1/responses",
proxy.URL+CodexDesktopPathPrefix+"/v1/responses",
strings.NewReader(`{"model":"glm-5.2:cloud","input":"`+prompt+`"}`),
)
if err != nil {
@@ -1338,9 +1378,9 @@ func TestHandlerWritesSafeActivityLog(t *testing.T) {
}
}
func TestHandlerRejectsNonLoopbackClients(t *testing.T) {
handler := newTestHandler(t, "http://127.0.0.1:11434", "https://chatgpt.com/backend-api/codex", writeCatalog(t, "glm"))
req := httptest.NewRequest(http.MethodGet, "http://example.test"+PathPrefix+"/_health", nil)
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()
@@ -1350,9 +1390,9 @@ func TestHandlerRejectsNonLoopbackClients(t *testing.T) {
}
}
func TestHandlerFailsClosedWhenCatalogIsMissing(t *testing.T) {
handler := newTestHandler(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"+PathPrefix+"/v1/responses", strings.NewReader(`{"model":"glm"}`))
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()
@@ -1362,10 +1402,9 @@ func TestHandlerFailsClosedWhenCatalogIsMissing(t *testing.T) {
}
}
func newTestHandler(t *testing.T, ollamaURL, chatGPTURL, catalogPath string) *Handler {
func newTestCodexDesktop(t *testing.T, ollamaURL, chatGPTURL, catalogPath string) *CodexDesktop {
t.Helper()
handler, err := New(Config{
PathPrefix: PathPrefix,
handler, err := NewCodexDesktop(CodexDesktopConfig{
OllamaURL: ollamaURL,
ChatGPTURL: chatGPTURL,
RoutingCatalogPath: catalogPath,
@@ -1390,7 +1429,7 @@ func writeCatalogWithAutoReview(t *testing.T, autoReviewModel string, models ...
if err != nil {
t.Fatal(err)
}
path := filepath.Join(t.TempDir(), ModelCatalogFilename)
path := filepath.Join(t.TempDir(), CodexDesktopModelCatalogFilename)
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatal(err)
}
+10 -11
View File
@@ -6,30 +6,29 @@ import (
"os"
"path/filepath"
"github.com/ollama/ollama/app/codexproxy"
"github.com/ollama/ollama/envconfig"
"github.com/ollama/ollama/internal/proxy"
)
const (
codexProxyChatGPTURL = "https://chatgpt.com/backend-api/codex"
codexProxyOpenAIURL = "https://api.openai.com/v1"
codexProxyLogFilename = "codex-proxy.log"
codexDesktopChatGPTURL = "https://chatgpt.com/backend-api/codex"
codexDesktopOpenAIURL = "https://api.openai.com/v1"
codexDesktopLogFilename = "codex-proxy.log"
)
func newCodexProxyHandler() (http.Handler, error) {
func newCodexDesktopProxy() (http.Handler, error) {
home, err := os.UserHomeDir()
if err != nil {
slog.Warn("failed to find home directory for Codex proxy", "error", err)
home = os.TempDir()
}
return codexproxy.New(codexproxy.Config{
PathPrefix: codexproxy.PathPrefix,
return proxy.NewCodexDesktop(proxy.CodexDesktopConfig{
OllamaURL: envconfig.ConnectableHost().String(),
ChatGPTURL: codexProxyChatGPTURL,
OpenAIURL: codexProxyOpenAIURL,
RoutingCatalogPath: filepath.Join(home, ".codex", codexproxy.RoutingCatalogFilename),
ActivityLogPath: filepath.Join(home, ".ollama", "logs", codexProxyLogFilename),
ChatGPTURL: codexDesktopChatGPTURL,
OpenAIURL: codexDesktopOpenAIURL,
RoutingCatalogPath: filepath.Join(home, ".codex", proxy.CodexDesktopRoutingCatalogFilename),
ActivityLogPath: filepath.Join(home, ".ollama", "logs", codexDesktopLogFilename),
Logger: slog.Default(),
})
}
+5 -3
View File
@@ -32,13 +32,13 @@ import (
"golang.org/x/sync/errgroup"
"github.com/ollama/ollama/api"
"github.com/ollama/ollama/app/codexproxy"
"github.com/ollama/ollama/auth"
"github.com/ollama/ollama/discover"
"github.com/ollama/ollama/envconfig"
"github.com/ollama/ollama/format"
"github.com/ollama/ollama/fs/ggml"
internalcloud "github.com/ollama/ollama/internal/cloud"
"github.com/ollama/ollama/internal/proxy"
"github.com/ollama/ollama/llm"
"github.com/ollama/ollama/logutil"
"github.com/ollama/ollama/manifest"
@@ -1840,7 +1840,7 @@ func allowedHostsMiddleware(addr net.Addr) gin.HandlerFunc {
}
func (s *Server) GenerateRoutes() (http.Handler, error) {
codexProxyHandler, err := newCodexProxyHandler()
codexDesktopProxy, err := newCodexDesktopProxy()
if err != nil {
return nil, err
}
@@ -1885,7 +1885,9 @@ func (s *Server) GenerateRoutes() (http.Handler, error) {
r.HEAD("/api/version", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"version": version.Version}) })
r.GET("/api/version", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"version": version.Version}) })
r.GET("/api/status", s.StatusHandler)
r.Any(codexproxy.PathPrefix+"/*path", gin.WrapH(codexProxyHandler))
// Codex uses this existing Ollama listener for both native and Ollama
// models. The proxy selects the upstream per request.
r.Any(proxy.CodexDesktopPathPrefix+"/*path", gin.WrapH(codexDesktopProxy))
// Local model cache management (new implementation is at end of function)
r.POST("/api/pull", s.PullHandler)