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):
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;ainvokeaggregates the stream transparently.max_tokensis not forwarded — the backend rejectsmax_output_tokens.- System messages are rewritten to the
developerrole. The Codex backend rejectsrole: "system"(400 System messages are not allowed), and everySymfonicAgentrun sends a system prompt. The provider transparently rewrites outgoingrole: "system"messages torole: "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:
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:
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.