gateway: a Codex on another computer reads magpie's models with a gateway key from GET /v1/codex/models, in Codex's own catalog shape, as its model_catalog_url; /v1/models stays OpenAI's list (#1281, jskorlol)
Codex decodes a model_catalog_url as ModelsResponse {models:[ModelInfo]}
(codex-rs model-provider/src/models_endpoint.rs), which /v1/models' OpenAI
list isn't, and /backend-api/codex/models relays to the ChatGPT backend,
which answers a gateway key (replaced by magpie's token) with 401. The new
route lists the models magpie shows Codex, as codexcat writes them for
model_catalog_json, kept to the key's models (keyAllowed, as /v1/models),
the ChatGPT account's own included (provider.CodexCatalog). Codex reads at
most 1 MiB of such a catalog (MAX_MODEL_CATALOG_BYTES); entries past it are
left out and counted in X-Magpie-Left-Out rather than failing the list.
Verified:
- TestCodexCatalogForAGatewayKey through lanGuard(Handler) from a LAN
address: required ModelInfo fields, unique slugs, held key sees only its
models, no key / wrong key 401, /v1/models unchanged. Without the route it
fails "404 ... magpie serves /v1/chat/completions ..."; with keyAllowed
removed it fails "held key: [plan/m1 spare/m2]".
- TestCodexCatalogFitsCodexsLimit: 300 models -> 127 in 1047147 bytes, 173
left out. Fails 404 without the route.
- Real Codex CLI 0.161.0 (npm) and 0.160.1 in sandbox CODEX_HOMEs against a
sandbox `magpie serve` (0.0.0.0:3591, LAN on, two gateway keys) over
10.0.0.111 with a fake chat upstream: models_cache.json holds the catalog
(held key: fake/fake-coder only; free key: both), no "Unknown model"
fallback warning, codex exec answered PONG-1281 through /v1/responses,
also with the reporter's requires_openai_auth = true. 0.160.1 needs
[features] api_key_model_discovery = true.
- go test -tags nogui ./internal/gateway ./internal/provider in a temp HOME:
ok. Full ./...: 44 packages ok (internal/agent had timed out once
under load; alone it passes in 45s). go vet; GOOS=linux and
GOOS=windows vet of gateway+provider and nogui builds.
Not verified: Codex's TUI /model picker driven by hand.
This commit is contained in:
1 parent
e387c5a2ed
commit
fe405b23e2
7 files changed
+279
-8
No files matched your search
+32
-2
@@ -377,8 +377,8 @@ openai/gpt-5 anthropic/*` (`all` takes the restriction off). A pattern is
|
||||
serves the call, so a bare model name is resolved first, or a routing group,
|
||||
`group/<id>` (`group/*` for every group). A key that names a group may use
|
||||
the group with every member in it, though not those members asked for by
|
||||
name. Such a key sees only its models in `/v1/models` and the Anthropic and
|
||||
Gemini lists, a routing group it doesn't name only when it may use every
|
||||
name. Such a key sees only its models in `/v1/models`, `/v1/codex/models`
|
||||
and the Anthropic and Gemini lists, a routing group it doesn't name only when it may use every
|
||||
member, and is refused any other model with a 403 in the API's error shape
|
||||
before a provider is asked; a fallback it may not use is skipped. A key with
|
||||
no models listed may use every model.
|
||||
@@ -418,6 +418,35 @@ own: `MAGPIE_ADDR=127.0.0.1:3425` behind Tailscale Serve, or one interface's
|
||||
address, stays where it is while shared, and what reaches it from elsewhere
|
||||
still needs an enabled gateway key (#1112).
|
||||
|
||||
**Codex on another computer** (#1281) can name the shared magpie its model
|
||||
provider, with a gateway key, and read magpie's models in Codex's own
|
||||
catalog shape from `GET /v1/codex/models`. `/v1/models` is OpenAI's list,
|
||||
which Codex can't read, and `/backend-api/codex/models` is the ChatGPT
|
||||
backend's, for a Codex signed in to ChatGPT on the gateway's own computer;
|
||||
a gateway key gets a 401 there.
|
||||
|
||||
```toml
|
||||
model_provider = "magpie"
|
||||
model = "provider/model"
|
||||
|
||||
[model_providers.magpie]
|
||||
name = "magpie"
|
||||
base_url = "http://192.168.1.20:3425/v1"
|
||||
model_catalog_url = "http://192.168.1.20:3425/v1/codex/models"
|
||||
env_key = "MAGPIE_API_KEY" # the gateway key
|
||||
wire_api = "responses"
|
||||
supports_websockets = false
|
||||
```
|
||||
|
||||
The list is the models the shared magpie shows Codex (its Agents page
|
||||
picks), each as the `model_catalog_json` magpie writes for a Codex on its
|
||||
own computer describes it, and only the models the key may use. Codex 0.161
|
||||
reads it by default; 0.160 needs `[features] api_key_model_discovery = true`.
|
||||
Codex reads at most 1 MiB of a catalog, and every entry carries Codex's
|
||||
prompt, so past about 125 models the list keeps the first that fit and the
|
||||
answer's `X-Magpie-Left-Out` header says how many it left out: pick fewer
|
||||
for Codex, or hold the key to fewer.
|
||||
|
||||
A request that reaches loopback through a proxy or tunnel on this computer
|
||||
(Cloudflare Tunnel's `cloudflared`, ngrok, Tailscale serve or funnel, frp's
|
||||
HTTP proxy, Caddy, nginx) is someone else's, and is answered as one from
|
||||
@@ -1054,6 +1083,7 @@ It exposes:
|
||||
| `/v1/messages/count_tokens` | Anthropic token counting |
|
||||
| `/v1beta/models/{model}:generateContent` | Google Gemini (also `:streamGenerateContent`, `:countTokens`) |
|
||||
| `/v1/models`, `/v1beta/models` | the catalog |
|
||||
| `/v1/codex/models` | the catalog as Codex's `model_catalog_url` reads it ([Codex on another computer](#providers-and-the-gateway)) |
|
||||
|
||||
Each `/v1/models` entry includes `reasoning` and `supported_reasoning_levels`
|
||||
(`[{"effort":"low"}, ...]`). A routing group is marked `reasoning` when any
|
||||
|
||||
@@ -16,7 +16,7 @@ served by a plugin are in [Provider and plugin ownership](provider-plugins.md).
|
||||
| Part | Responsibility | Source |
|
||||
| --- | --- | --- |
|
||||
| Address and caller | `DefaultAddr`. The local token `magpie` and the per-agent `magpie-<agent>` (`TokenFor`). `agentOf` tells which agent asked, first by token, then by User-Agent. | [`gateway.go`](../../internal/gateway/gateway.go) |
|
||||
| HTTP surface | `Handler` routes `/v1/chat/completions`, `/v1/responses`, `/v1/messages` (each also without `/v1`), `/v1/models`, Gemini's `/v1beta/models/…`, the Codex backend path, `/mcp/{name}` and the `/v1/magpie/*` status endpoints. A GET on `/v1/responses` (a client opening Responses over a WebSocket) is answered 426, a plain one 405: Responses is relayed over HTTP only. Every route is wrapped in `counted`, `corsGuard`, `callerGuard`, `withCaller` and `keyLimited`; `/v1/messages`, `/messages` and `/v1/messages/count_tokens` in `keepsSignIn` too. `corsGuard` (#1051) serves CORS only to the origins listed in Settings › Network › Web pages (`settings.CORSOrigins`, cleaned to `scheme://host[:port]` by `settings.CleanOrigins`, which refuses a wildcard). For a listed origin it answers the preflight (204) and adds `Access-Control-Allow-Origin`, and every other request from it needs an enabled gateway key (`access.Authenticate`), even on loopback, or gets a 401. It takes the key from the caller `lanGuard` recorded too, since on the running server `lanGuard` has already put magpie's own token in the key's place. Any other web page is refused 403 on every route (`foreignPage`): on loopback a call needs no key and the body's Content-Type isn't checked, so a page the user visits could otherwise send a "simple" `text/plain` POST and spend their subscriptions blind. Only an `http`/`https` Origin is judged, and one on this computer (`localhost`, `*.localhost`, `127.0.0.0/8`, `[::1]`, any port) or the request's own origin (its host and port equal to `Host`, such as a page on the gateway's LAN address) passes, as does usemagpie.ai on `/v1/magpie/omarchy`. `null` and other schemes (`app://`, `file://`, `tauri://`, `vscode-webview://`, `chrome-extension://`) come from desktop apps, webviews and extensions that don't enforce CORS, and pass. A request with no Origin (every agent's) passes through with no CORS headers, as before. There is no `Host` check, so a page whose own name resolves to 127.0.0.1 (DNS rebinding) counts as same-origin. | [`gateway.go`](../../internal/gateway/gateway.go), [`cors.go`](../../internal/gateway/cors.go), [`internal/settings/origins.go`](../../internal/settings/origins.go) |
|
||||
| HTTP surface | `Handler` routes `/v1/chat/completions`, `/v1/responses`, `/v1/messages` (each also without `/v1`), `/v1/models`, Gemini's `/v1beta/models/…`, the Codex backend path, `/v1/codex/models` (Codex's catalog shape for a Codex elsewhere with a gateway key, `codexCatalog`, kept to the key's models by `keyAllowed`; #1281), `/mcp/{name}` and the `/v1/magpie/*` status endpoints. A GET on `/v1/responses` (a client opening Responses over a WebSocket) is answered 426, a plain one 405: Responses is relayed over HTTP only. Every route is wrapped in `counted`, `corsGuard`, `callerGuard`, `withCaller` and `keyLimited`; `/v1/messages`, `/messages` and `/v1/messages/count_tokens` in `keepsSignIn` too. `corsGuard` (#1051) serves CORS only to the origins listed in Settings › Network › Web pages (`settings.CORSOrigins`, cleaned to `scheme://host[:port]` by `settings.CleanOrigins`, which refuses a wildcard). For a listed origin it answers the preflight (204) and adds `Access-Control-Allow-Origin`, and every other request from it needs an enabled gateway key (`access.Authenticate`), even on loopback, or gets a 401. It takes the key from the caller `lanGuard` recorded too, since on the running server `lanGuard` has already put magpie's own token in the key's place. Any other web page is refused 403 on every route (`foreignPage`): on loopback a call needs no key and the body's Content-Type isn't checked, so a page the user visits could otherwise send a "simple" `text/plain` POST and spend their subscriptions blind. Only an `http`/`https` Origin is judged, and one on this computer (`localhost`, `*.localhost`, `127.0.0.0/8`, `[::1]`, any port) or the request's own origin (its host and port equal to `Host`, such as a page on the gateway's LAN address) passes, as does usemagpie.ai on `/v1/magpie/omarchy`. `null` and other schemes (`app://`, `file://`, `tauri://`, `vscode-webview://`, `chrome-extension://`) come from desktop apps, webviews and extensions that don't enforce CORS, and pass. A request with no Origin (every agent's) passes through with no CORS headers, as before. There is no `Host` check, so a page whose own name resolves to 127.0.0.1 (DNS rebinding) counts as same-origin. | [`gateway.go`](../../internal/gateway/gateway.go), [`cors.go`](../../internal/gateway/cors.go), [`codex_catalog.go`](../../internal/gateway/codex_catalog.go), [`internal/settings/origins.go`](../../internal/settings/origins.go) |
|
||||
| Model resolution | `serve` resolves the model the agent asked for: Claude tier stand-ins, a routing group (`provider.GroupFor`: its id, the model's id however a vendor spells it, else the one group whose name it is, unless a provider serves a model by that id), auto stand-ins, then `provider.Resolve`. An unknown model gets a 404 naming why: a switched-off provider, a disabled group or an empty group. | [`gateway.go`](../../internal/gateway/gateway.go), [`internal/provider/group.go`](../../internal/provider/group.go) |
|
||||
| Candidates | `plan` lists one provider's candidates: one per key or account on, then each `Fallback` model, with resting ones last (`restLast`). `planGroup` and `planLevel` weigh a group's members together. A nested group plans by its own routing (#576). A member's own fallbacks are not the group's. | [`fallback.go`](../../internal/gateway/fallback.go) |
|
||||
| Order of keys and accounts | `Routing`: smart (""), `order`, `rotate`, `usage` (least used, with an `usageHalfLife` of 1h), `pace` or `weight` (`weightedFirst`). Smart, `usage` and `pace` weigh a subscription by its allowance (`allowances`), and a key by its own usage windows when magpie reads them (`keyAllowance`: a sub2api key given a 5-hour, day or 7-day limit, or a key bought as a plan, such as Kimi Code's or a GLM Coding Plan's); any other key has none. A lone key or account is weighed against nobody, but what it has left is still read into the trace (`weighRouted`), so the Routing page shows it as it does for several (#1016); all but used up, it keeps its conversation rather than being said to move (`affine`). A failure rests a candidate (`failure`, `failureOf`). | [`routing.go`](../../internal/gateway/routing.go), [`weighted.go`](../../internal/gateway/weighted.go), [`internal/provider/routing.go`](../../internal/provider/routing.go) |
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/yetone/magpie/internal/codexcat"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
)
|
||||
|
||||
// CodexCatalogPath is Codex's model list in its own shape, for a Codex that
|
||||
// names magpie its model_provider (base_url <gateway>/v1) on another
|
||||
// computer, with a gateway key (#1281): its model_catalog_url. /v1/models
|
||||
// stays OpenAI's list, which Codex can't read, and CodexPath's list is the
|
||||
// ChatGPT backend's, for a Codex signed in to ChatGPT here.
|
||||
const CodexCatalogPath = "/v1/codex/models"
|
||||
|
||||
// codexCatalogMost is the most of a catalog Codex reads from a
|
||||
// model_catalog_url (MAX_MODEL_CATALOG_BYTES in codex-rs's models
|
||||
// endpoint): past it the whole list fails to load and Codex's picker has
|
||||
// none of magpie's models.
|
||||
const codexCatalogMost = 1 << 20
|
||||
|
||||
// codexCatalog answers CodexCatalogPath: the models magpie shows Codex, as
|
||||
// the model_catalog_json magpie writes for a Codex here describes them,
|
||||
// kept to the models a gateway key may use (keyAllowed, as /v1/models is).
|
||||
// Codex's ?client_version= changes nothing. Entries that would take the
|
||||
// answer past codexCatalogMost are left out, the last first, and the
|
||||
// answer says how many in X-Magpie-Left-Out.
|
||||
func (s *Server) codexCatalog(w http.ResponseWriter, r *http.Request) {
|
||||
// the catalog, the groups and the windows read the providers over and
|
||||
// over: held, they are built once (#746)
|
||||
defer provider.Hold()()
|
||||
shown, _ := provider.CatalogFor("codex")
|
||||
ms := provider.CodexCatalog(keyAllowed(r, shown))
|
||||
head, tail := []byte(`{"models":[`), []byte("]}\n")
|
||||
var b bytes.Buffer
|
||||
b.Write(head)
|
||||
n, out := 0, 0
|
||||
for _, e := range codexcat.Entries(ms, 0) {
|
||||
var one bytes.Buffer
|
||||
enc := json.NewEncoder(&one)
|
||||
enc.SetEscapeHTML(false)
|
||||
if enc.Encode(e) != nil {
|
||||
continue
|
||||
}
|
||||
raw := bytes.TrimRight(one.Bytes(), "\n")
|
||||
if b.Len()+1+len(raw)+len(tail) > codexCatalogMost {
|
||||
out++
|
||||
continue
|
||||
}
|
||||
if n > 0 {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
b.Write(raw)
|
||||
n++
|
||||
}
|
||||
b.Write(tail)
|
||||
if out > 0 {
|
||||
log.Printf("%s: %d of Codex's %d models left out, past the %d bytes Codex reads of a catalog; pick fewer for Codex on the Agents page, or hold the key to fewer", CodexCatalogPath, out, n+out, codexCatalogMost)
|
||||
w.Header().Set("X-Magpie-Left-Out", strconv.Itoa(out))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(b.Bytes())
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/access"
|
||||
"github.com/yetone/magpie/internal/provider"
|
||||
)
|
||||
|
||||
// codexModelInfo is what codex-rs's ModelInfo can't do without
|
||||
// (protocol/src/openai_models.rs): a field missing here fails the whole
|
||||
// list there, "failed to decode models response".
|
||||
type codexModelInfo struct {
|
||||
Slug *string `json:"slug"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
BaseInstructions *string `json:"base_instructions"`
|
||||
Levels *[]map[string]any `json:"supported_reasoning_levels"`
|
||||
ShellType *string `json:"shell_type"`
|
||||
Visibility *string `json:"visibility"`
|
||||
SupportedInAPI *bool `json:"supported_in_api"`
|
||||
Priority *int `json:"priority"`
|
||||
Verbosity *bool `json:"support_verbosity"`
|
||||
Truncation *struct {
|
||||
Mode string `json:"mode"`
|
||||
Limit int `json:"limit"`
|
||||
} `json:"truncation_policy"`
|
||||
Tools *[]string `json:"experimental_supported_tools"`
|
||||
}
|
||||
|
||||
// A Codex on another computer that names magpie its model_provider, with a
|
||||
// gateway key (#1281), reads magpie's models from /v1/codex/models in
|
||||
// Codex's own shape — through the server as it runs, lanGuard and all —
|
||||
// and sees only the models its key may use; another computer without a key
|
||||
// gets nothing, and /v1/models stays OpenAI's list.
|
||||
func TestCodexCatalogForAGatewayKey(t *testing.T) {
|
||||
fresh(t)
|
||||
t.Setenv("MAGPIE_ADDR", "")
|
||||
plan := &fake{t: t, ctype: "application/json", reply: `{}`}
|
||||
spare := &fake{t: t, ctype: "application/json", reply: `{}`}
|
||||
twoProviders(t, plan, spare)
|
||||
keys, secrets := newCaller(t, "Held", "Free")
|
||||
if _, err := access.Update("models-key", access.Change{Key: keys[0].ID, Models: []string{"plan/*"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := lanGuard(New().Handler())
|
||||
host := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r.RemoteAddr = "192.168.1.9:5000" // another computer
|
||||
h.ServeHTTP(w, r)
|
||||
}))
|
||||
t.Cleanup(host.Close)
|
||||
get := func(path, key string) (*http.Response, []byte) {
|
||||
req, _ := http.NewRequest("GET", host.URL+path, nil)
|
||||
if key != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+key)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
b, _ := io.ReadAll(res.Body)
|
||||
return res, b
|
||||
}
|
||||
// what Codex asks: model_catalog_url with ?client_version= after it
|
||||
slugs := func(key string) []string {
|
||||
t.Helper()
|
||||
res, b := get(CodexCatalogPath+"?client_version=0.161.0", key)
|
||||
if res.StatusCode != 200 {
|
||||
t.Fatalf("%d %s", res.StatusCode, b)
|
||||
}
|
||||
var list struct {
|
||||
Models []codexModelInfo `json:"models"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &list); err != nil {
|
||||
t.Fatal(err, string(b))
|
||||
}
|
||||
var out []string
|
||||
for _, m := range list.Models {
|
||||
if m.Slug == nil || m.DisplayName == nil || m.BaseInstructions == nil || *m.BaseInstructions == "" ||
|
||||
m.Levels == nil || m.ShellType == nil || m.Visibility == nil || m.SupportedInAPI == nil ||
|
||||
m.Priority == nil || m.Verbosity == nil || m.Truncation == nil || m.Tools == nil {
|
||||
t.Fatalf("an entry Codex can't read: %s", b)
|
||||
}
|
||||
if slices.Contains(out, *m.Slug) {
|
||||
t.Fatalf("slug %s twice: Codex refuses the list", *m.Slug)
|
||||
}
|
||||
out = append(out, *m.Slug)
|
||||
}
|
||||
return out
|
||||
}
|
||||
if got := slugs(secrets[1]); !slices.Contains(got, "plan/m1") || !slices.Contains(got, "spare/m2") {
|
||||
t.Fatalf("free key: %v", got)
|
||||
}
|
||||
// a key held to some models is listed only those, as on /v1/models
|
||||
if got := slugs(secrets[0]); !slices.Contains(got, "plan/m1") || slices.Contains(got, "spare/m2") {
|
||||
t.Fatalf("held key: %v", got)
|
||||
}
|
||||
if res, b := get(CodexCatalogPath, ""); res.StatusCode != 401 {
|
||||
t.Fatalf("no key: %d %s", res.StatusCode, b)
|
||||
}
|
||||
if res, b := get(CodexCatalogPath, "sk-not-a-key"); res.StatusCode != 401 {
|
||||
t.Fatalf("a wrong key: %d %s", res.StatusCode, b)
|
||||
}
|
||||
// /v1/models is OpenAI's list as it was
|
||||
res, b := get("/v1/models", secrets[1])
|
||||
var l struct {
|
||||
Object string `json:"object"`
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
Models any `json:"models"`
|
||||
}
|
||||
if json.Unmarshal(b, &l); res.StatusCode != 200 || l.Object != "list" || len(l.Data) == 0 || l.Models != nil {
|
||||
t.Fatalf("/v1/models: %d %s", res.StatusCode, b)
|
||||
}
|
||||
}
|
||||
|
||||
// Codex reads at most 1 MiB of a model_catalog_url and refuses the whole
|
||||
// list past it; every entry carries Codex's prompt (~8 KB), so a gateway
|
||||
// with hundreds of models hands Codex as many as fit, and says how many it
|
||||
// left out, rather than a list Codex loads none of.
|
||||
func TestCodexCatalogFitsCodexsLimit(t *testing.T) {
|
||||
fresh(t)
|
||||
var ms []string
|
||||
for i := range 300 {
|
||||
ms = append(ms, fmt.Sprintf("model-%03d", i))
|
||||
}
|
||||
if err := provider.Save(provider.Provider{ID: "many", Name: "Many", Key: "k", Chat: "http://127.0.0.1:9/v1", Models: ms}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
New().Handler().ServeHTTP(w, httptest.NewRequest("GET", CodexCatalogPath, nil))
|
||||
if w.Code != 200 || w.Body.Len() > codexCatalogMost {
|
||||
t.Fatalf("%d, %d bytes", w.Code, w.Body.Len())
|
||||
}
|
||||
var list struct {
|
||||
Models []codexModelInfo `json:"models"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &list); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, _ := strconv.Atoi(w.Header().Get("X-Magpie-Left-Out"))
|
||||
t.Logf("%d listed in %d bytes, %d left out", len(list.Models), w.Body.Len(), out)
|
||||
if len(list.Models) < 100 || out == 0 || len(list.Models)+out != 300 {
|
||||
t.Fatalf("%d listed, %d left out", len(list.Models), out)
|
||||
}
|
||||
if first := *list.Models[0].Slug; !strings.HasPrefix(first, "many/model-000") {
|
||||
t.Fatalf("the first kept is %s", first)
|
||||
}
|
||||
}
|
||||
@@ -522,6 +522,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("POST /_magpie/claude-mcp/{token}", s.subscription.mcpCall)
|
||||
mux.HandleFunc("/mcp/{name}", s.mcpProxy)
|
||||
mux.HandleFunc(CodexPath+"/", s.codexBackend)
|
||||
mux.HandleFunc("GET "+CodexCatalogPath, s.codexCatalog)
|
||||
mux.HandleFunc("GET /v1beta/models", s.geminiModels)
|
||||
mux.HandleFunc("POST /v1beta/models/{call...}", s.gemini)
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -370,7 +370,19 @@ func CodexListed() []catalog.Model {
|
||||
return codexListed(shown, func(id string) []Member {
|
||||
_, ms, _ := find(id)
|
||||
return ms
|
||||
})
|
||||
}, false)
|
||||
}
|
||||
|
||||
// CodexCatalog is shown as a Codex that names magpie its model_provider is
|
||||
// handed it from GET /v1/codex/models (#1281): every model, a ChatGPT
|
||||
// account's own among them, since that Codex reaches them through magpie's
|
||||
// /v1 by magpie's id, not through the ChatGPT backend's list.
|
||||
func CodexCatalog(shown []Entry) []catalog.Model {
|
||||
find := GroupFinder()
|
||||
return codexListed(shown, func(id string) []Member {
|
||||
_, ms, _ := find(id)
|
||||
return ms
|
||||
}, true)
|
||||
}
|
||||
|
||||
// CodexNativeHidden is the ChatGPT account's own model slugs the user took
|
||||
@@ -513,8 +525,9 @@ func codexWindowsTag() []catalog.Model {
|
||||
|
||||
// codexListed marks a group Fast when a ChatGPT account's GPT model is in
|
||||
// it, so Codex offers /fast there too; the tier goes out only to that
|
||||
// account (buildResponses).
|
||||
func codexListed(shown []Entry, members func(id string) []Member) []catalog.Model {
|
||||
// account (buildResponses). own keeps a ChatGPT account's own models in,
|
||||
// which a signed-in Codex has from the backend already.
|
||||
func codexListed(shown []Entry, members func(id string) []Member, own bool) []catalog.Model {
|
||||
var ms []catalog.Model
|
||||
// named among all shown: the account's own, which the backend lists,
|
||||
// are in Codex's picker beside these
|
||||
@@ -523,7 +536,7 @@ func codexListed(shown []Entry, members func(id string) []Member) []catalog.Mode
|
||||
s := settings.Load()
|
||||
find := func(id string) (Group, []Member, bool) { return Group{}, members(id), true }
|
||||
for i, e := range shown {
|
||||
if CodexOwn(e) {
|
||||
if CodexOwn(e) && !own {
|
||||
continue
|
||||
}
|
||||
m := catalog.Model{ID: e.ID, Name: labels[i], Efforts: e.Efforts, Images: e.Images || seen, Context: e.Context, AgentsV2: e.AgentsV2}
|
||||
|
||||
@@ -90,7 +90,7 @@ func TestCodexListedKeepsGroups(t *testing.T) {
|
||||
{ID: "or/glm-5.3", Model: "glm-5.3", Provider: Provider{ID: "or"}},
|
||||
}, func(id string) []Member {
|
||||
return []Member{{Provider: Provider{ID: "or"}, Model: "glm-5.3"}, {Provider: acct, Model: "gpt-5.5"}}
|
||||
})
|
||||
}, false)
|
||||
var ids []string
|
||||
for _, m := range ms {
|
||||
ids = append(ids, m.ID)
|
||||
|
||||
Reference in new issue
Block a user