English | 简体中文
Run Claude Code or Codex with OpenCode models through a local API adapter. Claude Code uses the local Anthropic-compatible Messages API; Codex uses the local OpenAI-compatible Responses API. The adapter translates requests to the upstream API, injects temporary credentials into the child process, and cleans up the local server when the child exits.
Status: Early-stage release. The protocol conversion layer and test suite are available, but real upstream API compatibility should be validated with your OpenCode account before production use.
- Quick Start
- Installation
- Commands
- Configuration
- Credentials and Profiles
- Providers
- Codex
- Models and Routing
- API Translation
- Token Usage Statistics
- Security and Privacy
- Platform Support
- Development
- CI and Publishing
- Troubleshooting
- Contributing
Requirements:
- Node.js 20 or newer
- An OpenCode API key
- Claude Code installed and available as
claudeonPATH - Codex installed and available as
codexonPATHwhen using Codex
npx @tanzz/agentx claudeOn an interactive terminal, AgentX prompts for your OpenCode API key on first use and walks you through provider/model selection (see Runtime configuration) — you don't set anything up yourself beforehand. The key is kept for the current session only and never written to disk; persisting it to your shell profile is a separate, explicit opt-in in agentx config. It is used to configure the adapter and inject the environment Claude Code needs. AgentX then starts a loopback-only adapter, waits for it to listen, launches Claude Code with temporary ANTHROPIC_* variables, forwards the terminal streams, and shuts the adapter down after Claude Code exits.
The real OpenCode key is never passed to Claude Code. Claude Code receives a random per-process local token instead.
Running non-interactively (CI, scripts, no terminal to prompt on)? Set AGENTX_OPENCODE_API_KEY up front — see Configuration.
See Commands below for codex, auth, usage, and quota.
Use without installation — note the @tanzz/ scope: the unscoped agentx name on npm belongs to an unrelated package, so npx agentx will not run this tool:
npx @tanzz/agentx claudeInstall globally:
npm install --global @tanzz/agentx
agentx claudeStart the adapter and Claude Code together:
agentx claude
agentx claude --model deepseek-v4-flash
agentx claude --port 9000 --host 127.0.0.1
agentx claude --native # skip the adapter; run the real `claude` with your own environmentStart the adapter and Codex together. Codex is launched with -c overrides that point an inline model provider at the local adapter:
agentx codex
agentx codex --native # skip the adapter; run the real `codex` with your own environmentRun any command through the local adapter. Unlike claude/codex, exec never shows the interactive runtime picker — it always resolves provider/model non-interactively (CLI flags → env vars → the most recent selection → built-in defaults), so it is safe to use in scripts and CI:
agentx exec -- claude
agentx exec -- opencode
agentx exec -- my-command --argumentBy default exec injects Anthropic-shaped environment variables (ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN/ANTHROPIC_MODEL), matching any tool that accepts an Anthropic-compatible endpoint. For a tool that only understands OPENAI_BASE_URL/OPENAI_API_KEY/OPENAI_MODEL, pass --client-protocol openai:
agentx exec --client-protocol openai -- my-openai-compatible-toolThe command's stdin, stdout, stderr, exit code, and termination signals are forwarded where supported by the host platform.
Start only the local adapter. Press Ctrl+C to stop it:
agentx proxyThe local API is exposed at http://127.0.0.1:<port> and provides GET /health, GET /v1/models, GET /v1/models/{id}, POST /v1/messages, POST /v1/messages/count_tokens, POST /v1/responses, and POST /v1/chat/completions. On startup, proxy prints the full URL of all three client-facing endpoints.
Every route except GET /health requires the adapter's local token; /health stays open so a supervisor can poll it. POST /v1/messages/count_tokens answers Claude Code's context-usage query: a native Anthropic upstream is asked for the exact count, and the other protocols — which have no equivalent endpoint — get a local estimate computed from the request itself.
Inspect the local environment and configuration:
agentx doctorThe report includes Node.js, platform/WSL status, architecture, API key presence, supported models, Claude Code discovery, and an upstream probe that reports whether the configured endpoint is reachable and whether it accepts the key — a rejected key being the most common reason a launch fails.
Scrub saved model ids that upstream no longer offers (for example an OpenRouter free launch that was renamed to its real vendor id):
agentx forgetThe command refreshes OpenRouter's live catalog and screens every remembered model id against it. Interactive terminals open the "saved models" manager so you can pick ids to forget; non-interactive terminals print the stale list:
agentx forget # output: DeepSeek:\n deepseek-v4-pro (no longer in the catalog)Forgetting removes every trace of the id from runtime.json — per-client
defaults, per-provider last model, and the most recent selection — so the
renamed id stops being offered as "current" on every launch.
Remove a custom provider entirely (definition and all saved memory of it, not
just a stale model id) with --provider <id> --remove-provider (see Custom
providers).
Show credential setup instructions and status (credentials live in environment variables; AgentX stores nothing itself):
agentx auth login --provider deepseek # print setup instructions
agentx auth status --provider deepseek # show current source and state
agentx auth logout --provider deepseek # explain how to remove the variableConfigure providers without starting the adapter or a client — inspect credential status and add or remove custom endpoints:
agentx config # interactive provider manager
agentx config --provider "My Local LLM" --base-url http://localhost:11434 --protocol chat-completions # non-interactive addAgentX keeps no key store of its own. Selecting an unconfigured provider (or adding one) in the interactive manager offers to load its API key and write it to your shell profile — zsh ~/.zshrc (honoring ZDOTDIR) or bash ~/.bashrc/~/.bash_profile — as an export block delimited by # >>> agentx credentials markers. The write happens only after you confirm where it goes, backs an existing profile up to <profile>.agentx.bak, replaces the block idempotently on later runs, and is undone by deleting the block (or restoring the backup); a warning is printed if the profile is readable by other local users. On later launches AgentX reads these blocks back automatically (current shell's profile only, environment variables still take precedence), so the provider works without sourcing the profile first — but the key is never copied into the environment the launched client inherits. Nothing is ever written without that confirmation, and shells AgentX can't edit safely (e.g. fish) fall back to printed instructions. Non-interactive terminals print the provider list with credential status instead.
Print token usage statistics collected from every request the adapter serves:
agentx usage # all time
agentx usage --period today # today / week / month / all
agentx usage --session <id> # totals for one client session
agentx usage --json # machine-readable output for scripts and status linesThe report groups tokens by provider and model and shows input/output/total
counts. Statistics are stored per adapter run; the optional --period flag
filters by time range, --session <id> narrows the report to a single client
session, and --json emits the same numbers as JSON instead of a table.
agentx usage --provider <id> is a deprecated alias for agentx quota --provider <id> (below); it still works but prints a deprecation notice.
Query provider quota (remote account balance/limit) where the upstream exposes one:
agentx quota --provider deepseek
agentx quota --provider openrouterOpenCode currently reports an explicit unsupported result because it does not expose a documented public quota endpoint.
agentx versionRuntime resolution for agent clients follows this order:
- Explicit CLI options (
--provider,--model,--api-key, …) - Saved default runtime for the client (from
runtime.json) - Environment variables (
AGENTX_PROVIDER,AGENTX_MODEL) - Interactive selection in the launcher (when no CLI/env/model override is present)
- The most recent selection, then built-in defaults (
opencode/gpt-5.6-luna)
Every selection made in the interactive launcher is persisted as the client's default, so the next launch starts from it.
For claude/codex, --native (or Launch native (skip AgentX) in the launcher) skips this resolution chain entirely — see Native launch.
| CLI option | Environment variable | Default | Description |
|---|---|---|---|
--api-key <key> |
AGENTX_OPENCODE_API_KEY (legacy OPENCODE_API_KEY also accepted) |
none | OpenCode credential |
--host <host> |
AGENTX_HOST |
127.0.0.1 |
Local bind address |
--port <port> |
AGENTX_PORT |
8787 |
Preferred local port |
--model <model> |
AGENTX_MODEL |
gpt-5.6-luna |
Concrete upstream model id |
--provider <id> |
AGENTX_PROVIDER |
none | Upstream provider (opencode, deepseek, openrouter) |
--background-model <id> |
AGENTX_BACKGROUND_MODEL |
none | Model for Claude Code's background (haiku) lane |
--effort <level> |
AGENTX_EFFORT |
none | Reasoning effort: codex none/minimal/low/medium/high/xhigh/max/ultra; claude low/medium/high/xhigh/ultracode |
--retry <n> |
AGENTX_RETRY |
3 |
Retry attempts on upstream 408/429/502/503/504 (0 disables) |
--max-concurrency <n> |
AGENTX_MAX_CONCURRENCY |
0 |
Max upstream requests in flight; extra requests queue locally (0 = unlimited) |
--client-protocol <anthropic|openai> |
anthropic |
exec only: env vars to inject for the launched program |
|
--verbose |
AGENTX_LOG_LEVEL |
info |
Reserved for verbose logging |
AGENTX_USAGE_DIR |
~/.config/agentx |
Directory for token usage statistics |
If the preferred port is already in use, the adapter tries subsequent ports. A non-loopback host is intentionally opt-in and should only be used on a trusted network:
agentx proxy --host 0.0.0.0agentx doctor accepts --client <claude|codex|all> (default all) to limit checks to one client, and --offline to skip the upstream probe (the only network-dependent check). Skipped checks are noted in the report.
Credentials come exclusively from environment variables: AgentX-specific variables are namespaced with the AGENTX_ prefix (e.g. AGENTX_OPENCODE_API_KEY) so they never clash with same-named variables set for other tools; at runtime the value is injected into upstream requests as the plain key — the prefix exists only in the variable name. A legacy unprefixed variable (such as OPENCODE_API_KEY) is still picked up directly if it is already set. Resolution order: --api-key, AGENTX_<PROVIDER>_API_KEY, legacy <PROVIDER>_API_KEY, then the matching block agentx config wrote to your shell profile, then an interactive prompt. On startup AgentX reads those profile blocks back (current shell's profile only) so a provider configured there works immediately, without sourcing the file first; environment variables always win over the profile, and the profile value is never merged into the process environment — only the adapter uses it, it is not inherited by the launched client. When you type a key interactively, AgentX keeps it for the current session only and prints the manual export … line you can add to your shell profile. The launch flow never writes to your shell profile itself; only agentx config will — after you explicitly confirm it — write the marked export block described there.
Non-secret runtime selection is stored in a single ~/.config/agentx/runtime.json file: per-client defaults, the last model per provider, and the most recent selection. API keys are never written to this file or to any AgentX-managed storage; the one place AgentX can write a key is the confirmed shell-profile block managed by agentx config, and that block is read back at startup as described above.
For Claude Code, the local token is injected as ANTHROPIC_AUTH_TOKEN rather than ANTHROPIC_API_KEY, matching provider integrations such as DeepSeek and avoiding Claude Code's custom API-key confirmation screen. The upstream key remains private to the adapter.
The adapter has three layers: a client layer for Claude Code/Codex, protocol adapters for Anthropic Messages and OpenAI Responses/Chat Completions, and a provider layer for upstream platforms.
Supported upstream providers:
| Provider | Credential (preferred) | Credential (legacy) | Example |
|---|---|---|---|
| OpenCode | AGENTX_OPENCODE_API_KEY |
OPENCODE_API_KEY |
gpt-5.6-luna |
| DeepSeek | AGENTX_DEEPSEEK_API_KEY |
DEEPSEEK_API_KEY |
deepseek-v4-pro |
| OpenRouter | AGENTX_OPENROUTER_API_KEY |
OPENROUTER_API_KEY |
anthropic/claude-sonnet-4 |
For scripts and advanced usage, --provider/--model override the configured runtime for a single invocation:
agentx claude --provider deepseek --model deepseek-v4-pro
agentx codex --provider openrouter --model anthropic/claude-sonnet-4These flags are the Advanced / Automation API: ordinary day-to-day provider switching happens in the interactive runtime configuration (see Runtime configuration). The equivalent environment variables are AGENTX_PROVIDER and AGENTX_MODEL (they also bypass the interactive launcher). Provider credentials are only used by the adapter and are never injected into the client process.
Every Claude Code model tier (main, opus/sonnet/haiku aliases, subagents) is pinned to the selected model — the user's choice is used for all traffic, including the small background requests Claude Code fires through its haiku tier (permission checks, topic detection, summarization). Optionally, --background-model <id> (or AGENTX_BACKGROUND_MODEL) routes just that background lane to another model the same provider serves — useful when the main model is a heavyweight reasoning model whose non-streaming auxiliary calls run past client timeouts. Requests naming a model the configured provider serves are honored as-is; unknown ids fall back to the configured model.
Claude Code's reasoning effort is supported the same way: agentx claude --effort low|medium|high|xhigh|ultracode (or AGENTX_EFFORT) passes its own --effort flag for the session, and each request's effort is converted for the upstream protocol.
Beyond the three built-in providers, you can register an arbitrary OpenAI- or Anthropic-compatible endpoint — a local model server (Ollama, vLLM, LM Studio), an internal gateway, or any other compatible API. In the interactive launcher's "Change Provider" list, choose Add custom provider…, then pick the protocol on a single screen — each option shows the path AgentX appends (/v1/messages, /responses, or /chat/completions), and Chat Completions carries a legacy note (it is OpenAI's earlier API, still the shape almost every third-party and local endpoint implements). The Base URL prompt repeats the chosen path, so enter the base URL only — then a display name, and the API key prompt follows. The same picker also offers Remove custom provider… once one exists. To do any of this without launching a client, run agentx config (see config).
A gateway that needs extra request headers — attribution headers, or its key under a name of its own — takes them from --header, which is repeatable and persists with the provider:
agentx config --provider MyGateway --base-url https://gw.example/v1 \
--header "HTTP-Referer=https://example.com" --header "X-Org-Id=acme"Provider headers are merged over AgentX's defaults, so a gateway that wants something other than Authorization: Bearer can say so. They are stored in runtime.json and must not carry secrets — API keys belong in the credential environment variables.
A custom endpoint exposes no model list, so the first launch asks for its model id directly — the internal placeholder model is never offered as a choice nor exposed to Codex's model picker — and remembers it like any other model afterwards. (Non-interactive runs without --model still fall back to the placeholder, so pass --model in scripts.)
For scripts and non-interactive use, agentx config --provider <name> --base-url <url> registers (and persists) a custom provider without launching a client; exec/claude/codex accept the same flags to define it and run in one step. --provider becomes its display name, and --protocol selects the upstream shape (chat-completions by default, or responses/anthropic):
# Define a local OpenAI-compatible server once, without launching a client
agentx config --provider "My Local LLM" --base-url http://localhost:11434 --protocol chat-completions --model llama3
# Or define it and launch in one step
agentx exec --provider "My Local LLM" --base-url http://localhost:11434 --protocol chat-completions --model llama3 -- claude
# A provider that speaks the native Anthropic Messages API
agentx config --provider "Internal Anthropic Gateway" --base-url https://gateway.internal --protocol anthropic --model claude-xOnce registered, reuse it by id (the name, lowercased and hyphenated) without repeating --base-url:
agentx claude --provider my-local-llmThe credential works exactly like a built-in provider's — set AGENTX_<ID>_API_KEY (uppercased, underscored) in your environment, or answer the prompt when asked; the connection metadata is persisted in runtime.json, but the API key never is (the only place a key is ever written is the confirmed shell-profile block described under config). Remove a custom provider entirely (definition and all saved memory of it, not just a stale model id) with:
agentx forget --provider my-local-llm --remove-providerBuilt-in providers cannot be removed this way. Both Claude Code and Codex can reach a custom provider regardless of which protocol it speaks — Codex talks to an anthropic-protocol custom provider through the same local translation layer that lets it reach Chat Completions upstreams (see API Translation).
When claude or codex is started on an interactive terminal without --provider/--model and without AGENTX_PROVIDER/AGENTX_MODEL, AgentX leads with a top-level menu that asks the transport question first — AgentX-routed or native — before anything else. Its options depend on what's already known for this client:
- Nothing configured yet:
Configure a provider/Launch native/Cancel. - A provider is configured, but this client hasn't launched before:
Select provider / model/Launch native/Cancel. - This client already has a saved default:
Start/Launch native/Change provider / model/Forget a saved model…(only once something is actually saved) /Cancel.
┌ Claude Code — AgentX
│
◆
│ ● Start DeepSeek / deepseek-v4-pro
│ ○ Launch native (skip AgentX)
│ ○ Change provider / model
│ ○ Forget a saved model…
│ ○ Cancel
└
Launch native only appears for clients with their own login/billing outside AgentX (Claude Code, Codex — see Native launch); Start only appears once this client has a saved default to reuse. Choosing to configure/select/change a provider opens the provider and model pickers:
┌ Claude Code — AgentX
│
◆ Provider
│ ● OpenCode (connected · 2 models)
│ DeepSeek (connected · 2 models)
└
The menu remembers which action (Start or Launch native) you picked last time, per client, and pre-selects it on the next launch. The model picker is searchable: type to filter the list by model id, with ↑/↓ to select and Enter to confirm. Switching provider automatically resolves a model for that provider and remembers the last model used on it. Completing the pickers always saves the selection as the client's default, so the next launch starts from it. Non-interactive sessions skip the UI and resolve --provider → env vars → saved default → built-in defaults.
Claude Code and Codex have their own login/billing outside AgentX, so both support a native launch that bypasses AgentX entirely: no provider/model resolution, no local adapter, no ANTHROPIC_*/OPENAI_* environment injection — the client runs exactly as if you had invoked it yourself. Reach it either way:
agentx claude --native
agentx codex --nativeor, on the top-level menu, choose Launch native (skip AgentX) — it's offered there regardless of whether a provider is configured yet or this client has launched before. --native combined with --provider/--model silently ignores them, since there is nothing left for AgentX to configure.
If --native runs nested inside a client AgentX itself launched — for example, typing agentx claude --native inside a Claude Code session started by agentx claude — the inherited environment still carries the outer launch's ANTHROPIC_*/OPENAI_* overrides. AgentX detects this (via an internal marker set on every environment it constructs) and strips exactly its own variables before spawning, so the nested client still starts native instead of silently pointing back at the adapter it's meant to skip. A hand-configured environment that never went through AgentX — including one that happens to set the same variable names for your own purposes — is left completely untouched.
AgentX remembers, per Claude Code/Codex session id, whether that session was last launched natively or through a specific provider/model. When you resume a known session with an explicit session id —
agentx claude -- --resume <session-id>
agentx codex -- resume <session-id>— AgentX looks up that id and relaunches it the same way automatically: native stays native, and an AgentX-routed session reuses its provider/model without showing the picker. Any explicit --native, --provider, or --model you pass still wins over the recalled record. A --resume/resume without an id (interactive picker, search term, or --continue/--last) can't be resolved ahead of the launch, so it falls back to the normal flow — but AgentX still records what that launch used once it starts, ready for the next explicit resume. This tracking is best-effort: it depends on locating the session's local transcript file, and simply does nothing if that lookup is inconclusive (e.g. another session was touched around the same time).
Start Codex with an OpenAI-compatible local Responses endpoint:
npx @tanzz/agentx codex
npx @tanzz/agentx codex --model gpt-5.6-lunaThe launcher passes -c overrides that define an inline agentx model provider pointing at http://127.0.0.1:<port>/v1, whose bearer token is the temporary local token injected as OPENAI_API_KEY. It also generates a model catalog (~/.config/agentx/codex-models.json, passed via model_catalog_json) so registry models — and any custom OpenRouter model id you enter in the launcher — resolve with real metadata instead of Codex's fallback-metadata warning: context windows and output limits for every provider come from the public models.dev registry when available, fall back to OpenRouter's public catalog for models models.dev lacks, and use conservative defaults otherwise. DeepSeek's deepseek-v4-pro/deepseek-v4-flash are an exception: they are OpenCode's own branding (served both through the OpenCode gateway and the direct DeepSeek provider), so neither public registry has a matching entry, and the catalog declares their real ~1M context window explicitly instead of falling back to a conservative 128k — otherwise Codex would auto-compact long DeepSeek sessions far earlier than necessary, the same class of issue CLAUDE_CODE_MAX_CONTEXT_TOKENS fixes for Claude Code (see Models and Routing). This works with current Codex releases (which no longer honor those environment variables) and skips Codex's sign-in screen entirely — no ChatGPT login or ~/.codex/auth.json required, and your existing ~/.codex/config.toml stays untouched. Codex can use both Responses and Chat Completions models: Responses models are passed through, while Chat Completions models are translated at the local Responses boundary. Claude Code and Codex can therefore use every model in the provider catalog.
Reasoning effort is configurable too. The generated catalog advertises Codex's full scale (none, minimal, low, medium, high, xhigh, max, ultra) for every model, so Codex's model picker opens a "Select Reasoning Level" step after you choose a model (max/ultra live behind its Advanced Reasoning step), and whatever you pick there is persisted by Codex itself. For one-off or scripted runs, agentx codex --effort <level> (or AGENTX_EFFORT) overrides it for that launch, becoming Codex's own -c model_reasoning_effort. The adapter maps the chosen level onto the upstream's native control — DeepSeek thinking/reasoning_effort, Anthropic thinking budgets — or passes it through unchanged for Responses upstreams.
The OpenCode model catalog is fetched only when no provider is selected or the selected provider is opencode, and only when the last-fetched snapshot (persisted in runtime.json) is more than 24 hours old — a fresh snapshot is reused without a network round trip. When the fetch is skipped, absent, or fails, the last persisted snapshot is used; if none has ever been saved, the built-in fallback catalog below is used:
| Model | Upstream protocol |
|---|---|
gpt-5.6-luna |
Responses API |
deepseek-v4-pro |
Chat Completions API |
deepseek-v4-flash |
Chat Completions API |
minimax-m3, minimax-m2.7, minimax-m2.5 |
Chat Completions API |
kimi-k3, kimi-k2.7-code, kimi-k2.6, kimi-k2.5 |
Chat Completions API |
glm-5.3, glm-5.2, glm-5.1, glm-5 |
Chat Completions API |
mimo-v2.5-pro, mimo-v2.5, hy3 |
Chat Completions API |
Models returned by the API use the Responses API (gpt-5.6-luna) or the Chat Completions API (everything else). The local /v1/models endpoint always reflects the current catalog; requests must resolve to a concrete configured model.
The OpenRouter provider accepts any model id (defaulting to OPENROUTER_MODEL or openai/gpt-4o-mini). In the interactive launcher its model picker includes three extra options:
- Search / enter any model id… — type any OpenRouter model id (e.g.
anthropic/claude-sonnet-4.5) directly. - Browse OpenRouter catalog… — search the full live catalog (~400 models) fetched from
https://openrouter.ai/api/v1/models, so you can find real vendor-prefixed ids (e.g.deepseek/deepseek-v4-pro) without typing them blind. The catalog is persisted inruntime.jsonfor offline screening. - Forget a saved model… — open the saved-model manager scoped to OpenRouter, so a custom id that was renamed or pulled upstream can be scrubbed without leaving the picker.
Because OpenRouter accepts free-form ids, a launch can save a model that later gets renamed or pulled upstream (e.g. a free tier renamed to its real vendor id). You can scrub such stale ids from inside the model picker itself, or run agentx forget for the full session (see forget).
Select a concrete model explicitly:
agentx claude --model gpt-5.6-lunaImplicit request-shape routing has been removed. Use an explicit background model when Claude Code's background lane should use another model from the same provider.
Claude Code assumes an unrecognized model has its default ~200k-token context window and auto-compacts well before that. DeepSeek's deepseek-v4-flash/deepseek-v4-pro (and their [1m] variants) actually offer a much larger window, so the claude launcher declares CLAUDE_CODE_MAX_CONTEXT_TOKENS and CLAUDE_CODE_AUTO_COMPACT_WINDOW for them automatically (unless you already set those variables yourself). Without this, long DeepSeek sessions were auto-compacted far earlier than necessary, silently dropping a large fraction of the conversation.
The adapter does not persist conversation history. Claude Code sends the complete conversation on each request; the only local persistence is aggregated token usage statistics (see Token Usage Statistics).
Supported translation areas include:
- Anthropic
systemcontent to Responsesinstructionsor a Chat Completions system message max_tokensto the upstream output-token limit- Anthropic text messages and response text
- Anthropic streaming events to Anthropic SSE events
- Anthropic
tools,tool_use, andtool_resultto function tools and function call outputs, including atool_resultwhose content is a block array: its text and its images are separated rather than serialized together as JSON (see Media in tool results) - Anthropic
thinking/output_config.effortto the upstream's reasoning controls (DeepSeek'sthinking/reasoning_effortover Chat Completions, orreasoning.effortover the Responses API) - Anthropic
tool_choiceto the upstream's Chat Completions or Responses tool-choice shape - Responses and Chat Completions usage data to Anthropic usage fields
- Responses requests/responses to and from a native Anthropic Messages API upstream (for a custom provider whose protocol is
anthropic), including streaming — this is the one direction that also runs for Codex, not just Claude Code, since Codex only ever sees the local Responses endpoint - Chat Completions requests/responses (the local
/v1/chat/completionsendpoint) to and from a native Anthropic Messages or Responses API upstream, including streaming; this conversion covers mainstream fields only (messages/tools/tool_choice/max_tokens/temperature/top_p/stop/stream) and does not map DeepSeek'sthinking/reasoning_effortextensions, which a generic Chat Completions client has no reason to send
For DeepSeek specifically, its thinking mode requires every assistant turn's reasoning_content to be echoed back anchored to the same message as the tool call it led to; the adapter keeps an assistant message's text, reasoning, and tool calls together instead of splitting them across separate messages, and only forwards reasoning_content for DeepSeek (other Chat Completions upstreams do not expect that field). An abnormal upstream stop — content_filter, insufficient_system_resource, or a stream that ends without either a finish_reason or [DONE] — surfaces as an error instead of silently reading back as a normal end_turn.
A tool can return an image — Claude Code's Read does, for an image file — as an image block inside tool_result.content. Each upstream protocol takes that differently, so the adapter places it where the protocol actually accepts one:
| Upstream protocol | Where the image goes |
|---|---|
anthropic |
Unchanged — the request is already Anthropic-shaped and passes through |
responses |
Inline, as input_image parts of the function_call_output's array output |
chat-completions |
Lifted out: the tool message keeps the text, and the images follow in their own user message, because a role:"tool" message accepts only a string |
When a model's catalog metadata states that it does not accept image input, the images are dropped and the tool message says so, rather than sending something the upstream would reject. A tool result that contained nothing but images still gets non-empty text, which several upstreams require.
The adapter translates tool protocols only. It does not execute tools and does not persist prompts, tool arguments, or conversation state.
Every successful request is automatically measured and normalized into a
provider-independent TokenUsage record. Providers supply their own usage
adapters (src/providers/usage/) that map provider-specific fields into the
common format; the core runtime only ever sees TokenUsage.
Usage is persisted to a local SQLite database (via Node's built-in
node:sqlite) at ~/.config/agentx/usage.db, with a JSON-file fallback when
node:sqlite is unavailable. Records store provider, model, input/output/
total tokens, cached and reasoning tokens, session id, and a timestamp.
For streaming responses the adapter captures usage from the provider's final
chunk when it is present. When the provider sends no usage, the adapter
accumulates deltas and marks the record as estimated.
Usage statistics are not exposed over HTTP. The adapter has no /usage/*
endpoints; read the data with the agentx usage CLI command, which
reads the storage backend directly.
- Statistics live in
~/.config/agentx/usage.db(orusage.jsonfallback). AGENTX_USAGE_DIRoverrides the storage directory.- Only token counts and metadata are stored — never prompts, tool arguments, or conversation content.
- The upstream API key is read from the CLI or environment and sent only to the configured provider.
- Claude Code receives a random, non-persisted local bearer token for each adapter process.
- The default listener is
127.0.0.1; the launch flow modifies no shell profile or permanent OS environment variable. The only exception is the explicitly confirmed profile write inagentx config. - The adapter has no
/usage/*or other unauthenticated HTTP endpoints for reading stored data; usage statistics are only readable locally via theagentx usageCLI command. - Logs must not contain API keys, authorization headers, prompts, or sensitive tool input.
- Treat
--host 0.0.0.0as a deliberate network exposure and protect it with appropriate network controls.
The launcher is designed for Linux, macOS, Windows, and WSL. WSL is detected using WSL_INTEROP and the local WSL Node.js process is used; the adapter does not depend on a Windows Node.js installation. Claude Code discovery supports the platform's normal executable resolution, including Windows shell execution.
git clone https://github.com/tzzs/agentx.git
cd agentx
npm ci
npm test
npm run lint
npm run buildThe project uses TypeScript, native Node.js fetch, Node.js ESM, and the built-in node:test runner. Tests are compiled into dist/test before execution.
npm run lint runs ESLint (flat config in eslint.config.js), npm run typecheck runs tsc --noEmit over src/ and test/, and make check runs both plus the package check. src/ is free of any: wire payloads are read through the typed accessors in src/json.ts.
To run one test file: npm run test:one -- dist/test/catalog.test.js (or make test-one F=test/catalog).
The test suite covers request/response conversion, system instructions, streaming events, tool calls, provider routing, chat-completion conversion, token usage adapters, storage, and the usage query API. Tests do not require an API key or network access.
GitHub Actions runs lint and type-check once per push, and the test suite on Node.js 20/22/24 across Linux, macOS and Windows, for every push and pull request against main. Release Please is configured in .github/workflows/release-please.yml and creates a release PR from conventional commits. Publishing is configured in .github/workflows/publish.yml:
- Add an
NPM_TOKENsecret to thenpmGitHub environment. - Push a tag matching
v*.*.*, or manually run Publish package. - The workflow runs the full test suite and publishes to
https://registry.npmjs.orgwith provenance.
OpenCode API key not found
Set AGENTX_OPENCODE_API_KEY (a previously set OPENCODE_API_KEY still works) or pass --api-key <key>. The key is required before the local server starts.
Claude Code was not found
Install Claude Code and ensure claude is available in the same shell's PATH, then run agentx doctor.
Codex not found: the "codex" command is not installed or not on PATH
When a client executable is missing, AgentX explains the problem and prints the recommended install command (for example, npm install -g @openai/codex), then exits. AgentX never installs clients for you — run the command yourself, then re-run the same agentx <client> command.
The port is busy
The adapter automatically tries the next ports after the configured port. Use --port to choose another starting point.
Upstream requests fail
Run agentx doctor, verify the API key and model availability, and check network access to the upstream provider. Do not paste API keys or authorization headers into issue reports.
Issues and pull requests are welcome. Keep changes focused, add or update tests for protocol behavior, run npm test, and avoid committing secrets or generated directories such as dist and node_modules.
MIT © tzzs