diff --git a/README.md b/README.md index a67bedf..ab669d8 100644 --- a/README.md +++ b/README.md @@ -94,10 +94,10 @@ The Workers-only install is a smaller rewrite of CPA that fits the free plan. If | Feature | Container | Workers‑only | |---|---|---| -| ChatGPT (Codex) login: text and code | Yes | **No** (use a Codex API key) | -| ChatGPT login: voice | WebSocket and WebRTC | WebSocket only, not yet tested live | +| ChatGPT (Codex) login: text and code | Yes | **No**: chatgpt.com blocks Workers with HTTP 403 (use a Codex API key) | +| ChatGPT login: voice | WebSocket and WebRTC | WebSocket only (WebRTC gets the same 403) | | ChatGPT login: quota in the dashboard | Yes | **No** | -| Claude login | Any app | Claude Code only (other apps need a Claude API key) | +| Claude login | Any app | Claude Code only (Anthropic answers other apps with HTTP 429; use a Claude API key) | | Logins | Every CPA provider | Claude, ChatGPT, Kimi, and Vertex service accounts | | Images, video, speech (`/v1/images`, `/v1/videos`, `/v1/audio/speech`) | Yes | **No** | | Responses WebSocket, `/v1/responses/compact`, legacy `/v1/completions` | Yes | **No** | diff --git a/worker/README.md b/worker/README.md index 5eb94b8..8981782 100644 --- a/worker/README.md +++ b/worker/README.md @@ -12,7 +12,8 @@ It covers **Claude, ChatGPT (Codex) and Kimi subscriptions**, **Vertex AI servic - Each client talks in its own format: Anthropic (`/v1/messages`), OpenAI Chat (`/v1/chat/completions`), OpenAI Responses (`/v1/responses`, or `/backend-api/codex/responses` for the Codex CLI), Gemini (`/v1beta/models/{model}:generateContent`) and Gemini Interactions (`/v1beta/interactions`, Interactions keys only, as in CPA). - Realtime voice through ChatGPT logins, as in CPA: a Realtime WebSocket at `/v1/realtime?model=gpt-realtime` and WebRTC offers at `/v1/realtime/calls` (or `POST /v1/realtime`). Every `gpt-realtime` model is served as `gpt-live-1-codex`. Audio never passes through the Worker, so a long call costs no CPU time. Not yet: Realtime client secrets, sideband sockets, SIP and translations, which answer 501. - Any client reaches any provider. A request the provider can't take as sent is translated, following CPA's translators, with thinking, tool calls, images and streaming. Claude Code and Codex CLI requests to their own logins pass through untouched. -- Claude logins serve Claude Code only: without CPA's cloaking, Anthropic accepts a subscription token from Claude Code alone. Other clients use Claude models through a Claude API key. +- Claude logins serve Claude Code only: without CPA's cloaking, Anthropic accepts a subscription token from Claude Code alone and answers other apps with HTTP 429. Other clients use Claude models through a Claude API key. +- ChatGPT logins serve the Realtime WebSocket only: chatgpt.com blocks every request from a Worker with HTTP 403, so text, code and WebRTC calls fail. Use a Codex API key for text. - Model lists as in CPA: `GET /v1/models`, `GET /v1beta/models` and the dashboard's **Available Models** list every model across logins and keys, from CPA's model catalog (refreshed every three hours). The Codex CLI gets its own catalog format (see `model_catalog_url` below). - CPA's routing, set in the `routing` config section: `round-robin`, `weighted-round-robin` and `fill-first`, failover to the next login or key, retries, and CPA's cooldowns for rate-limited or failing credentials, honouring the provider's stated reset. **Auth Files** shows each login's cooldowns. A request makes at most 10 upstream attempts within 2 minutes, to stay inside Workers Free's limits. - Usage and logs as in CPA: success and failure counts per login and key, and with **Logging to file** on, a **Logs** line per API request (the last 2,000), never with request bodies or keys. diff --git a/worker/src/app.ts b/worker/src/app.ts index 97dfc3e..410bafa 100644 --- a/worker/src/app.ts +++ b/worker/src/app.ts @@ -542,8 +542,9 @@ async function proxied(request: Request, url: URL, route: Route & { format: Form const loginsFor = (provider: LoginEntry["provider"], upstreamModel = bare) => logins.filter((login) => login.provider === provider && (prefix === "" || login.prefix === prefix)).map((login) => (provider === "kimi" || provider === "vertex" ? servedCandidate(login, upstreamModel) : loginCandidate(login, upstreamModel))); if (route.format === "claude" && /^claude-/i.test(bare)) usable.push(...loginsFor("claude")); - // ChatGPT logins take Codex's own models, and any Responses model nothing else serves. - if (route.format === "openai-response" && (inDefaultFamily("codex", bare) || usable.length === 0)) usable.push(...loginsFor("codex")); + // ChatGPT logins take Codex's own models, and any other Responses model nothing else serves, + // except Claude models: those belong to a Claude key or login, never to ChatGPT. + if (route.format === "openai-response" && (inDefaultFamily("codex", bare) || (usable.length === 0 && !/^claude-/i.test(bare)))) usable.push(...loginsFor("codex")); // Claude Code or a Chat client asking for a ChatGPT model by name, e.g. ANTHROPIC_MODEL=gpt-5.6-sol. if ((route.format === "claude" || route.format === "openai") && inDefaultFamily("codex", bare)) usable.push(...loginsFor("codex")); // Vertex service accounts take Gemini models, from any client, translated where needed. diff --git a/worker/test/app.test.ts b/worker/test/app.test.ts index 650ad07..90afe11 100644 --- a/worker/test/app.test.ts +++ b/worker/test/app.test.ts @@ -896,6 +896,14 @@ describe("Claude Code on ChatGPT logins and Codex keys", () => { expect(await sent?.json()).toMatchObject({ model: "gpt-5.6-sol", stream: true, store: false, input: [{ role: "developer" }, { role: "user", content: [{ type: "input_text", text: "hello" }] }] }); }); + it("never sends a Responses client's Claude model to a ChatGPT login", async () => { + const { config, seen } = await loginSetup(); + const response = await handle(at("/v1/responses", { method: "POST", headers: { authorization: `Bearer ${KEY}` }, body: '{"model":"claude-sonnet-5-5","input":"hi"}' }), config); + expect(response.status).toBe(400); + expect(await response.json()).toMatchObject({ error: { message: expect.stringMatching(/Claude logins serve Claude Code only/) } }); + expect(seen).toHaveLength(0); + }); + it("answers a non-streaming request and counts tokens without an upstream call", async () => { const { config, seen } = await loginSetup(); const whole = await handle(messages({ "x-api-key": KEY }, '{"model":"gpt-5.6-sol","max_tokens":8,"messages":[{"role":"user","content":"hello"}]}'), config);