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
1,439 changes: 1,066 additions & 373 deletions Cargo.lock

Large diffs are not rendered by default.

22 changes: 11 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
[package]
name = "link-assistant-router"
version = "0.19.0"
edition = "2021"
edition = "2024"
description = "Link.Assistant.Router — Claude MAX OAuth proxy and token gateway for Anthropic APIs"
readme = "README.md"
license = "Unlicense"
keywords = ["proxy", "anthropic", "claude", "gateway", "oauth"]
categories = ["web-programming::http-server"]
repository = "https://github.com/link-assistant/router"
documentation = "https://github.com/link-assistant/router"
rust-version = "1.70"
rust-version = "1.85"

[lib]
name = "link_assistant_router"
Expand All @@ -21,29 +21,29 @@ path = "src/main.rs"

[dependencies]
axum = { version = "0.8", features = ["macros"] }
hyper = { version = "1.0", features = ["full"] }
hyper = { version = "1.10", features = ["full"] }
hyper-util = { version = "0.1", features = ["tokio"] }
reqwest = { version = "0.12", features = ["stream", "json"] }
tokio = { version = "1.0", features = ["rt-multi-thread", "macros", "time", "signal", "fs", "process", "sync"] }
reqwest = { version = "0.13", features = ["stream", "json"] }
tokio = { version = "1.52", features = ["rt-multi-thread", "macros", "time", "signal", "fs", "process", "sync"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
jsonwebtoken = "9.0"
jsonwebtoken = { version = "10.0", default-features = false, features = ["rust_crypto"] }
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.0", features = ["v4"] }
tower = { version = "0.5", features = ["util"] }
tower-http = { version = "0.6", features = ["trace", "cors"] }
tower-http = { version = "0.7", features = ["trace", "cors"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
log-lazy = "0.1"
bytes = "1.0"
log-lazy = "0.2"
bytes = "1.11"
futures-util = "0.3"
http = "1.0"
http = "1.4"
http-body-util = "0.1"
lino-arguments = "0.3"
clap = { version = "4", features = ["derive", "env"] }
base64 = "0.22"
async-trait = "0.1"
sha2 = "0.10"
sha2 = "0.11"
hex = "0.4"
aes-gcm = "0.10"

Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Link.Assistant.Router is a transparent proxy that sits between API clients (such

- **Proxies all Anthropic API requests** transparently, including SSE/streaming responses
- **Supports Claude MAX (OAuth)** by reading Claude Code session credentials
- **Vendor subscriptions** — `UPSTREAM_PROVIDER=codex|gemini|qwen` reads each vendor CLI's OAuth credentials read-only (`~/.codex`, `~/.gemini`, `~/.qwen`), refreshes expired tokens in memory, and routes to the ChatGPT/Code Assist/DashScope backend with full dialect translation
- **OpenAI-compatible endpoints** — `/v1/chat/completions`, `/v1/responses`, `/v1/models` translate to Anthropic or forward to a configured OpenAI-compatible provider
- **Optional Gonka upstream** — `UPSTREAM_PROVIDER=gonka` forwards OpenAI-compatible routes to Gonka instead of translating them to Anthropic
- **Optional Crater ForgeFed upstream** — `UPSTREAM_PROVIDER=crater` turns OpenAI chat requests into ForgeFed `Offer{Ticket}` tasks and waits for resolved task results
Expand Down Expand Up @@ -60,6 +61,28 @@ chat requests, delivers a ForgeFed `Offer` containing a `Ticket` to
`CRATER_FORGEFED_INBOX`, reads `Accept.result`, polls that task URI until
`isResolved:true`, and maps the resolved content back to OpenAI JSON or SSE.

### Vendor subscriptions (Codex, Gemini, Qwen)

Set `UPSTREAM_PROVIDER` to `codex`, `gemini`, or `qwen` to serve a vendor
subscription instead of an API key. Clients still authenticate to the router
with their `la_sk_...` token; the router supplies the vendor OAuth token.

| Provider | `UPSTREAM_PROVIDER` (aliases) | Credentials (read-only) | Upstream |
| --- | --- | --- | --- |
| Claude | `anthropic` | `~/.claude/.credentials.json` | `api.anthropic.com` |
| Codex / ChatGPT | `codex` (`chatgpt`, `openai-codex`) | `~/.codex/auth.json` | ChatGPT backend Responses API |
| Gemini | `gemini` (`google`, `code-assist`) | `~/.gemini/oauth_creds.json` | Code Assist `generateContent` |
| Qwen | `qwen` (`qwen-code`, `dashscope`) | `~/.qwen/oauth_creds.json` | DashScope OpenAI-compatible |

The credential files are produced by each vendor's own CLI (run its `login`
once); the router only reads them. Expired tokens are refreshed in memory using
the vendor's public OAuth client — the files on disk are never modified and
secrets are never logged. `/v1/chat/completions` and `/v1/responses` are
translated to each backend's dialect (Codex uses the OpenAI Responses API;
Gemini uses the Code Assist envelope with synthesized SSE for streaming; Qwen is
OpenAI-compatible). Run `router doctor` to verify each credential file is
present and its token valid.

## Quick Start

### Prerequisites
Expand Down
14 changes: 14 additions & 0 deletions changelog.d/20260617_173000_issue_37_case_study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
bump: patch
---

### Added

- Added the issue #37 case-study package under `docs/case-studies/issue-37`,
analyzing how to adopt the best experience from ProxyPal
(`heyhuynhgiabuu/proxypal`) to fully support Claude, Codex, Gemini, and Qwen
subscriptions. Includes a requirement trace (process + functional), file-level
solution plans per requirement, an existing-components survey (CLIProxyAPI,
ProxyPal, LiteLLM, the `oauth2`/`openidconnect` crates), online research with
primary sources for each provider's OAuth endpoints/tokens/quotas, a deep
inventory of ProxyPal and its CLIProxyAPI engine, and raw research snapshots.
31 changes: 31 additions & 0 deletions changelog.d/20260617_180000_issue_37_subscriptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
bump: minor
---

### Added

- Multi-provider subscription support for Codex (ChatGPT), Gemini (Code Assist),
and Qwen (DashScope), alongside the existing Claude support, adopting the best
practices from ProxyPal. The router now reads each vendor CLI's OAuth
credential file read-only (`~/.codex/auth.json`, `~/.gemini/oauth_creds.json`,
`~/.qwen/oauth_creds.json`) via a unified `subscription` module and routes
`/v1/chat/completions`, `/v1/responses`, and `/v1/models` to the correct
upstream.
- `UpstreamProvider::{Codex, Gemini, Qwen}` selectable upstreams with provider
aliases (e.g. `chatgpt`, `google`, `dashscope`).
- Dialect translation between OpenAI Chat Completions, the OpenAI Responses API
(Codex/ChatGPT backend), and the Gemini Code Assist `generateContent` envelope,
including SSE synthesis when a client requests streaming from Gemini.
- In-memory OAuth token refresh: expired Codex/Gemini/Qwen tokens are refreshed
using each vendor's public OAuth client and cached in memory, keeping the proxy
working even when the vendor CLI is not running. Vendor credential files remain
read-only and secrets are never logged.
- `router doctor` now probes the Codex/Gemini/Qwen subscription credential files
and reports whether each is present, valid, or expired.
- Rate-limit headers (`Retry-After`, `x-ratelimit-*`) from subscription upstreams
are relayed to clients so they can back off intelligently.

### Changed

- Updated dependencies to their latest versions and built on the latest stable
Rust (edition 2024).
143 changes: 143 additions & 0 deletions docs/case-studies/issue-37/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Issue 37 Case Study: Adopt the best of ProxyPal — full multi-provider subscription support

## Summary

[Issue #37](https://github.com/link-assistant/router/issues/37) asks us to "use
all the best experience from
[heyhuynhgiabuu/proxypal](https://github.com/heyhuynhgiabuu/proxypal)" so that
the router can **"fully support claude, codex, gemini, qwen, and their
subscriptions with all our features and more."** It then specifies a concrete
*process*: collect the issue's data into `docs/case-studies/issue-37/`, do a deep
case-study analysis (including online research), list **every** requirement,
propose a solution/plan for each, and survey existing components/libraries — all
in PR #38.

This case study delivers that process. It is a **planning and analysis
deliverable**: the functional super-goal (four providers' OAuth subscriptions +
login + auto-config + analytics + a possible GUI) is a multi-PR roadmap, and the
issue's explicit ask is to *analyze it and plan it*, which is exactly what these
documents do. Implementation is then sequenced as follow-up PRs, each verified
against a real subscription (the same discipline issue #35 used for Claude).

## What's in this folder

| File | Purpose |
| --- | --- |
| [`README.md`](./README.md) | This analysis: what ProxyPal teaches, where we stand, the gap, the plan. |
| [`requirements.md`](./requirements.md) | Every requirement (process + functional) traced to a solution and status. |
| [`solution-plans.md`](./solution-plans.md) | One detailed, file-level plan per functional requirement, plus execution order. |
| [`components-survey.md`](./components-survey.md) | Existing components/libraries surveyed, with build-vs-borrow decisions. |
| [`online-research.md`](./online-research.md) | Cited primary sources: each provider's OAuth endpoints, tokens, quotas, 429 handling. |
| [`proxypal-analysis.md`](./proxypal-analysis.md) | Deep inventory of ProxyPal + the CLIProxyAPI engine it wraps. |
| [`raw/`](./raw/) | Captured data: issue JSON/comments + ProxyPal snapshot metadata. |

## The key insight: ProxyPal is a UX shell, not an engine

The most important finding (full evidence in
[`proxypal-analysis.md`](./proxypal-analysis.md)): **ProxyPal implements no proxy
and no OAuth.** It is a Tauri v2 + SolidJS desktop GUI that wraps the Go-based
[**CLIProxyAPI**](https://github.com/router-for-me/CLIProxyAPI) binary as a
sidecar. CLIProxyAPI is the engine that performs the OAuth, the model/API-dialect
translation, the multi-account rotation, and the proxying. ProxyPal's value is
the *experience*: one-click login per provider, auto-configuring installed coding
tools, usage analytics, and lifecycle management.

```
ProxyPal (what the issue points at) link-assistant/router (us)
─────────────────────────────────── ──────────────────────────
Tauri shell (Rust) We already ARE a Rust proxy engine.
└─ SolidJS UI ───────────────────────► Adopt these IDEAS (login, configure,
(login / configure / analytics) analytics) as CLI/JSON surfaces.
└─ CLIProxyAPI engine (Go) ───────► Adopt this ARCHITECTURE (per-provider
(OAuth, translate, rotate) auth + translation + rotation) in OUR
engine — don't embed a foreign one.
```

So "use the best experience from ProxyPal" resolves to **two distinct things**:

1. **Adopt CLIProxyAPI's engine architecture** — per-provider OAuth, a dialect
translation registry, and smart account rotation/cooldown — implemented
*natively in our Rust engine* (we already own one; we shouldn't bolt on a Go
sidecar and throw away the `la_sk_` token gateway, Lino store, Gonka/Crater,
and single-binary deployment that define this project).
2. **Adopt ProxyPal's UX** — login, auto-configure-your-coding-tool, and usage
analytics — re-expressed as `router` subcommands and JSON endpoints (with a
GUI as optional later work).

## Where the router stands today

Full inventory was produced by reading every source file; highlights relevant to
this issue (file evidence in [`requirements.md`](./requirements.md) and
[`solution-plans.md`](./solution-plans.md)):

- **Claude is fully supported.** `src/oauth.rs` reads the real nested
`~/.claude/.credentials.json`, the proxy injects `anthropic-version` +
`anthropic-beta: oauth-2025-04-20`, and `la_sk_` tokens hide the real OAuth
credential (issue #35). This is our model for every other provider.
- **A capable proxy core:** axum server; OpenAI↔Anthropic translation
(`src/openai.rs`, incl. streaming SSE); `/v1/messages`, `/v1/chat/completions`,
`/v1/responses`, `/v1/models`; admin + `/metrics` + `/v1/usage` + `/v1/accounts`.
- **Multi-account routing** (`src/accounts.rs`): round-robin/priority/least-used
with a fixed 60s cooldown on 429 — **Claude-only**.
- **Several upstreams** via `UPSTREAM_PROVIDER`: `anthropic` (default, incl.
Bedrock/Vertex request shapes), `gonka` (signed), `crater` (ForgeFed),
`openai-compatible`/`litellm` (static API key).
- **Scoped tokens:** `la_sk_` JWTs with TTL, revocation, and per-token
`max_requests` budgets; dual Lino-text + binary store.

## The gap (what "fully support codex/gemini/qwen" requires)

| Capability | Today | Needed |
| --- | --- | --- |
| Claude subscription | ✅ file read + beta header | refresh + native login (hardening) |
| Codex/ChatGPT subscription | ❌ API-key only | read `~/.codex` + ChatGPT Responses route; native login |
| Gemini subscription | ❌ (Vertex *shape* only, no Google creds) | read `~/.gemini` + Code Assist route + Gemini translation; native login |
| Qwen subscription | ❌ | read `~/.qwen` → DashScope; device-code login |
| Provider abstraction | one variant (`OpenAICompatible`) | per-provider auth + model map + translation |
| Native OAuth login / device-code | ❌ (reads files only) | `router login <provider>` (PKCE / device-code) via `oauth2` crate |
| Token refresh | ❌ (`refresh_token()` re-reads the file) | real `exchange_refresh_token()` per provider |
| Cross-provider account pool | ❌ Claude-only | mixed-provider pool + `fill-first` + `Retry-After` cooldown |
| Auto-configure client tools | ❌ | `router configure <tool>` + `router doctor` detection |
| Per-provider usage/quota/savings | coarse counters | per-provider token+cost + vendor quota polling |
| GUI / dashboard | ❌ (JSON/Prometheus only) | optional thin web dashboard / desktop wrapper |

## The plan

The full, file-level, phased plan is in
[`solution-plans.md`](./solution-plans.md). The shape:

- **Read before login.** For each new provider, first *read the credential file
the vendor CLI already writes* (Phase 1) — zero OAuth code, immediate
subscription access, exactly how Claude works today — then add native
`router login` (Phase 2) to drop the vendor-CLI dependency.
- **Foundation first.** Generalize the provider abstraction + a translation
registry (Plan 5) before adding Codex (Plan 2), Gemini (Plan 3), Qwen (Plan 4).
- **Borrow Rust libraries, not engines.** Use the `oauth2` crate (PKCE +
device-code + refresh) for login; adopt CLIProxyAPI's rotation/cooldown
*design*; re-express ProxyPal's UX as CLI subcommands.
- **Verify each provider live.** Every implementation PR ships redacted live
evidence against a real subscription, like issue #35's Claude proof.

### Suggested follow-up PR order

1. Provider abstraction + translation registry (Plan 5)
2. Codex via `~/.codex` read (Plan 2 Phase 1)
3. Gemini via `~/.gemini` read + Gemini translation (Plan 3 Phase 1)
4. Native `router login` + refresh (Plan 6)
5. Cross-provider pool + smart cooldown (Plan 7)
6. `router configure` auto-setup (Plan 8)
7. Per-provider usage/quota (Plan 9)
8. Qwen (Plan 4) · 9. Optional GUI (Plan 10)

## Why implementation is not all in this PR

The issue's explicit deliverables (collect/analyze/list/plan/survey) are
**complete here**. The functional super-goal spans four providers' OAuth, native
login + refresh, a generalized provider abstraction, cross-provider routing,
auto-config, and analytics — each of which can only be *verified* against a real
subscription (Codex/Gemini/Qwen credentials are not available in this
environment). Shipping untested OAuth for credentials we cannot exercise would
contradict this repo's "reproduce and verify" discipline. So this PR delivers the
exhaustive, evidence-backed roadmap, and each provider lands in a focused
follow-up PR with its own live proof — the same path issue #35 used to land
Claude correctly.
Loading
Loading