Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Junie ← Claude Max + Codex (local subscription backends)

Wire two subscriptions you already pay for into Junie (JetBrains' CLI coding agent) as local, OpenAI-/Anthropic-compatible backends:

  • Claude Maxmeridian (primary; official Agent SDK path) or dario (backup; Claude Code wire replay) on 127.0.0.1:3456
  • ChatGPT / CodexCLIProxyAPI (OAuth) on 127.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.


Background & tool choice

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:


Topology

                 ┌─────────────────────────────────────────┐
   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.


How Junie model registration actually works

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.


Effort levels (xhigh / max)

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 (open-weights models)

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.


Files in this setup

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))"

Bring-up runbook

1. Codex — one-time OAuth (needs your browser / ChatGPT login)

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.

2. Run the Codex proxy durably

brew services start cliproxyapi     # KeepAlive + RunAtLoad; reads the config above

CLIProxyAPI watches ~/.cli-proxy-api/ (fsnotify), so step 1 and step 2 can run in either order — a login is hot-reloaded without a restart.

3. Generate correct Codex profiles

./gen-codex-profiles.sh             # writes ~/.junie/models/<id>.json per live model

Claude backend — choose meridian or dario

Both 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.

Primary: meridian

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-login

Backup: dario

Dario 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 runtime

To 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.


Verification

# 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 401

Then 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.


Troubleshooting

  • Codex /v1/models is empty → you haven't run -codex-login, or the server isn't running. Check brew services list and ~/.cli-proxy-api/.
  • 401 from CLIProxyAPI → Junie profile apiKey doesn't match the config's api-keys entry. 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-*.json profiles: change baseUrl from http://127.0.0.1:3456/v1/messages to just http://127.0.0.1:3456.
  • Codex reasoning models misbehave (streaming / tool-call glitches) → switch those profiles to apiType: "OpenAIResponses" + baseUrl: …/v1/responses (marked in gen-codex-profiles.sh).
  • Junie doesn't see the profiles → confirm ~/.junie/config.json model-locations includes ~/.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 with meridian 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 401 or reports unhealthy OAuth → run dario doctor, then dario refresh or dario login as directed. If you set DARIO_API_KEY, the Junie profiles' apiKey must equal it; the documented localhost-only setup leaves it unset and accepts the dummy key x.
  • A dario -xhigh / -max picker entry stays at Junie's effort → dario ignores x-opencode-effort. Change the profile's id to the suffixed form (for example claude-opus-4-8-max) shown above.
  • EADDRINUSE on :3456 → meridian and dario cannot listen on the same port simultaneously. Inspect with lsof -nP -iTCP:3456 -sTCP:LISTEN and stop the backend you are replacing.

Restore / teardown

# 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 logout

Remove any additional aliases shown by dario accounts list as well.


Security posture

  • The active Claude backend and CLIProxyAPI bind 127.0.0.1 only (verify with lsof -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-key404 on /v0/management/*).
  • Long-lived OAuth refresh tokens sit in ~/.cli-proxy-api/ and whichever Claude backend is configured: ~/.config/meridian/profiles/ or ~/.dario/credentials.json plus ~/.dario/accounts/. Treat those home directories as the blast radius.

Terms of service

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.


Appendix — optional launchd agents for the Claude backend

Install one of these agents, not both: each owns 127.0.0.1:3456.

meridian

~/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.

dario

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.plist

The same bash -lc wrapper resolves the globally installed dario binary.


References


License

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.

About

Use Claude Max, ChatGPT/Codex, and OpenCode Zen as local OpenAI-/Anthropic-compatible backends for JetBrains' Junie CLI (via meridian + CLIProxyAPI)

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages