Repository navigation
v0.2.7 — embeddings, direct to provider
POST /v1/embeddings — served directly by a named fallback provider, because the subscription backend has none.
The gap
Everything this proxy fronts goes to chatgpt.com/backend-api, which speaks the Responses API and nothing else: there is no embeddings endpoint behind a ChatGPT account, and there never will be. Any client that wants both chat and embeddings from one OpenAI-compatible base URL (an agent runtime's memory search is the concrete case) had to be pointed at a second, paid endpoint with a second key — and its spend fell outside the per-client attribution this proxy exists for.
Design: direct, not a failover
A new [embeddings] section names one already-declared [[fallback]] provider and reuses its base_url, auth_style and api_key — including the CODEXPROXY_FALLBACK_{NAME}_API_KEY override. No new secret, no new env var:
[embeddings]
provider = "openrouter" # must be a [[fallback]] name
# path = "/embeddings" # appended to the provider's base_url
[embeddings.model_map]
"text-embedding-3-small" = "openai/text-embedding-3-small"
"text-embedding-3-large" = "openai/text-embedding-3-large"The route never touches the account pool and never cascades: one provider, one JSON request, one JSON response. Only model_map'd ids are accepted — anything else is a 400 naming what is, before a byte leaves the proxy. The provider's own id (openai/...) is an implementation detail: the response's model is rewritten back to what the client asked for, the same way /v1/chat/completions echoes echo_model. A provider non-2xx is relayed unchanged. A 2xx body is buffered under max_body_bytes and refused past it rather than truncated — a truncated success body would reach the client as JSON that isn't.
Deliberately not FallbackProvider::send and not FallbackChain::run: the former asks for text/event-stream, the latter treats a non-2xx as "try the next provider", and both are wrong for a single fixed JSON endpoint. Like them, it sends no Codex/ChatGPT headers to a third party.
Startup refuses a section whose provider isn't a declared fallback name or whose model_map is empty, and logs embeddings configured: direct to provider, no pool, no failover once it has loaded. Without the section the route answers a JSON 404 that points at the config, so a client aimed at an unconfigured deploy sees a message, not a bare Axum 404.
Telling it apart from an outage
Every request here carries account=<provider> — the same string a real pool→fallback switch would carry, because it is the same provider. What distinguishes them is the endpoint label, and nothing else was added: no new metric label, no new series on the three existing metrics.
codexproxy_requests_total{endpoint="/v1/embeddings", account="openrouter", ...}is the normal state, not a symptom.- No failover log line is ever emitted for it.
- Usage is booked as prompt tokens; the completion side is
0. modelis clamped to themodel_mapkeys (elseother), the same cardinality guard the chat path applies via its supported-model list — the two sets don't overlap.
Alerts on "served by fallback" must therefore exclude by endpoint, not by provider name: codexproxy_requests_total{account!="data", account!="-", endpoint!="/v1/embeddings"}. EMBEDDINGS_ENDPOINT in src/server.rs documents that coupling.
Full changelog: v0.2.6...v0.2.7