Skip to content

Subscription OAuth providers (dev/test only)

Run agents against a Claude, ChatGPT, or Kimi subscription using the OAuth token your CLI already stored — no metered API key. This is a local development and testing convenience only. None of these providers bill per tenant, refresh tokens automatically, or are safe under multi-tenant / production load.

Three providers implement it:

Provider Backend Credential source Auth style
AnthropicOAuthProvider Anthropic Messages API Claude Code OAuth token (Keychain / ~/.claude/.credentials.json) auto-loaded from the local store
CodexOAuthProvider OpenAI Codex (chatgpt.com/backend-api/codex) ChatGPT subscription token env vars (OPENAI_CHATGPT_*)
KimiOAuthProvider Kimi / Moonshot (OpenAI-compatible) kimi.com subscription token env vars (KIMI_OAUTH_*)

Local/dev only — never deploy to production

Each provider replays a personal subscription OAuth token to a first-party backend from a third-party program (subscription-auth reuse). Depending on the vendor's Terms of Service this may be throttled or disallowed — the vendors ship their own sanctioned CLIs (claude -p, Codex CLI, kimi-cli) for subscription-backed use. These providers exist so a single developer can run the full pipeline (autonomous learning, metacognition, the ReAct loop) against a real LLM without provisioning a metered API key. For production, use the metered providers (AnthropicProvider, OpenAIProvider, KimiProvider) with a real API key. Every OAuth provider refuses to construct in a production-like environment (see Production guard).


AnthropicOAuthProvider

Reuses your Claude Code OAuth token (macOS Keychain item Claude Code-credentials, or ~/.claude/.credentials.json after claude login) to call the Anthropic Messages API on a Claude Pro/Max subscription — no ANTHROPIC_API_KEY.

Credentials

Sign in with Claude Code once; the provider auto-loads the token from the local store (no env var or manual export needed):

claude login          # writes the Keychain item / ~/.claude/.credentials.json

The token is never logged (only a short fingerprint). The provider swaps in an Anthropic SDK client configured with Authorization: Bearer <token> and the oauth-2025-04-20 beta header.

Usage

from symfonic.core.oauth_provider import AnthropicOAuthProvider
from symfonic.core.config import ModelConfig

provider = AnthropicOAuthProvider()  # auto-loads the Claude Code token
chat = provider.get_chat_model(ModelConfig(model_name="claude-haiku-4-5"))
response = await chat.ainvoke("Reply with the single word: pong")

# Or hand it straight to an agent:
from symfonic.agent.engine import SymfonicAgent
agent = SymfonicAgent(model_provider=AnthropicOAuthProvider())

A SystemMessage round-trips normally, so real SymfonicAgent runs (which always send a system prompt) work out of the box. See examples/tui_chat/ for a zero-infra example.


CodexOAuthProvider

Calls the OpenAI Codex backend with your ChatGPT Plus/Pro/Team plan, so you can use the latest Codex-served models (e.g. gpt-5.6-sol) without a metered OPENAI_API_KEY.

Credentials

Sign in with the Codex CLI once, or export the token yourself:

codex login                              # writes ~/.codex/auth.json
# --- or ---
export OPENAI_CHATGPT_ACCESS_TOKEN=...   # required
export OPENAI_CHATGPT_ACCOUNT_ID=...     # optional -> chatgpt-account-id header
export OPENAI_CHATGPT_BASE_URL=...        # optional -> override the backend URL

The provider reads the env vars; the bundled example additionally falls back to ~/.codex/auth.json. The token is never logged (only a short fingerprint).

Usage

from symfonic.core.codex_provider import CodexOAuthProvider
from symfonic.core.config import ModelConfig

provider = CodexOAuthProvider()  # reads OPENAI_CHATGPT_* from the environment
chat = provider.get_chat_model(ModelConfig(model_name="gpt-5.6-sol"))
response = await chat.ainvoke("Reply with the single word: pong")

Runnable example

python -m examples.codex_oauth
python -m examples.codex_oauth "Explain a Python decorator in one line."

The Codex backend wire contract

The Codex backend is not the general OpenAI API — it has stricter request requirements. get_chat_model() handles them for you on the default (Responses-API) path:

  • store=False — the backend rejects server-side response storage.
  • streaming=True — the backend accepts streaming requests only; ainvoke aggregates the stream transparently.
  • max_tokens is not forwarded — the backend rejects max_output_tokens.
  • System messages are rewritten to the developer role. The Codex backend rejects role: "system" (400 System messages are not allowed), and every SymfonicAgent run sends a system prompt. The provider transparently rewrites outgoing role: "system" messages to role: "developer" (the Responses API's accepted alternative) on the Responses-API path, so agents work without any caller change.

You do not set these yourself; they are applied automatically whenever use_responses_api=True (the default).

Pointing at a non-Codex gateway

To reuse the provider against an OpenAI-compatible Chat-Completions gateway (vLLM, LiteLLM, Azure, …) instead of the Codex backend, disable the Responses API. On that path the Codex-only fields (including the system→developer rewrite) are not injected and config.max_tokens is forwarded:

CodexOAuthProvider(base_url="https://my-gateway/v1", use_responses_api=False)

KimiOAuthProvider

Calls the Kimi / Moonshot OpenAI-compatible backend with your kimi.com subscription token, so you can use Kimi models (e.g. kimi-for-coding) without a metered MOONSHOT_API_KEY.

Credentials

Sign in with kimi-cli once, then export the token:

kimi-cli login                           # writes ~/.kimi-code/credentials/kimi-code.json
export KIMI_OAUTH_ACCESS_TOKEN=...       # required — the subscription access token
export KIMI_OAUTH_BASE_URL=...           # optional — see the endpoint note below
export KIMI_OAUTH_PLATFORM=...           # optional -> X-Msh-Platform header

The provider is env-vars-only: it does not run the device-auth flow and does not embed Moonshot's CLI client identity. You supply the token yourself.

It defaults to the kimi.com subscription (coding) endpoint (https://api.kimi.com/coding/v1, model kimi-for-coding) — the only endpoint a subscription OAuth token authenticates against — so subscription tokens work without any override.

Endpoint, model, and token lifetime

A kimi.com subscription OAuth token authenticates only against https://api.kimi.com/coding/v1 (the default). The metered api.moonshot.ai platform endpoint rejects it with 401 — override KIMI_OAUTH_BASE_URL only if your token is authorized elsewhere. Note also: kimi-for-coding (K2.7) accepts only temperature=1, and the subscription access token is a short-lived (~15 min) JWT that kimi-cli refreshes on use — this provider does not refresh, so an expired token returns 401. Refresh it with any kimi-cli call, e.g. kimi -p "ping".

Usage

from symfonic.core.kimi_oauth_provider import KimiOAuthProvider
from symfonic.core.config import ModelConfig

provider = KimiOAuthProvider()  # defaults to the coding endpoint + kimi-for-coding
chat = provider.get_chat_model(
    ModelConfig(model_name="kimi-for-coding", temperature=1.0),
)
response = await chat.ainvoke("Reply with the single word: pong")

Kimi rides OpenAI-compatible Chat Completions, where role: "system" is valid, so system prompts round-trip without any rewrite.

Auto-refresh (no manual token juggling)

The kimi.com access token is a short-lived (~15 min) JWT. Instead of exporting it by hand and re-running kimi when it expires, let the provider manage it:

provider = KimiOAuthProvider.from_kimi_cli()  # after `kimi-cli` login, once

from_kimi_cli() reads ~/.kimi-code/credentials/kimi-code.json directly and auto-refreshes on every request using the stored refresh token — so a long agent session never 401s mid-run, with no manual step. The rotated refresh token is written back to the credential file (behind a file lock + atomic write) so your kimi-cli login stays valid. The plain KimiOAuthProvider() constructor also auto-detects this file when KIMI_OAUTH_ACCESS_TOKEN is unset.

Passing an explicit KIMI_OAUTH_ACCESS_TOKEN (or access_token=) opts out of auto-refresh — you own the token's lifetime in that mode.

When auth can't be auto-recovered

If the refresh token itself is expired or revoked, refresh can't recover it and the provider raises KimiCredentialError telling you to run kimi-cli login again. A 401/403 during a call also logs a clear WARNING naming the fix, rather than surfacing an opaque SDK error. (Every OAuth provider does this — Codex points you at codex login, Anthropic at claude login.)

Runnable example

A ready-to-run demo ships under examples/kimi_oauth/:

python -m examples.kimi_oauth
python -m examples.kimi_oauth "Explain a Python decorator in one line."

Production guard

Every OAuth provider refuses to construct in a production-like environment (SYMFONIC_ENV / ENVIRONMENT / ENV / NODE_ENV set to production / prod) to prevent accidental subscription billing under multi-tenant load:

CodexOAuthProvider()                      # raises CodexProductionMisuseError in prod
CodexOAuthProvider(allow_production=True)  # explicit, ToS-reviewed override

KimiOAuthProvider behaves identically (KimiProductionMisuseError). AnthropicOAuthProvider also guards, but additionally exposes a credentials_resolver seam for advanced local multi-account setups; for the Codex and Kimi providers allow_production=True is the sole escape hatch and the guard fires regardless of how the token is supplied.


Verifying against the real backend

Subscription-OAuth wire behavior can only be proven with a live call, so gated tests exist under tests/integration/live_llm/ (deselected by default via the live_llm marker). Each skips cleanly when no credentials are present and bills a tiny amount against your subscription:

# All three, using whatever local credentials you have:
pytest -m live_llm \
  tests/integration/live_llm/test_anthropic_oauth_provider_live.py \
  tests/integration/live_llm/test_codex_provider_live.py \
  tests/integration/live_llm/test_kimi_oauth_provider_live.py -s

Each test sends a SystemMessage + HumanMessage — the exact shape a real SymfonicAgent run produces — and asserts a non-empty response, so they prove the full agent path, not just a bare completion.

Limitations

  • Token refresh. Only KimiOAuthProvider.from_kimi_cli() auto-refreshes (see above). Codex and Anthropic do not refresh — when their token expires (401), re-run the vendor CLI (codex login / claude login). In every case an unrecoverable auth failure logs/raises a clear message naming the CLI to re-run, so you are never left guessing.
  • Not multi-tenant / not billable per user. For a single developer's machine only.
  • Subscription model availability. Model availability follows what your plan exposes; behavior may differ from the same model via the metered API.