Wire two subscriptions you already pay for into Junie (JetBrains' CLI coding agent) as local, OpenAI-/Anthropic-compatible backends:
- Claude Max →
meridian(primary; official Agent SDK path) ordario(backup; Claude Code wire replay) on127.0.0.1:3456 - ChatGPT / Codex →
CLIProxyAPI(OAuth) on127.0.0.1:8317 - OpenCode Zen (pay-as-you-go; open-weights models like GLM 5.2) → upstream of the same CLIProxyAPI endpoint
Everything is bound to localhost. Personal, single-developer use. See the ToS note at the bottom — this leans on consumer subscriptions in a way both vendors disfavor; the account risk is yours.
Last updated: 2026-07-13.
These backends were chosen after comparing the main subscription-proxy options (meridian, CLIProxyAPI, alex, dario) on architecture, provider coverage, auth model, and account risk — landing on meridian for Claude (official Agent SDK path, lowest ban risk) and CLIProxyAPI for Codex (broad OAuth-backed gateway). dario is also documented as a cold-standby Claude backend: it is less conservative because it uses Claude Code OAuth credentials and replays Claude Code's request shape, but it is independently maintained and can take over the same local endpoint if meridian stalls or disappears. The two independent comparisons that drove the decision:
┌─────────────────────────────────────────┐
junie ─────► │ ~/.junie/models/*.json (model profiles)│
(model picker) └───────────────┬──────────────┬───────────┘
│ │
apiType: Anthropic │ │ apiType: OpenAICompletion
http://127.0.0.1:3456/v1/messages http://127.0.0.1:8317/v1/chat/completions
│ │
┌──────▼─────┐ ┌─────▼───────────┐
│ meridian │ │ CLIProxyAPI │
│ or dario │ │ :8317 (+key) │
│ :3456 │ └─────┬───────────┘
└──────┬─────┘ │
Agent SDK / CC replay Codex OAuth (ChatGPT)
(Claude Max) callback :1455
| Component | Bind | Verified version | Config / creds | Auth to client |
|---|---|---|---|---|
| meridian | 127.0.0.1:3456 |
1.45.3 (≥ 1.45 for Fable) | ~/.config/meridian/ (profile max) |
none (key x) |
| dario (alternative) | 127.0.0.1:3456 |
5.0.1 | ~/.dario/ (pool alias login) |
none on loopback (key x) |
| CLIProxyAPI | 127.0.0.1:8317 |
7.2.70 | /usr/local/etc/cliproxyapi.conf, ~/.cli-proxy-api/ |
Bearer key |
| OpenCode Zen | upstream of :8317 |
hosted | API key in the conf's openai-compatibility block |
same local Bearer key |
meridian model pins: fable=claude-fable-5 opus=claude-opus-4-8 sonnet=claude-sonnet-4-6 haiku=claude-haiku-4-5
(also exposes claude-opus-4-6, claude-opus-4-7).
dario reads Anthropic's live model catalog (with a baked offline fallback) and
also accepts the fable, opus, sonnet, and haiku shortcuts. Only one
Claude backend can own :3456 at a time.
The important finding (verified against JetBrains docs + the installed binary):
- Junie does NOT honor
ANTHROPIC_BASE_URL/OPENAI_BASE_URL. Those vars don't exist in Junie. The BYOK "use your own API key" menu talks to the real vendor endpoints and cannot be pointed at localhost. - The only override surface is custom model-profile JSON files in
~/.junie/models/*.json(user scope) or<project>/.junie/models/*.json. The "custom models and endpoints (LiteLLM/Ollama/LM Studio)" wizard just writes these files. Multiple files are supported → that's how both providers show up in one picker.
Profile schema:
| Field | Notes |
|---|---|
id |
model id sent upstream; filename (minus .json) is the profile name |
baseUrl |
full endpoint URL (not just host) |
apiType |
OpenAICompletion (/v1/chat/completions) · OpenAIResponses (/v1/responses) · Anthropic (/v1/messages) · Google |
apiKey |
optional; sent as bearer / x-api-key. Supports ${ENV} expansion |
extraHeaders |
optional object |
fasterModel |
optional { "id": ... } used for lightweight sub-tasks |
Discovery location is also declared explicitly in ~/.junie/config.json via
model-locations.
Junie's model-picker Effort column is Junie's own knob and tops out at
High. The higher tiers both subscriptions support are reached with dedicated
picker entries — -xhigh / -max model variants — where the effort is forced
at the proxy layer and always wins over whatever Junie sends. (ultra in the
Codex app and ultracode in Claude Code are harness orchestration modes, not
API effort values — they cannot be wired through a plain API endpoint.)
Codex (CLIProxyAPI). oauth-model-alias forks expose extra client-visible
model ids routed to the same upstream, and payload.override rules pin
reasoning.effort per fork name. Verified: the rules match the
client-visible name (a probe fork with an invalid value got the upstream
invalid_value rejection back), and upstream support differs per model —
gpt-5.4-mini enumerates none…xhigh (no max), while all three gpt-5.6-*
accept max. The forks appear in /v1/models, so gen-codex-profiles.sh
turns them into Junie profiles automatically. Config block appended to
/usr/local/etc/cliproxyapi.conf:
oauth-model-alias:
codex:
- name: "gpt-5.5"
alias: "gpt-5.5-xhigh"
fork: true
- name: "gpt-5.6-sol"
alias: "gpt-5.6-sol-xhigh"
fork: true
- name: "gpt-5.6-sol"
alias: "gpt-5.6-sol-max"
fork: true
# ... same -xhigh/-max pair for gpt-5.6-luna and gpt-5.6-terra
payload:
override:
- models:
- name: "*-xhigh"
protocol: "codex"
params:
"reasoning.effort": "xhigh"
- models:
- name: "*-max"
protocol: "codex"
params:
"reasoning.effort": "max"Claude (meridian ≥ 1.45). meridian reads per-request effort from the
x-opencode-effort header (low|medium|high|xhigh|max); the header takes
priority over body.effort / body.reasoning_effort. Junie profiles bake it
in via extraHeaders — the profile filename is the picker name while id
stays the real model id:
{
"id": "claude-fable-5",
"baseUrl": "http://127.0.0.1:3456/v1/messages",
"apiType": "Anthropic",
"apiKey": "x",
"extraHeaders": { "x-opencode-effort": "max" },
"fasterModel": { "id": "claude-haiku-4-5" }
}Plain (unsuffixed) picker entries keep whatever Junie itself sends — at most
high.
Claude (dario 5.0.1 alternative). The ordinary Claude profiles above are
drop-in compatible because dario serves the same /v1/messages endpoint and
does not enforce a client key on its default loopback bind. It does not read
meridian's x-opencode-effort header, however. For a forced picker entry,
put the effort suffix in the id; dario strips the suffix before sending the
request upstream and maps it to output_config.effort:
{
"id": "claude-opus-4-8-max",
"baseUrl": "http://127.0.0.1:3456/v1/messages",
"apiType": "Anthropic",
"apiKey": "x",
"fasterModel": { "id": "claude-haiku-4-5" }
}Use claude-opus-4-8-xhigh for the corresponding xhigh entry. These
suffixed ids are dario-specific; when switching back to meridian, restore the
real id plus extraHeaders. Dario also has a server-wide --effort=...
override, but that pins every request and therefore loses the per-picker-entry
behavior.
OpenCode Zen is OpenCode's pay-as-you-go model
gateway. Unlike the Claude/Codex paths it is not a ToS gray area: it's a
plain metered API whose docs say you can "use it with any other coding agent."
It's wired as an openai-compatibility upstream in
/usr/local/etc/cliproxyapi.conf, so its models ride the existing pipeline:
they appear on 127.0.0.1:8317/v1/models and gen-codex-profiles.sh writes
their Junie profiles automatically.
Curated set (exact upstream ids): glm-5.2, glm-5.1, glm-5,
kimi-k2.7-code, deepseek-v4-pro, deepseek-v4-flash-free, minimax-m3.
The full catalog (55 models, including several *-free ones) is public:
curl https://opencode.ai/zen/v1/models. To add one, append a
- name/alias pair to the conf's opencode-zen models list — hot reload picks
it up — and re-run ./gen-codex-profiles.sh.
One-time setup: sign in to the Zen dashboard, add billing, copy your API key,
and paste it over PASTE-YOUR-ZEN-API-KEY-HERE in the conf's
opencode-zen block. Models are listed even with the placeholder; live
requests need the real key (until then they 401).
GLM-5.2 pricing at time of writing: $1.40/M input, $4.40/M output.
| Path | Role |
|---|---|
/usr/local/etc/cliproxyapi.conf |
CLIProxyAPI config. Edited: host: 127.0.0.1, single localhost api-keys entry. |
/usr/local/etc/cliproxyapi.conf.junie-bak |
Pre-edit backup (restore target). |
~/.junie/models/claude-opus-4-8.json |
Claude → active backend on :3456 (apiType: Anthropic), fasterModel: claude-haiku-4-5. |
~/.junie/models/claude-sonnet-4-6.json |
Claude → active backend on :3456. |
~/.junie/models/claude-haiku-4-5.json |
Claude → active backend on :3456 (fast/cheap). |
~/.junie/models/gpt-5-codex.json |
Codex → CLIProxyAPI (apiType: OpenAICompletion). Placeholder id until login — regenerate with the script below. |
~/.junie/config.json |
model-locations → ~/.junie/models. |
~/.junie/models/claude-fable-5.json |
Fable → active backend on :3456 (needs meridian ≥ 1.45; dario autodetects it). |
~/.junie/models/claude-{fable-5,opus-4-8}-{xhigh,max}.json |
Effort-forced Claude variants: header pin for meridian, suffixed id for dario. |
~/.junie/models/gpt-*-{xhigh,max}.json |
Effort-forced Codex forks (written by gen-codex-profiles.sh). |
~/.junie/models/{glm-*,kimi-*,deepseek-*,minimax-*}.json |
OpenCode Zen open-weights models (written by gen-codex-profiles.sh). |
./gen-codex-profiles.sh |
Regenerates Codex profiles from CLIProxyAPI's live /v1/models. |
The CLIProxyAPI client key lives in the config and in gpt-5-codex.json. Retrieve it:
python3 -c "import re;print(re.search(r'api-keys:\s*\n\s*-\s*\"([^\"]+)\"',open('/usr/local/etc/cliproxyapi.conf').read()).group(1))"CLIProxyAPI -codex-login # opens browser; OAuth callback on :1455
# headless box? add -no-browser (or use: CLIProxyAPI -codex-device-login)Writes credentials to ~/.cli-proxy-api/. This token is independent of the
native Codex CLI's ~/.codex/auth.json — same ChatGPT account & shared plan
quota, separate refresh tokens.
brew services start cliproxyapi # KeepAlive + RunAtLoad; reads the config aboveCLIProxyAPI watches ~/.cli-proxy-api/ (fsnotify), so step 1 and step 2 can run
in either order — a login is hot-reloaded without a restart.
./gen-codex-profiles.sh # writes ~/.junie/models/<id>.json per live modelBoth listen on 127.0.0.1:3456, deliberately, so the normal Junie profiles do
not need a URL change during failover. Do not start both at once.
meridian has no service wrapper. Simplest:
npm install -g @rynfar/meridian@latest # ≥ 1.45 required for claude-fable-5
MERIDIAN_HOST=127.0.0.1 MERIDIAN_PORT=3456 meridian &For persistence across reboots, see the optional launchd agent in the appendix.
Re-auth when the Claude OAuth token expires (tokens last about a week; each
profile dir under ~/.config/meridian/profiles/<id> is its own
CLAUDE_CONFIG_DIR):
meridian refresh-token # routine refresh
CLAUDE_CONFIG_DIR="$HOME/.config/meridian/profiles/max" claude login # full re-loginDario takes a different, higher-risk route: it uses Claude Code OAuth and rebuilds requests to match Claude Code's wire shape instead of going through the official Agent SDK. Install and authenticate it while meridian is still healthy so the backup is ready; only the proxy process conflicts on the port.
npm install -g @askalf/dario@latest
dario login # add --manual for SSH/headless setups
dario doctor # verify OAuth, Claude Code drift, and runtimeTo fail over, stop meridian and start dario on the same endpoint:
kill $(lsof -ti :3456) # stop whichever Claude backend owns the port
dario proxy --host=127.0.0.1 --port=3456 &The ordinary Claude profiles now work unchanged. Convert the forced
-xhigh/-max profiles to dario's suffixed-id form shown in the effort section
if you need those entries too. Dario refreshes its stored OAuth credentials
automatically; use dario status, dario refresh, or dario doctor when auth
looks unhealthy. Its no-argument dario command opens the local status TUI.
# Active Claude backend (meridian or dario)
curl -s http://127.0.0.1:3456/v1/models | python3 -m json.tool
# Codex side (after login + server up)
KEY=$(python3 -c "import re;print(re.search(r'api-keys:\s*\n\s*-\s*\"([^\"]+)\"',open('/usr/local/etc/cliproxyapi.conf').read()).group(1))")
curl -s -H "Authorization: Bearer $KEY" http://127.0.0.1:8317/v1/models | python3 -m json.tool
curl -s -o /dev/null -w "no-auth => HTTP %{http_code}\n" http://127.0.0.1:8317/v1/models # expect 401Then launch junie and pick a model from the picker — the claude-* and
Codex profiles appear as custom models. No default is forced in config.json.
- Codex
/v1/modelsis empty → you haven't run-codex-login, or the server isn't running. Checkbrew services listand~/.cli-proxy-api/. 401from CLIProxyAPI → Junie profileapiKeydoesn't match the config'sapi-keysentry. Re-run./gen-codex-profiles.sh(it reads the key for you).- Claude requests 404 in Junie → Junie's Anthropic adapter may be
double-appending the path. Edit the
claude-*.jsonprofiles: changebaseUrlfromhttp://127.0.0.1:3456/v1/messagesto justhttp://127.0.0.1:3456. - Codex reasoning models misbehave (streaming / tool-call glitches) → switch
those profiles to
apiType: "OpenAIResponses"+baseUrl: …/v1/responses(marked ingen-codex-profiles.sh). - Junie doesn't see the profiles → confirm
~/.junie/config.jsonmodel-locationsincludes~/.junie/models, and restart Junie (it reads profiles at startup). Junie auto-updates on launch; that's expected. - meridian health
degraded/authentication_error: Claude OAuth token has expired→ the profile's token is dead.meridian refresh-token; if that fails, do the full re-login shown in the runbook. Check state withmeridian profile list. - Fable missing from meridian's catalog → meridian < 1.45; upgrade and
restart. Note a running instance keeps the OLD code in memory after an npm
upgrade — kill it by port,
kill $(lsof -ti :3456)(pkill -f '@rynfar/meridian'misses it: the process cmdline is the mise bin symlink). - dario returns
401or reports unhealthy OAuth → rundario doctor, thendario refreshordario loginas directed. If you setDARIO_API_KEY, the Junie profiles'apiKeymust equal it; the documented localhost-only setup leaves it unset and accepts the dummy keyx. - A dario
-xhigh/-maxpicker entry stays at Junie's effort → dario ignoresx-opencode-effort. Change the profile'sidto the suffixed form (for exampleclaude-opus-4-8-max) shown above. EADDRINUSEon:3456→ meridian and dario cannot listen on the same port simultaneously. Inspect withlsof -nP -iTCP:3456 -sTCP:LISTENand stop the backend you are replacing.
# Revert CLIProxyAPI config
cp /usr/local/etc/cliproxyapi.conf.junie-bak /usr/local/etc/cliproxyapi.conf
brew services stop cliproxyapi
# Un-register from Junie
rm -f ~/.junie/models/claude-*.json ~/.junie/models/gpt-*.json
rm -f ~/.junie/config.json # or just remove the model-locations key
# Revoke the local Codex token stored by CLIProxyAPI (does NOT touch ~/.codex)
rm -f ~/.cli-proxy-api/*codex*
# Stop the active Claude backend (pkill can miss meridian's mise symlink)
kill $(lsof -ti :3456)
# Optional: revoke dario's single-account local Claude OAuth credentials
dario accounts remove login
dario logoutRemove any additional aliases shown by dario accounts list as well.
- The active Claude backend and CLIProxyAPI bind
127.0.0.1only (verify withlsof -nP -iTCP:8317 -iTCP:3456 -sTCP:LISTEN). - Dario deliberately has no client-key check on a loopback bind. If you expose
it beyond localhost, it requires
DARIO_API_KEY; this guide does not expose it. - CLIProxyAPI requires a bearer key on every request; its Management API is
disabled (empty
secret-key→404on/v0/management/*). - Long-lived OAuth refresh tokens sit in
~/.cli-proxy-api/and whichever Claude backend is configured:~/.config/meridian/profiles/or~/.dario/credentials.jsonplus~/.dario/accounts/. Treat those home directories as the blast radius.
Routing a consumer subscription into a third-party client is disfavored by both Anthropic's and OpenAI's consumer terms, and subscription-proxy bans are documented. Dario's OAuth + Claude Code wire-replay path is materially less conservative than meridian's official Agent SDK path. This stays entirely on localhost, but the account risk is yours. OpenCode Zen is the exception: a metered API explicitly marketed for use "with any other coding agent" — no gray area.
Install one of these agents, not both: each owns 127.0.0.1:3456.
~/Library/LaunchAgents/com.local.meridian.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.local.meridian</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string><string>-lc</string>
<string>exec meridian</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>MERIDIAN_HOST</key><string>127.0.0.1</string>
<key>MERIDIAN_PORT</key><string>3456</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
</dict></plist>launchctl load ~/Library/LaunchAgents/com.local.meridian.plist # bootstrap on newer macOS: launchctl bootstrap gui/$(id -u) <plist>The bash -lc wrapper picks up meridian from your mise-activated PATH.
Run dario login first, then save
~/Library/LaunchAgents/com.local.dario.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.local.dario</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string><string>-lc</string>
<string>exec dario proxy --host=127.0.0.1 --port=3456</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
</dict></plist>launchctl load ~/Library/LaunchAgents/com.local.dario.plistThe same bash -lc wrapper resolves the globally installed dario binary.
- Junie: Custom LLMs · config.json · Environment variables · CLI reference
- meridian · dario · CLIProxyAPI · OpenCode Zen
- Other alternative compared: alex
MIT — see LICENSE. Provided for personal reference, without
warranty. This documents tooling that routes consumer AI subscriptions into a
third-party client, which is disfavored by Anthropic's and OpenAI's consumer
terms; you accept your own account risk. Not affiliated with or endorsed by
JetBrains, Anthropic, OpenAI, or OpenCode.