Per-agent configuration for every tested client. All agents share the same basics — point the tool at http://127.0.0.1:3456 with any API key value — but several have their own config formats or adapters, documented here.
Step 1: Run meridian setup (required, one time)
meridian setupThis adds the Meridian plugin to your OpenCode global config (~/.config/opencode/opencode.json). The plugin enables:
- Session tracking — reliable conversation continuity across requests
- Safe model defaults — Opus uses 1M context (included with Max subscription); Sonnet uses 200k to avoid Extra Usage charges (details)
- Subagent model selection — subagents automatically use
sonnet/opus(200k), preserving rate-limit budget
If the plugin is missing, Meridian warns at startup and reports "plugin": "not-configured" in the health endpoint.
Step 2: Start
ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 opencodeOr set these in your shell profile so they're always active:
export ANTHROPIC_API_KEY=x
export ANTHROPIC_BASE_URL=http://127.0.0.1:3456oh-my-opencagent adds multi-agent orchestration on top of OpenCode. It works transparently through Meridian with no extra configuration — OMO uses the same OpenCode headers and tool format, so Meridian detects it automatically.
Meridian parses OMO's Task tool descriptions to extract subagent names (explore, code-review, etc.) and builds SDK AgentDefinitions so Claude can route to the correct agent. Internal orchestration markers (<!-- OMO_INTERNAL_INITIATOR -->, [SYSTEM DIRECTIVE: OH-MY-OPENCODE ...]) are stripped automatically to prevent context leakage.
OMO requires passthrough mode (the default for OpenCode) — subagent delegation flows through tool calls that must be forwarded back to the client.
Add a provider to ~/.config/crush/crush.json:
{
"providers": {
"meridian": {
"id": "meridian",
"name": "Meridian",
"type": "anthropic",
"base_url": "http://127.0.0.1:3456",
"api_key": "dummy",
"models": [
{ "id": "claude-fable-5", "name": "Claude Fable 5 (1M)", "context_window": 1000000, "default_max_tokens": 32768, "can_reason": true, "supports_attachments": true },
{ "id": "claude-opus-5", "name": "Claude Opus 5 (1M)", "context_window": 1000000, "default_max_tokens": 32768, "can_reason": true, "supports_attachments": true },
{ "id": "claude-opus-4-8", "name": "Claude Opus 4.8 (1M)", "context_window": 1000000, "default_max_tokens": 32768, "can_reason": true, "supports_attachments": true },
{ "id": "claude-opus-4-7", "name": "Claude Opus 4.7 (1M)", "context_window": 1000000, "default_max_tokens": 32768, "can_reason": true, "supports_attachments": true },
{ "id": "claude-sonnet-4-6", "name": "Claude Sonnet 4.6 (1M)", "context_window": 1000000, "default_max_tokens": 64000, "can_reason": true, "supports_attachments": true },
{ "id": "claude-opus-4-6", "name": "Claude Opus 4.6 (1M)", "context_window": 1000000, "default_max_tokens": 32768, "can_reason": true, "supports_attachments": true },
{ "id": "claude-haiku-4-5-20251001", "name": "Claude Haiku 4.5", "context_window": 200000, "default_max_tokens": 16384, "can_reason": true, "supports_attachments": true }
]
}
}
}crush run --model meridian/claude-sonnet-4-6 "refactor this function"
crush --model meridian/claude-opus-4-6 # interactive TUICrush is automatically detected from its Charm-Crush/ User-Agent — no plugin needed.
Add Meridian as a custom model provider in ~/.factory/settings.json:
{
"customModels": [
{ "model": "claude-fable-5", "name": "Fable 5 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
{ "model": "claude-opus-5", "name": "Opus 5 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
{ "model": "claude-opus-4-8", "name": "Opus 4.8 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
{ "model": "claude-opus-4-7", "name": "Opus 4.7 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
{ "model": "claude-sonnet-4-6", "name": "Sonnet 4.6 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
{ "model": "claude-opus-4-6", "name": "Opus 4.6 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" },
{ "model": "claude-haiku-4-5-20251001", "name": "Haiku 4.5 (Meridian)", "provider": "anthropic", "baseUrl": "http://127.0.0.1:3456", "apiKey": "x" }
]
}Then pick any custom:claude-* model in the Droid TUI. No plugin needed — Droid is automatically detected.
1. Authenticate:
cline auth --provider anthropic --apikey "dummy" --modelid "claude-sonnet-4-6"2. Set the proxy URL in ~/.cline/data/globalState.json:
{
"anthropicBaseUrl": "http://127.0.0.1:3456",
"actModeApiProvider": "anthropic",
"actModeApiModelId": "claude-sonnet-4-6"
}3. Run:
cline --yolo "refactor the login function"No plugin needed — Cline uses the standard Anthropic SDK.
ANTHROPIC_API_KEY=x ANTHROPIC_BASE_URL=http://127.0.0.1:3456 \
aider --model anthropic/claude-sonnet-4-6Note:
--no-streamis incompatible due to a litellm parsing issue — use the default streaming mode.
Codex CLI ≥ 0.96 dropped wire_api = "chat" and speaks only the OpenAI Responses API (/v1/responses), which Meridian serves. Add a provider to ~/.codex/config.toml:
model = "claude-sonnet-5"
model_provider = "meridian"
[model_providers.meridian]
name = "Meridian"
base_url = "http://127.0.0.1:3456/v1"
wire_api = "responses"
env_key = "MERIDIAN_KEY" # any value unless MERIDIAN_API_KEY is setMERIDIAN_KEY=x codex "refactor this function"
MERIDIAN_KEY=x codex exec "run the tests and summarize failures" # non-interactiveCodex is a tool-driving agent — Meridian runs the /v1/responses endpoint in passthrough mode automatically (Codex executes its own shell/apply-patch tools), so no MERIDIAN_PASSTHROUGH change is needed. A harmless Model metadata for 'claude-sonnet-5' not found warning from Codex is expected — it doesn't recognize non-OpenAI model ids but works regardless.
model_reasoning_effort is supported and won't stall the CLI, but Claude's private thinking isn't yet carried across turns — the Responses API's encrypted-reasoning envelope is OpenAI-specific and incompatible with Claude's signed thinking blocks, so cross-turn reasoning continuity is deferred (each turn still reasons with full context including tool results). Verified on Codex 0.144 with plain, tool-driving, and reasoning-enabled turns.
Meridian speaks the OpenAI protocol natively — no LiteLLM or translation proxy needed.
POST /v1/chat/completions — accepts OpenAI chat format, returns OpenAI completion format (streaming and non-streaming)
image_urlparts are supported when provided as data URLs (data:image/...;base64,...)- multimodal tool flows where a tool returns
tool_result.content = [text, image]are preserved through the structured multimodal path instead of being flattened to text
GET /v1/models — returns available Claude models in OpenAI format
Point any OpenAI-compatible tool at http://127.0.0.1:3456 with any API key value:
# Open WebUI: set OpenAI API base to http://127.0.0.1:3456, API key to any value
# Continue: set apiBase to http://127.0.0.1:3456 with provider: openai
# Any OpenAI SDK: set base_url="http://127.0.0.1:3456", api_key="dummy"Note: Multi-turn conversations work by packing prior turns into the system prompt. Each request is a fresh SDK session — OpenAI clients replay full history themselves and don't use Meridian's session resumption.
Cherry Studio is a desktop chat client. Point it at Meridian by setting the Anthropic API base URL to http://127.0.0.1:3456 (any API key value works).
Because Cherry Studio is a chat client rather than a coding agent, select the cherry adapter so Claude's built-in web search is available (coding-agent adapters block it in favour of their own):
MERIDIAN_DEFAULT_AGENT=cherry meridianThe cherry adapter runs in internal mode: Claude executes WebSearch/WebFetch itself and Meridian returns the grounded answer — the internal tool calls are hidden from the client. This resolves the "no WebSearch/WebFetch tool exposed" error (#481).
Cherry Studio doesn't send a Meridian-specific header, so set
MERIDIAN_DEFAULT_AGENT=cherryon a Meridian dedicated to it, or sendx-meridian-agent: cherryif your setup allows custom headers.
Add a custom provider to ~/forge/.forge.toml:
[[providers]]
id = "meridian"
url = "http://127.0.0.1:3456/v1/messages"
models = "http://127.0.0.1:3456/v1/models"
api_key_vars = "MERIDIAN_FORGE_KEY"
response_type = "Anthropic"
auth_methods = ["api_key"]
[session]
provider_id = "meridian"
model_id = "claude-opus-4-6"Set the API key env var. Any value works unless you've enabled authentication with MERIDIAN_API_KEY, in which case use your auth key here:
export MERIDIAN_FORGE_KEY=xThen log in and select the model:
forge provider login meridian # enter any value when prompted
forge config set provider meridian --model claude-opus-4-6Start Meridian with the ForgeCode adapter:
MERIDIAN_DEFAULT_AGENT=forgecode meridianForgeCode uses reqwest's default User-Agent, so automatic detection isn't possible. The MERIDIAN_DEFAULT_AGENT env var tells Meridian to use the ForgeCode adapter for all unrecognized requests. If you run other agents alongside ForgeCode, use the x-meridian-agent: forgecode header instead (add [providers.headers] to your .forge.toml).
Pi uses the @mariozechner/pi-ai library which supports a configurable baseUrl on the model. Add a provider-level override in ~/.pi/agent/models.json:
{
"providers": {
"anthropic": {
"baseUrl": "http://127.0.0.1:3456",
"apiKey": "x",
"headers": {
"x-meridian-agent": "pi"
}
}
}
}Pi mimics Claude Code's User-Agent, so automatic detection isn't possible. The x-meridian-agent: pi header in the config above tells Meridian to use the Pi adapter. Alternatively, if Pi is your only agent, you can set MERIDIAN_DEFAULT_AGENT=pi as an env var instead.
Pi runs in passthrough mode by default — it executes its own tools and Meridian just forwards the tool_use blocks. Opt out with MERIDIAN_PASSTHROUGH=0.
Prime Agent is a fork of Pi with a
different prompt and tool surface, so it uses its own prime adapter rather
than Pi's. Its only tool by default is ipython, a persistent kernel in the
client — passthrough is effectively required, and internal mode
(MERIDIAN_PASSTHROUGH=0) drops that tool entirely.
Connect it with an extension. Save as .prime/agent/extensions/meridian.ts in
a project, or ~/.prime/agent/extensions/meridian.ts globally:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
const MERIDIAN_PROVIDER = "meridian"
export default function (pi: ExtensionAPI) {
pi.registerProvider(MERIDIAN_PROVIDER, {
name: "Meridian (Claude Max)",
baseUrl: "http://127.0.0.1:3456",
apiKey: "MERIDIAN_API_KEY", // env var name, or any literal if auth is off
api: "anthropic-messages",
headers: { "x-meridian-agent": "prime" },
models: [
{
id: "claude-opus-5",
name: "Claude Opus 5 (Meridian)",
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1000000,
maxTokens: 64000,
},
],
})
// Required, not an optimisation — see below.
pi.on("before_provider_request", (event: any, ctx: any) => {
if (ctx?.model?.provider !== MERIDIAN_PROVIDER) return undefined
const sessionId = ctx?.sessionManager?.getSessionId?.()
if (typeof sessionId !== "string" || !sessionId) return undefined
return {
...(event.payload as Record<string, unknown>),
metadata: { user_id: JSON.stringify({ session_id: sessionId }) },
}
})
}Then pick a model with /model or --provider meridian --model claude-opus-5.
contextWindow must say 1000000. Provider models registered this way are
static — Prime Agent never calls GET /v1/models for them — so this literal is
the only thing driving the TUI footer and auto-compaction
(contextTokens > contextWindow - reserveTokens). Meridian routes every primary
opus request to the SDK's opus[1m] alias, so a 200000 here compacts the
conversation at a fifth of the window you are actually paying for. Restart Prime
Agent after editing: providers are registered once per process.
Set it back to 200000 if you opt out of extended context on the proxy side —
MERIDIAN_1M_CONTEXT_SUPPORT=0, MERIDIAN_OPUS_MODEL=opus, or a plan that
doesn't include Opus 1M (Meridian then falls back to plain opus after one
Extra-Usage rejection). Leaving 1000000 in that case surfaces as upstream
errors on long conversations instead of local compaction.
maxTokens: 64000 is a conservative output cap; Opus 5 supports up to 128000
and Meridian does not clamp it.
Registering a new provider rather than overriding anthropic means
installing this changes nothing until you select one of its models, so an
already-running agent is unaffected.
The before_provider_request hook is required. Prime Agent sends no session
identity of its own, and under passthrough every tool round ends in a
tool_result — which Meridian treats as a self-contained request needing no
session resume. Each round then replays the whole conversation into a fresh
session, and since that replay strips the assistant's tool_use blocks by
design, the model loses track of what it has already run. Stamping
metadata.user_id makes each round a continuation instead, so the SDK keeps the
real tool calls in its own session state. getSessionId() is distinct per
agent, so RLM children get their own keys rather than colliding with the parent.
Detection is by the x-meridian-agent: prime header above, or
MERIDIAN_DEFAULT_AGENT=prime. There is deliberately no User-Agent rule: in
API-key mode Prime Agent sends the generic Anthropic/JS <version> that every
Anthropic SDK client sends, so matching on it would misroute unrelated traffic.
Known limitation — file-change tracking. Meridian reads file changes out of
ipython cells for %%bash bodies, ! shell escapes, and await edit(path=…)
calls. It does not parse arbitrary Python writes (open(p, "w").write(…),
Path.write_text, library calls), which would need real dataflow analysis. Those
edits still happen; they just don't appear in Meridian's file-change summary.
Measured on 2026-08-13/14 against Prime Agent 0.7.2:
| Verified | How |
|---|---|
| Adapter selection, live turns | Real prime-agent binary through Meridian |
| Tool-round session continuity | 45/45 rounds resumed as continuations across 12 sessions |
| RLM children get distinct keys | Parent and depth-1 child carried different metadata.user_id on the wire |
Works with codeSystemPrompt off |
6 multi-round loops per arm; preset on vs off scored identically |
| Survives long idle gaps | 7-minute silence, then lineage=continuation and the model recalled a codeword set before the gap |
| Not metered as a third-party app | Negative control → full 18KB prompt → control again, all passing with the preset off |
Not verified: cron/scheduled tick delivery. Two attempts, neither
conclusive. Against a shared supervisor the scheduler fired (runs=3) but no
request reached Meridian. Against an isolated supervisor, schedule add itself
hung and never persisted the job. Session continuity across a worker restart is
likewise untested.
Four constraints make this awkward to exercise, all learned the hard way:
- Only resident workers can be scheduled against.
print,rpcandjsoncreate client-owned workers, which Prime Agent deliberately omits from schedules — so every headless mode is structurally ineligible. A resident worker needs an interactive session, which needs a TTY (a pty works). - The
--daemon-socketflag must come before the subcommand:prime-agent --daemon-socket <path> schedule list. Placed after, it is rejected as an unknown option, which reads misleadingly like the subcommand cannot target a custom socket. PRIME_AGENT_CODING_AGENT_DIRdoes not isolate a session from a shared supervisor. The supervisor writes sessions into whichever agent directory it was started with, so a test run against an already-running daemon lands its sessions in that daemon's store. The supervisor socket is likewise global per UID ($TMPDIR/prime-agent-<uid>/daemon.sock), so real isolation needs--daemon-socketand a matchingPRIME_AGENT_CODING_AGENT_DIR.- A dangling
~/.cache/meridianmakes Meridian return500 ENOENT … mkdir '~/.cache/meridian'on affected requests. The client retries and appears to hang, which is easy to misread as the agent stalling. Check that path resolves before diagnosing anything else.
A resident worker configured this way does reach Meridian — verified — so the remaining gap is specifically the scheduler, not the transport.
The long-idle result above covers the part Meridian owns — a turn arriving on a session after a quiet gap resumes rather than replaying.
Also not verified: the real client driving its own IPython kernel through a full tool loop. Kernel provisioning stalled in the isolated test environment; the tool-round evidence above comes from replaying Prime Agent's exact prompt and tool schema rather than from its kernel.
The metering result is a dated snapshot, not a property of the prompt. It
means "not flagged on this account at this moment" — the same classifier was
observed changing its answer within a single day during the OpenClaw work. If
you hit 400 You're out of extra usage on Prime Agent traffic, re-measure
before assuming the prompt is fine.
Claude Code can point at Meridian like any other Anthropic API client. The common use case is sharing a single Claude Max subscription from one host across other machines on your network — run Meridian on the box that is logged into Claude Max, then run Claude Code anywhere else against it.
# On another machine (or the same one)
ANTHROPIC_AUTH_TOKEN=x ANTHROPIC_BASE_URL=http://meridian-host:3456 claudeNote: Use
ANTHROPIC_AUTH_TOKEN(orANTHROPIC_API_KEY) — Claude Code treats both as bearer credentials. Set the value to yourMERIDIAN_API_KEYif you've enabled authentication, otherwise any string works.
⚠️ Security for multi-machine setups. If you expose Meridian beyond loopback (e.g. bind to0.0.0.0or a LAN IP), setMERIDIAN_API_KEYto a strong secret and require it on clients. An unprotected network-accessible proxy is a Claude Max credential leak — anyone who can reach the port can burn your subscription.
Claude Code is detected automatically via its claude-cli/* User-Agent.
Requests flow through the Claude Code adapter which:
- Parses the client's real working directory from its
Primary working directory:system-prompt line so Claude answers path-related questions with your local path, not the proxy host's. - Leaves the SDK subprocess cwd on the proxy host (Claude Code's local paths don't exist there).
- Runs in passthrough mode by default — Claude Code executes its own tools on the machine it runs on; Meridian just forwards tool_use blocks.
Run several configurations of the same adapter side by side — e.g. a passthrough variant with thinking enabled and one without, or a dedicated config for a specific client. Define instances in ~/.config/meridian/adapter-instances.json (or the MERIDIAN_ADAPTER_INSTANCES env var as a JSON string):
base— which built-in adapter provides the behavior (tool handling, session tracking, transforms). Existing plugins and transforms scoped to the base adapter apply to its instances automatically.features— per-instance overrides of the SDK feature toggles (thinking, system prompts, memory, ...) layered over the base's settings. Same keys as the settings UI.passthrough— per-instance passthrough mode, overriding the adapter default andMERIDIAN_PASSTHROUGH.match— optional automatic selection: exact header values and/or a User-Agent prefix. Match rules outrank built-in User-Agent detection (that's their purpose). Withoutmatch, select the instance per request withx-meridian-agent: <instance-name>.
Built-in adapter names are reserved and can't be shadowed. With no instances configured, detection is exactly the built-in chain. Config file changes apply within ~5s, no restart needed.
Meridian proxies the Claude Design MCP API (api.anthropic.com/v1/design/*), so MCP clients can use Claude Design tools through your local endpoint.
1. Add the MCP server. For Claude Code:
claude mcp add -s user --transport http claude-design http://127.0.0.1:3456/v1/design/mcpAny other MCP client: point it at http://127.0.0.1:3456/v1/design/mcp (streamable HTTP).
2. Grant Claude Design consent (one time, per Anthropic account). Tool calls return a needs_consent error until you enable it: open claude.ai/design/settings, find "Claude product access" ("Let other Claude products, like Claude Code, read and edit your Design projects"), and switch it On. This is a setting on the Anthropic account itself — with multiple Meridian profiles, the account behind the profile handling the request is the one that needs the toggle.
That's it — your existing Claude Max login covers auth (initialize, tools/list, and tool calls are all verified working with a plain Max token). Verify with a quick handshake:
curl -s -X POST http://127.0.0.1:3456/v1/design/mcp -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'Multiple profiles: design requests use the active profile by default. To pin design traffic to a specific profile regardless of which is active, register the server with a profile header:
claude mcp add -s user --transport http --header "x-meridian-profile: personal" \
claude-design http://127.0.0.1:3456/v1/design/mcpFallback OAuth flow: if the upstream ever rejects your token with an auth_error (scope enforcement has varied over time), /design-login obtains a dedicated token with the user:design:read/user:design:write scopes:
curl http://127.0.0.1:3456/design-login # returns an authorize URL — open it in your browser
curl -X POST http://127.0.0.1:3456/design-login \
-H 'content-type: application/json' \
-d '{"code": "<code-from-browser>"}' # paste the code you were shownThe design token is stored at ~/.config/meridian/design-token.json (mode 0600, global across profiles) and refreshed automatically when it expires.
Contributed by @sittitep (#543).
export ANTHROPIC_API_KEY=x
export ANTHROPIC_BASE_URL=http://127.0.0.1:3456Hermes profiles route through Meridian via their model config — no adapter-specific setup on the Meridian side:
# $HERMES_HOME/config.yaml
model:
provider: anthropic
base_url: http://127.0.0.1:3456Any ANTHROPIC_API_KEY value satisfies Hermes' credential check; Meridian
handles the real authentication. Note that Hermes profiles have isolated
HERMES_HOME directories: repeat the config for each profile.
Strongly recommended: install the
meridian-affinity Hermes plugin — without
it, every agentic turn ending in a tool_result is treated as an
independent session and prompt-cache reuse is lost (each turn re-bills
cache creation for the whole history). With it, sessions are resumed and
cache hit rates reach ~100% on long runs. The plugin also wires
x-request-id, x-meridian-source and x-opencode-effort for per-task
cost control and telemetry correlation.
Which adapter Hermes lands on. Hermes sends User-Agent: python-httpx,
which matches no detection heuristic, so plain Hermes traffic falls through
to whatever MERIDIAN_DEFAULT_AGENT names — opencode only when that
variable is unset. Installing meridian-affinity changes this: the
x-session-affinity header it injects resolves to the opencode
adapter regardless of the default.
That is usually what you want — the plugin's headers are the ones the
OpenCode adapter reads — but it is a real switch, not a no-op. If you run
MERIDIAN_DEFAULT_AGENT=pi (or anything else) and rely on that adapter's
transforms for Hermes, set the adapter explicitly per request rather than
letting the header decide.
{ "oc-thinky": { "base": "opencode", "features": { "thinking": "enabled" } }, "lite-plain": { "base": "passthrough", "passthrough": true, "match": { "userAgentPrefix": "litellm/" } }, "team-webui": { "base": "opencode", "features": { "codeSystemPrompt": false }, "match": { "header": { "x-team": "alpha" } } } }