Turn your local WorkBuddy desktop account into a model provider for Claude Code and Codex.
Both editions are supported and auto-detected: international (workbuddy.ai) and China (workbuddy.cn).
English | 中文
The models included with the WorkBuddy desktop app are normally usable only inside the app's own interface. This project exposes them as a local API, so Claude Code, Codex, CC Switch, and any tool that speaks OpenAI- or Anthropic-compatible protocols can use it directly as a model provider.
| Edition | Executable | Domain | Credential file |
|---|---|---|---|
International ai |
WorkBuddyAI.exe |
www.workbuddy.ai |
workbuddy-desktop-ai.info |
China cn |
WorkBuddy.exe |
www.workbuddy.cn |
workbuddy-desktop.info |
The rule is simple: use the international edition when present, otherwise the China one; when both are present, both are usable.
The two apps can be installed side by side on the same machine, but they belong to different accounts. Picking the wrong one silently spends the wrong quota, so this bridge never guesses — it probes each edition and validates it.
How the chain works:
your agent (Claude Code / Codex / ...)
│ HTTP
▼
workbuddy-bridge ──① reads ──▶ desktop app credential file (AES-256-GCM encrypted)
│ │
│ ② decrypt: spawn the app's own exe
│ (ELECTRON_RUN_AS_NODE=1)
│ to read atRestSecretKey from its private storage binding
│
└──③ HTTPS ──▶ official cloud endpoint /v2/chat/completions
(using your already signed-in account token)
Requests go to the official cloud endpoint and reuse the account your desktop app is already signed into. They consume your own account's quota; billing is not bypassed.
npm install -g workbuddy-bridgeThree commands become available:
| Command | Purpose |
|---|---|
workbuddy-doctor |
Environment check — run this first |
workbuddy-bridge |
Start the HTTP server |
workbuddy-bridge-mcp |
Start the MCP server |
npx workbuddy-bridge # start the server
npx -p workbuddy-bridge workbuddy-doctor # environment checkgit clone https://github.com/hanhan1223/workbuddy-bridge.git
cd workbuddy-bridge
node doctor.jsZero runtime dependencies.
@modelcontextprotocol/sdkis needed only for MCP mode, and is an optional dependency.
| Requirement | Notes |
|---|---|
| Windows | Windows only for now (discovery reads the uninstall registry, and both apps are Windows builds) |
| Node.js 20.3+ | Uses AbortSignal.any |
| WorkBuddy desktop app | International or China edition, and signed in |
No path configuration needed. The bridge locates the app automatically, trying:
| Order | Strategy |
|---|---|
| 1 | Environment variable (WORKBUDDY_AI_ELECTRON_BIN / WORKBUDDY_ELECTRON_BIN) |
| 2 | Conventional install locations (%LOCALAPPDATA%\Programs, etc.) |
| 3 | Windows uninstall registry (covers non-standard installs) |
| 4 | The last path that successfully decrypted a token (cache) |
workbuddy-doctorIt inspects the whole machine: Node version, environment variables, the discovery process for each edition, whether credentials decrypt, the model catalog, and finally a live chat round-trip.
Sample output (on a machine with only the China edition installed):
=== Locating the WorkBuddy apps ===
[ai] international (workbuddy.ai)
install root no match
registry no match
resolved (none) — Could not find the international ... executable.
[cn] China (workbuddy.cn)
registry D:\WorkBuddy\data\WorkBuddy.exe (registry: WorkBuddy 5.8.2)
resolved D:\WorkBuddy\data\WorkBuddy.exe
=== Checking credentials ===
[cn] China (workbuddy.cn)
status decrypted OK
domain www.workbuddy.cn
default model auto
=== Summary ===
detected editions cn
active edition cn (China (workbuddy.cn))
All checks passed. The bridge should work.
Two fields are all you need:
| Field | Meaning |
|---|---|
detected editions |
Which editions are actually usable (ai / cn / both) |
active edition |
Which account's quota is spent by default |
⚠️ workbuddy-doctoris a one-shot check — it does not start the server. It verifies that the account works and then exits. If you curl port 8799 right after, you will getCould not connect to server, because nothing is listening yet. Starting the server is the next step.
workbuddy-bridge[workbuddy-http] listening on http://127.0.0.1:8799
[workbuddy-http] provider ai: unavailable — Could not find the international ... executable.
[workbuddy-http] provider cn: OK domain=www.workbuddy.cn default=auto
[workbuddy-http] active provider: cn
[workbuddy-http]
[workbuddy-http] Ready. Leave this window open — closing it stops the bridge.
[workbuddy-http] Query it from another terminal, e.g.:
[workbuddy-http] curl http://127.0.0.1:8799/health
Two things to know:
- This window must stay open. The bridge runs in the foreground; closing the
terminal or pressing
Ctrl+Cstops it, and your agent loses connection. To run it in the background instead, see Running in the background. - Send requests from a second terminal. The first one is occupied by the
server. Commands like
curlneed their own window.
The detected result is printed at startup, so which quota is being spent is visible immediately.
The server binds to
127.0.0.1only, because the process holds your account token and must never be reachable from the network.
| Protocol | Endpoint |
|---|---|
| Anthropic Messages | http://127.0.0.1:8799/v1/messages |
| OpenAI Chat Completions | http://127.0.0.1:8799/v1/chat/completions |
| OpenAI Responses | http://127.0.0.1:8799/v1/responses |
| Model list | http://127.0.0.1:8799/v1/models |
| Health check | http://127.0.0.1:8799/health |
Write to ~/.claude/settings.json:
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8799",
"ANTHROPIC_AUTH_TOKEN": "local",
"ANTHROPIC_MODEL": "auto",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "auto",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-pro",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "deepseek-v4.1-flash"
}
}Use model ids from the catalog of the edition you actually detected — run
workbuddy-doctorto list them.ANTHROPIC_AUTH_TOKENcan be any non-empty string: the server does not validate it (the real token is decrypted server-side and never sent to you), but Claude Code rejects an empty value.
Write to ~/.codex/config.toml:
model = "auto"
model_provider = "workbuddy"
[model_providers.workbuddy]
name = "workbuddy"
base_url = "http://127.0.0.1:8799/v1"
wire_api = "responses" # ← must be responses
requires_openai_auth = false
⚠️ wire_apimust beresponses. Codex speaks the Responses protocol natively;chat/completionsalso works but adds an unnecessary conversion layer.
Any tool supporting one of the three protocols above will work. When connecting CC Switch as a custom provider, pick Anthropic Messages (native) as the upstream format — not the OpenAI format, which would cause double conversion.
1. Explicit override (WORKBUDDY_EDITION / --edition)
2. International ai (when both are usable)
3. China cn
Installed ≠ usable. An edition becomes a provider only when all three hold:
- Its executable was located
- Its credential file exists and decrypts
- It carries a usable access token
An app that is installed but was never signed in does not count. This matters: routing to a signed-out app fails at request time with an error unrelated to the request itself.
| Method | Example |
|---|---|
| URL prefix | POST http://127.0.0.1:8799/cn/v1/chat/completions |
| Request header | X-WorkBuddy-Edition: cn |
| Body field | {"edition": "cn", ...} |
Check current status:
curl http://127.0.0.1:8799/health{
"status": "ok",
"defaultEdition": "cn",
"availableEditions": ["cn"],
"domain": "www.workbuddy.cn",
"providers": [
{ "edition": "ai", "available": false, "error": "..." },
{ "edition": "cn", "available": true, "domain": "www.workbuddy.cn" }
]
}The original had no refresh: an expired token meant opening the desktop app by hand. There are now three mechanisms:
| Mechanism | Trigger |
|---|---|
| Automatic | Refreshed before a request when the token expires within 5 minutes |
| 401 retry | On a 401 → refresh once → retry with the new token |
| Manual | POST /refresh, which also re-detects the whole machine |
Refreshed tokens are cached at $DSH_HOME/.workbuddy-<edition>-auth.json and
never written back into the app's own credential file.
curl -X POST http://127.0.0.1:8799/refresh
workbuddy-doctor --refreshAbout
WORKBUDDY_CLIENT_ID: refresh uses the Keycloakrefresh_tokengrant, which needs aclient_id. The desktop app does not leave a reliable client_id on disk, soconsole(the value in the token's ownazpclaim) is used by default — and that value was measured to be rejected by the server. If you obtain the real client_id, set this environment variable.A failed refresh does not break normal use: it degrades gracefully to "keep using the current token", and only reports when the token has truly expired.
curl http://127.0.0.1:8799/v1/modelsworkbuddy-doctor # shows the model catalog section
workbuddy-doctor --json | jq '.editions[].modelSample'/v3/config returns the account's entire roster, and it mixes real chat
models with entries that cannot serve a chat request — completion-only models,
small-parameter models, and image backends. Those appear in the list but fail
when used:
| Model | Result | Reason |
|---|---|---|
minimax-m2.5 |
❌ 11102 |
model not found for this account |
glm-5.1 |
❌ 11102 |
model not found for this account |
hunyuan-image-alpha |
❌ 11103 |
Backend [hunyuan-stream] is not supported |
codewise-completions |
❌ 11102 |
completion-only entry |
So picking an id from the plain list is a reliable way to get a confusing failure. Measure instead:
workbuddy-doctor --probe-models # probe the first 20 likely-chat ids
workbuddy-doctor --probe-models=50 # probe more
workbuddy-doctor --model=deepseek-v4.1-flash # probe exactly one id=== Probing which models actually respond ===
[cn] probed 6 of 33 — 5 usable, 1 unusable
usable:
✓ default 1137ms
✓ deepseek-v4-pro 1489ms
✓ deepseek-v4-flash 901ms
✓ deepseek-v4.1-flash 980ms
✓ deepseek-v3-2-volc 1478ms
unusable (listed but not callable):
✗ minimax-m2.5 not a chat model
Through HTTP, the same measurement is available as an annotated model list:
curl 'http://127.0.0.1:8799/v1/models?probe=true&limit=5'{
"object": "list",
"data": [ { "id": "deepseek-v4.1-flash", "probe": "usable" } ],
"probe": {
"edition": "cn",
"probed": 5,
"available": ["default", "deepseek-v4-pro", "deepseek-v4-flash", "..."],
"unusable": [{ "id": "minimax-m2.5", "reason": "not a chat model" }],
"undetermined": []
}
}| Query | Behaviour |
|---|---|
/v1/models |
Plain list. Unchanged shape — existing clients are unaffected. |
/v1/models?probe=true |
Measure, then annotate every entry |
/v1/models?probe=true&limit=N |
Bound how many are measured (max 50) |
/v1/models?probe=cached |
Return the last run without new requests |
/v1/models?probe=refresh |
Ignore the cache and measure again |
Over MCP, the workbuddy_probe tool does the same, so an agent can check before
choosing a model.
Each probe is a real request that spends account quota, so:
- probing is opt-in — a plain
workbuddy-doctorrun stays free - only the first 20 likely-chat models are probed by default (raise with
=N) - results are cached for 30 minutes per edition
- probes run 3 at a time to avoid the upstream's 429 rate limit
- a rate-limited probe is reported as
undetermined, never as unusable — a transient 429 must not make a working model look broken POST /refreshandworkbuddy_refreshclear the cache
| Agent | Protocol | Status |
|---|---|---|
| Claude Code | Anthropic Messages | ✅ Tested (including tool calls) |
| Codex | OpenAI Responses | ✅ Tested (including tool calls) |
| CC Switch | Custom provider | ✅ Compatible |
| OpenCode / others | Chat Completions | ⚪ Untested |
55 regression assertions were run in a dual-edition setup, all passing:
Both single-edition (cn) and dual-edition (ai+cn) runs:
Anthropic non-stream + stream ✅
OpenAI Chat non-stream + stream ✅
OpenAI Responses non-stream + stream ✅
Tool calls + full tool loop ✅
All three edition-routing methods ✅
Model catalog (tagged per edition) ✅
/refresh + 404 ✅
The bridge runs in the foreground, so it stops when you close the terminal or log out. To keep it available, register it as a scheduled task that starts at logon:
# Find the install location first
npm root -g
# e.g. C:\Users\<you>\AppData\Roaming\npm\node_modules
$dir = "C:\Users\<you>\AppData\Roaming\npm\node_modules\workbuddy-bridge"
$action = New-ScheduledTaskAction -Execute "node" -Argument "$dir\http-server.js" -WorkingDirectory $dir
$trigger = New-ScheduledTaskTrigger -AtLogOn
Register-ScheduledTask -TaskName "WorkBuddy Bridge" -Action $action -Trigger $triggerThen manage it with:
Start-ScheduledTask -TaskName "WorkBuddy Bridge"
Stop-ScheduledTask -TaskName "WorkBuddy Bridge"
Unregister-ScheduledTask -TaskName "WorkBuddy Bridge" -Confirm:$falseCheck whether it is running:
curl http://127.0.0.1:8799/healthA Could not connect to server here means the bridge is not running — that is the
only thing that error ever means, and it is not a configuration problem.
workbuddy-doctorreports whether a server is already listening, so it is a quick way to check without a curl.
workbuddy-bridge [options]
-p, --port <n> Port (default 8799)
-m, --model <id> Default model
-e, --edition <ai|cn> Force one edition
-h, --help Help
-v, --version Version
| Variable | Default | Purpose |
|---|---|---|
WORKBUDDY_EDITION |
auto-detect | Force ai or cn |
WORKBUDDY_AI_ELECTRON_BIN |
auto-detect | International exe path |
WORKBUDDY_ELECTRON_BIN |
auto-detect | China exe path |
WORKBUDDY_AI_AUTH_FILE |
%LOCALAPPDATA%\CodeBuddyExtension\…\workbuddy-desktop-ai.info |
International credential file |
WORKBUDDY_AUTH_FILE |
%LOCALAPPDATA%\CodeBuddyExtension\…\workbuddy-desktop.info |
China credential file |
WORKBUDDY_CLIENT_ID |
console |
OAuth client id used for refresh |
WORKBUDDY_MCP_PORT |
8799 |
HTTP port |
WORKBUDDY_MODEL |
per edition | Default model |
WORKBUDDY_TIMEOUT_MS |
300000 |
Upstream request timeout |
WORKBUDDY_SLOW_MS |
60000 |
Log requests slower than this |
WORKBUDDY_DEBUG |
— | Set to 1 to log upstream request details |
Do not mix the two
*_ELECTRON_BINvariables. Pointing one at the other edition's binary fails explicitly, because it would spend the other account's quota.
Besides the HTTP server, an MCP server lets an agent call WorkBuddy on its own:
claude mcp add workbuddy -- npx -y workbuddy-bridge-mcp| Tool | Purpose |
|---|---|
workbuddy_ask |
Send a prompt (accepts an edition argument) |
workbuddy_models |
List models available to an edition |
workbuddy_probe |
Test which models actually respond |
workbuddy_status |
Detected editions and their identities |
workbuddy_refresh |
Re-detect the machine and refresh tokens |
When to use MCP instead of HTTP?
| You want | Use |
|---|---|
| WorkBuddy as a model provider (switchable, aggregatable) | HTTP |
| The agent to ask WorkBuddy a question mid-conversation | MCP |
Both can coexist without conflict.
workbuddy-doctorIt lists every step for both editions; most problems are identified there.
WorkBuddy error codes are not unique — the same code can mean entirely
different things. Read msg, not just code.
| Error | Real meaning | What to do |
|---|---|---|
11128 / unapproved channel |
system prompt contains a competitor client identifier | See the section below |
11128 / first message is not system prompt |
First message is not system | Handled automatically |
11148 / tool calls and tool results do not match |
Tool calls and results cannot be paired | Fixed |
11102 / model … service info not found |
Model name does not exist | Check the model id (strip suffixes like [1M]) |
14003 / too many requests |
Rate limited | Wait 1–2 minutes, or pick a less busy model |
The most commonly misdiagnosed one. The message sounds like a security policy block, so it reads as a network or risk-control problem. In reality it just means the system prompt contains a client name WorkBuddy does not recognise:
| Content in system | Result |
|---|---|
You are a helpful assistant. |
✅ |
You are a Claude agent... Claude Agent SDK. |
✅ |
OpenAI (on its own) |
✅ |
Claude Code |
❌ |
Codex CLI |
❌ |
x-anthropic-billing-header |
❌ |
The bridge neutralises these automatically — it only replaces the client's own
self-description and never touches user content. If a new agent hits this error, its
prompt contains a new trigger word: start with WORKBUDDY_DEBUG=1 and extend
stripClientPreamble() in upstream.js accordingly.
workbuddy-doctor lists the result of each of the four discovery strategies:
registryshowsno match→ the app may not be installed, or is in an unregistered location- Several were found → point the matching
*_ELECTRON_BINat the one to use
status unavailable — Failed to decrypt accessToken.
- Confirm the exe is located (
workbuddy-doctorshows this) - Open the desktop app once (the key may rotate after an app upgrade)
- Confirm you are in a new terminal session (an old one may hold stale environment variables)
- A refresh is attempted automatically (see Token refresh)
- If refresh also fails → open the desktop app and sign in again
- Call again
The desktop app stores its tokens encrypted on disk (AES-256-GCM):
"accessToken": { "$wbEncrypted": 1, "envelope": "<base64>" }The key derives from a machine-local secret that is only obtainable through the private binding of WorkBuddy's modified Electron:
process._linkedBinding("electron_browser_workbuddy_storage").loggerGet()A stock Node process cannot call this private binding. So the bridge starts the
app's own binary with ELECTRON_RUN_AS_NODE=1, letting it run a one-line script
in "pure Node" mode that prints the secret.
This is why each edition must bind its own app binary — and specifically the correct one, since the two apps' keys are mutually exclusive.
workbuddy-bridge/
├── http-server.js HTTP server: routing and response translation for three protocols
├── providers.js Provider registry: which edition is usable, which is default
├── probe.js Model availability probing and caching
├── responses.js Bidirectional OpenAI Responses protocol translation
├── upstream.js Upstream calls: headers, SSE parsing, tool-call reassembly
├── credentials.js Token read/decrypt (AES-256-GCM) + automatic refresh
├── locate.js Dual-edition executable discovery
├── server.js MCP server
└── doctor.js Environment diagnostics
| File | Responsibility |
|---|---|
locate.js |
Classify and discover exes per edition (env / install root / registry / cache) |
credentials.js |
Locate credentials → spawn the matching exe for the key → decrypt → refresh |
providers.js |
Decide "installed" vs "usable", and pick the default edition |
probe.js |
Measure which catalog entries answer a chat request, with caching |
upstream.js |
Per-edition origin, identity headers, and default model |
http-server.js |
Routing, edition selection, three-protocol translation |
-
Windows only. Both apps are Windows builds and discovery reads the uninstall registry.
-
Decryption depends on the desktop app's binary. An app upgrade that changes key derivation will break decryption; reopening the app once usually restores it.
-
Token refresh needs a client_id. The default value was measured to be rejected (see Token refresh). Failure degrades gracefully and does not affect normal use.
-
No rate-limit backoff. Rapid consecutive requests trigger 429; it is returned as-is so clients (e.g. CC Switch failover) can react.
-
The API is reverse-engineered and may break when WorkBuddy changes.
-
The server must stay running. Stop it and the agent cannot connect.
- This project is unofficial, with no affiliation with or endorsement by WorkBuddy, Anthropic, or OpenAI.
- It accesses official endpoints using your own account's token and does not bypass billing.
- Using it may conflict with the upstream provider's terms of service; assess that risk yourself.
See DISCLAIMER.md for the full statement.
MIT © 2026 hanhan1223
- Restoration of the upstream endpoints and request formats referenced the public
implementation and measured findings of
dsh-workbuddy-connect.