EricFL1998/dsh-subscription-oauth ↗★ 0
dsh-plugin-subscription-oauth
Use subscription accounts as DeepSeek Harness LLM providers — Kimi Code (OAuth device flow), Grok / X Premium (OIDC + PKCE), Google Antigravity (Google OAuth + Cloud Code Assist) and GLM / BigModel (ZCode's browser sign-in) — with per-model visibility, drag-to-reorder Auto fallback and plan quotas from the web Settings page 适合已购买Kimi、Grok等订阅,想在DSH中免Key复用这些额度的用户。
설치
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:EricFL1998/dsh-subscription-oauthdsh-plugin-subscription-oauth
Use the AI subscriptions you already pay for as LLM providers in DeepSeek Harness — no API key. Sign in once per provider from Settings → Subscriptions (订阅) and that account's models join the session model picker, with its plan quota shown next to the connection.
dsh plugin --profile web add dsh-plugin-subscription-oauth
The same package installs straight from the repository, which needs no registry:
dsh plugin --profile web add github:EricFL1998/dsh-subscription-oauth
| Capability | 模型与推理 / Model & inference — it registers LLM provider routes |
| Licence | MIT |
| Built for | DeepSeek Harness 0.2.0-rc.2 (@deepseek-ai/dsh-llm), Node ≥ 20 |
| Platforms | Windows, macOS and Linux; nothing platform-specific beyond Node itself |
| Accounts | your own subscriptions only — credentials stay on your machine |
| Network | the providers' own hosts and the OAuth endpoints listed below; no telemetry, no third-party relay |
| Route | Provider | Login | Wire |
|---|---|---|---|
kimi | Kimi Code | OAuth device flow (RFC 8628) | OpenAI Responses — https://api.kimi.com/coding/v1/responses |
grok | Grok (X Premium) | OIDC authorization code + PKCE on a loopback callback | OpenAI Responses — https://api.x.ai/v1/responses |
antigravity | Google Antigravity | Google authorization code + PKCE (/oauth-callback) plus Cloud AI Companion project discovery | Cloud Code Assist v1internal — streamGenerateContent?alt=sse |
glm | GLM (BigModel) | ZCode's browser sign-in with a polled flow (China region) | OpenAI Responses — https://open.bigmodel.cn/api/coding/paas/v4/responses |
ark | Volcengine Ark | Volcengine SSO (authorization code + PKCE on a loopback callback) | OpenAI Responses — https://ark.cn-beijing.volces.com/api/coding/v3/responses |
auto | Auto | none — it holds no account of its own | delegates to whichever route owns the model it picks |
The Kimi device flow is the one opencodex uses; the Grok flow is the one its
CLI uses (auth.x.ai discovery, client b1a00492-…, port 56121, scope
grok-cli:access); the Antigravity flow is Google's desktop OAuth client
(accounts.google.com, port 51121, scopes including cclog and
experimentsandconfigs), with the account's project discovered through
loadCodeAssist.
GLM signs in the way ZCode does, not with an API key: the plugin asks
zcode.z.ai for a login flow, opens bigmodel.cn/login in the browser, and polls
the flow until the subscription's tokens come back. Nothing has to receive a
zcode://oauth/callback redirect and no app secret is involved, because the
callback is completed server-side and the flow is authenticated by a secret the
plugin generates itself. Allowances are read from BigModel's own
/api/monitor/usage/quota/limit, which meters a rolling five-hour window and a
weekly one.
Ark signs in with Volcengine SSO (authorization code + PKCE on a loopback callback, the flow its own CLI uses) and then has to satisfy two planes that want different credentials. The data plane takes only an ordinary API key as a plain bearer — it refuses STS credentials outright — while the control plane, a different host, is signed with the temporary STS credentials that the SSO token response carries. So a sign-in mints one API key through the control plane (reusing a key this plugin already created, if there is one) and keeps the STS set for the quota, which is read per subscription period and per model's free allowance.
What it registers
| Surface | What appears |
|---|---|
ctx.llm | the kimi, grok, antigravity, glm and auto provider routes (models, reasoning efforts, streaming) |
| Settings | a Subscriptions section: one card per provider — sign in, sign out, default account, token refresh, plan quota, model catalog, manage dialog |
| Model picker | each provider's own models, plus auto as its own route spanning them — Kimi ships kimi-for-coding, k3, k3[1m], k3-256k; Grok seeds grok-4.6, grok-4.5 and adds whatever its live catalog reports; Antigravity lists the agent-callable subset of what its endpoint reports (fetchAvailableModels), filtering out tabs, commit-message and transcription surfaces; GLM ships the glm-5.3 family (glm-5.3, glm-5.3-flash, glm-5.2, glm-5.1, glm-5) and adds whatever its coding host lists |
Credentials live at ~/.dsh/plugins/kimi-oauth/auth.json (mode 0600,
written atomically, one section per provider). The directory keeps the name this
plugin had when it served Kimi Code alone: it holds live logins, so it is left
in place rather than renamed, and every profile that mounted the earlier version
finds its accounts exactly where it left them. This machine's Kimi device id
lives beside it in kimi-device-id; it is stable across restarts, because the
service correlates usage to it.
Token lifetimes, and why the card may say "expired"
Kimi Code access tokens live 15 minutes; the refresh token lives 30 days. Grok access tokens are similar. Every request refreshes automatically when the access token is within five minutes of lapsing, and the Settings card refreshes eagerly while you are looking at it — so what it shows is the live expiry, and "token expired" appears only when a refresh genuinely failed and the account needs a new sign-in. Access-token expiry is normal and does not mean the login is gone.
Settings page
| Every route folded | A subscription card open | A card with its catalog |
|---|---|---|
![]() | ![]() | ![]() |
The page leads with auto, the way to reach every other route at once; the
routes it stands in for follow in the order they were configured. Each route is one
folded row — status dot, name, route key, a one-line summary of the connection,
and an Edit button — so the section reads as a list of providers and opens one
on demand. Every visit starts folded — an open card is a gesture within one
visit, not a stored setting — so re-opening Settings always shows the same compact
list. Inside an open card are the
account rows (★ sets the default account, 退出登录/Sign out removes one), the
model catalog, and the plan quota. Controls follow the host's own settings-form
vocabulary — a 6 px radius (--dsw-radius-sm), a transparent fill, a hairline
border, and the theme's hover/active/focus tokens — declared in one scoped
stylesheet, so the page keeps that look instead of falling back to the browser's
default button chrome.
The Models heading is its own disclosure, folded on every visit too: click it to open the
catalog. Each model has its own switch (a toggle,
not a checkbox), which decides whether the session model picker offers it. The
choice is stored on the host (~/.dsh/plugins/kimi-oauth/preferences.json) as a
hidden list, so it survives restarts and every session sees it; a model added by a
later catalog refresh appears automatically rather than arriving hidden. A hidden
model is listed dimmed and stays resolvable — an existing session that already
selected it keeps working — so hiding is a picker-visibility choice, not a
revocation. · 已隐藏 N 个 in the header counts the hidden ones.
Add model publishes an id this build has never heard of, so a model released
after the plugin was built is usable at once. The id is checked for shape, not
against a list — the endpoint is the only authority on what it serves — and it is
stored per route, published with conservative metadata, and listed with a × so a
typo can be removed again. Drag a row by its ⠿ grip (or press ↑ / ↓ while the
grip is focused) to re-order the list; that order is what the picker follows for
that route.
auto is a provider of its own (route auto), not a model inside each one: it
stands in for the enabled models of every provider the harness serves in one list,
each row badged with the provider that owns it. That includes the routes this plugin
registers and the ones it does not — the built-in DeepSeek providers and any
provider you added yourself — because it enumerates the harness roster
(ctx.llm.listProviders() / listModels()) and dispatches through it rather than
looking only at its own routes. It leads both the Settings section and the model
picker, because it is the one entry that reaches every route. It has no account, so
its card offers no connect or manage — only the list. A switch takes a model out of
Auto without hiding it from the picker, drag re-orders what Auto tries first, and
the arrangement is stored separately (auto.order / auto.off). Switch every model
off and Auto is simply not offered in the picker.
A provider with no signed-in account contributes no rows: listModels answers with
nothing until a route has a session, so signing out removes that route's models from
the picker and from Auto at the same time, and its card hides the catalog instead of
offering models that cannot be called.
Auto falls through to the next model when the provider refuses the preferred one
(renamed or gone, rate-limited, out of allowance, or a transport failure). A
concrete model id never falls back — it dispatches to exactly that model. Once a
stream has produced any output the turn belongs to that model, so a later failure
is reported instead of being papered over by a second model's text. Every fallback
is logged (model X unavailable … falling back to Y).
Plan and usage shows the subscription tier and every allowance the provider
states, with a meter and a reset time: Kimi's rolling 5-hour window, weekly window
and included credit (GET /coding/v1/usages); Grok's weekly included credit
(cli-chat-proxy.grok.com/v1/billing?format=credits, plus the plan tier from the
token's own tier claim); and Antigravity's four buckets — Gemini and Claude,
each rolling and weekly — read from v1internal:retrieveUserQuotaSummary, falling
back to the per-model quotaInfo of fetchAvailableModels (worst-spent model per
family, classified by id, display name and tier, so an Opus row counts as
Claude). A provider that states nothing says so instead of inventing a figure.
A reset is stated by how far off it is, on every route: minutes within the hour
(40 分钟后重置), hours within the day (4 小时后重置), and the date beyond that
(重置时间 2026/10/17 13:54) — the count is what a reader acts on when deciding whether
there is room left, and a date is what they need when it is a week away. Because the
page re-reads the figures on a slow interval, the count follows along while it is
open.
Manage opens a dialog with per-account token refresh, the credential-file path, and the resolved endpoints.
While a device authorization is pending the card shows the code, its expiry, and the buttons to open or copy it, and the status dot turns amber. A loopback provider (Grok) instead shows a sign-in link and waits for the browser redirect.
Install
dsh plugin --profile web add dsh-plugin-subscription-oauth
web is the profile that serves the Settings page; use desktop (or any other
profile name) if that is the one you run.
The CLI forwards its arguments to pnpm, so a pinned version, a git URL and a local path are all spelled the same way:
dsh plugin --profile web add dsh-plugin-subscription-oauth@0.2.2
dsh plugin --profile web add github:EricFL1998/dsh-subscription-oauth
dsh plugin --profile web add /path/to/dsh-plugin-subscription-oauth
Restart dsh web (or the desktop app) afterwards so the new bundle loads. A local
directory is installed as a link:, so afterwards npm run build in the checkout
is enough to publish a change; restart again to pick it up.
The host packages (@deepseek-ai/dsh-llm and the rest) are optional peers: the
harness provides them at runtime, so installing this plugin does not fetch a second
copy of the host it is running inside.
Releasing (maintainers)
prepublishOnly rebuilds lib/ — the TypeScript output plus the browser bundle — so
a published tarball can never carry a stale client:
npm version patch # or minor/major
npm publish
For a headless composition that only needs the model route (no Settings page):
dsh --profile headless --patch /path/to/dsh-plugin-subscription-oauth/overlay.yml "your task"
where overlay.yml is a one-entry patch naming
/path/to/dsh-plugin-subscription-oauth/lib/index.js. Log in once from a web profile
first — the token file is shared.
Hiding a built-in provider from the picker
A plugin cannot unregister another bundle's route (ctx.llm has no unregister
API), so hiding a built-in provider is a profile-patch change. To hide the
DeepSeek API-key route and its models while keeping the DeepSeek account
(sign-in) route:
# $DSH_HOME/profiles/
/cordis.patch.yml
- id: llm-deepseek
name: "@deepseek-ai/dsh-llm-deepseek-api-key"
config:
models: []
models replaces the adapter's default catalog, so deepseek-official reports
no models and the picker hides the empty group (Empty groups are hidden); the
route stays registered, so a session that already saved one of its model ids
keeps working. Leave llm-deepseek-account alone to keep the account route.
Restart the profile (or let HMR reload) for the change to apply.
Use
- Open Settings → Subscriptions (订阅) and press Connect on the card you want.
- Finish the sign-in the card shows: Kimi Code and (when answered) a device code you approve in the browser; Grok and Antigravity open a sign-in page and wait for the redirect. The section updates by itself while an attempt is pending.
- Open the model picker in any session and choose a model under that provider — or leave it on Auto to let the enabled models be tried in order.
Not signed in? The provider stays out of the picker, and a request fails with
INVALID_CREDENTIAL pointing at the Settings page. Nothing else breaks.
Multiple accounts are supported: connect again to add another. The ★ default account serves requests; use Use by default to switch.
Config
- id: llm-subscription-oauth
name: dsh-plugin-subscription-oauth
config:
providers: [kimi, grok, antigravity] # routes to register; all are default
enabled: true
discovery: true # consult each endpoint's live model list
streamIdleTimeoutMs: 300000 # abandon a silent stream after this
defaultModel: kimi-for-coding # model the picker preselects on the Kimi route
extraModels: [] # Kimi ids to publish without shipped metadata
# oauthHost: https://auth.kimi.com
# apiBase: https://api.kimi.com/coding/v1
Only the Kimi route takes oauthHost/apiBase/defaultModel/extraModels: the
Grok and Antigravity endpoints are fixed by their own clients, and their model
lists come from the live catalog. Omitting a route from providers simply leaves
it unregistered.
oauthHost and apiBase can also come from the environment
(KIMI_CODE_OAUTH_HOST / KIMI_OAUTH_HOST, KIMI_CODE_API_BASE /
KIMI_API_BASE); an explicit config value wins. apiBase accepts either the
bare base or the full .../responses URL.
How it works
The package is a standard DeepSeek Harness plugin: lib/index.js exports
`apply(


