droid-oauth-opencode
v0.3.4
Published
OpenCode plugin for Factory (Droid) LLM proxy models — Claude (incl. Fable 5, Opus Fast Modes) on the Anthropic route plus Droid Core (GLM-5.2 Fast, Kimi, MiniMax, DeepSeek) on the OpenAI route — via the local Factory CLI credential.
Maintainers
Readme
droid-oauth-opencode
Use Factory (Droid) proxied models inside OpenCode, authenticated with the credential the Factory droid CLI already stores on your machine. No API key to paste, no local proxy server, no opencode auth login step.
Two providers are registered:
| OpenCode provider | Route | Models |
| --- | --- | --- |
| factory | /api/llm/a/v1 (Anthropic Messages API; SDK appends /messages) | Claude Opus 4.8/4.7/4.6/4.5 (+ Fast Modes), Fable 5, Sonnet 5/4.6/4.5, Haiku 4.5 |
| factory-g | /api/llm/o/v1 (OpenAI chat/completions) | GLM-5.2 Fast, GLM-5.2, Kimi K2.7 Code, Kimi K2.6, MiniMax M3, MiniMax M2.7, DeepSeek V4 Pro |
factory-g/glm-5.2-fast is Factory's "Droid Core" GLM-5.2 running on Fireworks — the fast variant that has no equivalent in the stock Z.AI / Devin providers. Anthropic Opus *-fast variants send speed: "fast" and the fast-mode-2026-02-01 anthropic-beta flag (same as the droid CLI).
Install
npx droid-oauth-opencode setup --globalThen restart OpenCode and pick a factory/* or factory-g/* model. The plugin reuses the droid CLI credential automatically. If you have never logged in with the droid CLI, run droid once first (or export FACTORY_API_KEY=fk-...).
Project-local install (writes ./opencode.json instead of the global config):
npx droid-oauth-opencode setup --projectHow it works
Both routes live on https://app.factory.ai behind one WorkOS bearer token. The plugin registers one provider per route and attaches a custom fetch that injects auth + Factory routing headers.
Authentication and token refresh
The droid CLI stores its WorkOS OAuth tokens encrypted at ~/.factory/auth.v2.file (AES-256-GCM, envelope iv:tag:ciphertext in base64, key base64 in ~/.factory/auth.v2.key) as JSON { access_token, refresh_token, active_organization_id }.
Access tokens are short-lived JWTs. On each request the plugin:
- Decrypts the credential and reads the access-token expiry.
- If expired (or within 60s of expiry), exchanges the refresh token at
https://api.workos.com/user_management/authenticate(grant_type=refresh_token, Factory's productionclient_id, noorganization_id— WorkOS returnsorganization_not_foundif you send one), then re-encrypts the rotated credential back into the exact same envelope (atomic write, so a concurrentdroidread never sees a half-written file). - If the server still returns
401, forces one more refresh and retries the request once.
Set FACTORY_API_KEY=fk-... to bypass all of this with a long-lived key.
The factory-g (OpenAI) route quirks
The generic route is fussier than the Anthropic one. sanitizeBody / factoryGenericFetch normalize four things discovered by probing the live endpoint:
x-api-providerheader must be the model's upstream backend (fireworks), not the registry enumgeneric-chat-completion-api(which 400s withInvalid x-api-provider header).- User-Agent must be
factory-cli/0.57.0or newer; older versions 400 withtoo old to use this endpoint. - Body leakage — the
@ai-sdk/openai-compatibleSDK leaks provider options (baseURL,apiKey) into the JSON request body, which the route rejects. Unknown keys are stripped to a chat/completions allow-list. role: "system"is rejected by the Fireworks route; messages are rewritten torole: "developer".- WAF phrases — Factory's edge 403s on exact OpenCode identity strings. Softened in-proxy (punctuation only, same meaning):
You are powered by the model named→You are powered by the model, namedYou are OpenCode, the best coding agent on the planet.→You are OpenCode. The best coding agent on the planet.
- Skills beta — OpenCode often sends
skills-*without acode_executiontool; Anthropic returns 400. The proxy drops that beta unless the tool is present.
Endpoints
- Anthropic:
POST https://app.factory.ai/api/llm/a/v1/messages - Generic:
POST https://app.factory.ai/api/llm/o/v1/chat/completions - Token refresh:
POST https://api.workos.com/user_management/authenticate
Environment variables
| Variable | Purpose |
| --- | --- |
| FACTORY_API_KEY | Long-lived fk-... key; bypasses the CLI credential + refresh entirely. |
| FACTORY_HOME | Override the ~/.factory directory. |
| FACTORY_API_BASE_URL | Override https://app.factory.ai. |
| FACTORY_CLI_VERSION | Override the factory-cli/<ver> User-Agent (default 0.57.0). |
| FACTORY_WORKOS_BASE_URL | Override the WorkOS user_management base URL. |
| FACTORY_WORKOS_CLIENT_ID | Override the WorkOS client id used for refresh. |
Development
bun install
bun run build # tsc -> dist/, bundles bin/setup.js
bun test # smoke test: decrypt credential + one live requestSource layout:
src/credentials.ts— decrypt / refresh / re-encrypt the Factory credential.src/models.ts— static model catalogs for both providers.src/index.ts— the plugin: provider registration + the twofetchhooks.bin/setup.ts— thenpxinstaller that patches OpenCode's config.
The model catalogs and route quirks were reverse-engineered from the droid binary's embedded model registry and confirmed against the live API.
Notes
- Model availability,
-fastvariants, and pricing are gated by Factory feature flags on your account; the picker only shows what your org can use. - HTTP 402 ("reached your 5-hour standard usage limit") is a Factory billing limit, not a plugin bug — the request authenticated fine.
- This project is unaffiliated with Factory; it just reuses the credential the official
droidCLI already stores locally.
License
MIT — see LICENSE.
