Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** |
Expand Down
3 changes: 2 additions & 1 deletion worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions worker/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions worker/test/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading