Skip to content

Repository files navigation

agentx

CI npm License: MIT

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.

Contents

Quick Start

Requirements:

  • Node.js 20 or newer
  • An OpenCode API key
  • Claude Code installed and available as claude on PATH
  • Codex installed and available as codex on PATH when using Codex
npx @tanzz/agentx claude

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

Installation

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 claude

Install globally:

npm install --global @tanzz/agentx
agentx claude

Commands

claude

Start 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 environment

codex

Start 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 environment

exec

Run 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 --argument

By 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-tool

The command's stdin, stdout, stderr, exit code, and termination signals are forwarded where supported by the host platform.

proxy

Start only the local adapter. Press Ctrl+C to stop it:

agentx proxy

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

doctor

Inspect the local environment and configuration:

agentx doctor

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

forget

Scrub saved model ids that upstream no longer offers (for example an OpenRouter free launch that was renamed to its real vendor id):

agentx forget

The 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).

auth

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 variable

config

Configure 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 add

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

usage

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 lines

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

quota

Query provider quota (remote account balance/limit) where the upstream exposes one:

agentx quota --provider deepseek
agentx quota --provider openrouter

OpenCode currently reports an explicit unsupported result because it does not expose a documented public quota endpoint.

version

agentx version

Configuration

Runtime resolution for agent clients follows this order:

  1. Explicit CLI options (--provider, --model, --api-key, …)
  2. Saved default runtime for the client (from runtime.json)
  3. Environment variables (AGENTX_PROVIDER, AGENTX_MODEL)
  4. Interactive selection in the launcher (when no CLI/env/model override is present)
  5. 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.0

agentx 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 and Profiles

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.

Providers

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

Selecting a provider and model

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

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

Custom providers

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

Once registered, reuse it by id (the name, lowercased and hyphenated) without repeating --base-url:

agentx claude --provider my-local-llm

The 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-provider

Built-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).

Runtime configuration

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.

Native launch

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

or, 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.

Resuming a session

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

Codex

Start Codex with an OpenAI-compatible local Responses endpoint:

npx @tanzz/agentx codex
npx @tanzz/agentx codex --model gpt-5.6-luna

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

Models and Routing

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 in runtime.json for 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-luna

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

API Translation

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 system content to Responses instructions or a Chat Completions system message
  • max_tokens to the upstream output-token limit
  • Anthropic text messages and response text
  • Anthropic streaming events to Anthropic SSE events
  • Anthropic tools, tool_use, and tool_result to function tools and function call outputs, including a tool_result whose 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.effort to the upstream's reasoning controls (DeepSeek's thinking/reasoning_effort over Chat Completions, or reasoning.effort over the Responses API)
  • Anthropic tool_choice to 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/completions endpoint) 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's thinking/reasoning_effort extensions, 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.

Media in tool results

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.

Token Usage Statistics

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.

Streaming

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.

Query API

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.

Storage and Data

  • Statistics live in ~/.config/agentx/usage.db (or usage.json fallback).
  • AGENTX_USAGE_DIR overrides the storage directory.
  • Only token counts and metadata are stored — never prompts, tool arguments, or conversation content.

Security and Privacy

  • 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 in agentx config.
  • The adapter has no /usage/* or other unauthenticated HTTP endpoints for reading stored data; usage statistics are only readable locally via the agentx usage CLI command.
  • Logs must not contain API keys, authorization headers, prompts, or sensitive tool input.
  • Treat --host 0.0.0.0 as a deliberate network exposure and protect it with appropriate network controls.

Platform Support

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.

Development

git clone https://github.com/tzzs/agentx.git
cd agentx
npm ci
npm test
npm run lint
npm run build

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

CI and Publishing

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:

  1. Add an NPM_TOKEN secret to the npm GitHub environment.
  2. Push a tag matching v*.*.*, or manually run Publish package.
  3. The workflow runs the full test suite and publishes to https://registry.npmjs.org with provenance.

Troubleshooting

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.

Contributing

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.

License

MIT © tzzs

About

Local Anthropic/OpenAI-compatible API adapter for running Claude Code, Codex, or Pi against OpenCode, DeepSeek, OpenRouter, and other LLM providers.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages