agentproto-llm-endpoint · git:20261024.97a0074 · 2026-10-24 · sha256 cf7747fc5d4bf100
agentproto-llm-endpoint git:20261024.97a0074B
Immutable. This exact content never changes and is served at /api/v1/blob/cf7747fc5d4bf100.
---
name: agentproto-llm-endpoint
description:
Use the local LLM endpoint proxy (localhost:18090) — or your own gated public
deployment — to route Claude Code / Claude SDK through custom providers
(OpenRouter, Moonshot, Groq, ZAI) via an Anthropic-compatible gateway. Covers
adapter-specific behaviors, auth patterns, proxy codenames, and common failure
modes.
metadata:
tags:
agentproto, llm-endpoint, proxy, gateway, openrouter, moonshot, groq, zai,
xai, grok
---
## When to use
- "Route Claude through OpenRouter / Moonshot / Groq / ZAI / xAI"
- "Use the local LLM proxy" or "point Claude at my gated LLM endpoint"
- "Why is my model codename rejected?"
- "Claude Code vs Claude SDK with custom endpoints"
- Spawn failures: "Invalid API key", "model does not exist", empty turns
## The proxy
The LLM endpoint proxy lives in `projects/agentproto/ts/packages/inference-endpoint`.
It exposes an Anthropic-compatible `/v1/messages` endpoint and maps "secret
codenames" to real provider models.
### Codename → provider mapping
| Code | Provider | Real model | Claude equivalent |
| ----------- | ---------- | ------------------------------- | ------------------- |
| `jupiter-7` | Moonshot | `kimi-k2.7-code` | `claude-opus-4-8` |
| `mars-6` | Moonshot | `kimi-k2.6` | `claude-3-opus` |
| `saturn-5` | OpenRouter | `deepseek/deepseek-v4-pro` | `claude-sonnet-5` |
| `neptune-4` | OpenRouter | `anthropic/claude-sonnet-4.6` | `claude-sonnet-4-6` |
| `uranus-8` | OpenRouter | `google/gemini-3.1-pro-preview` | `claude-fable-5` |
| `mercury-9` | OpenRouter | `z-ai/glm-5.2` | `claude-3-haiku` |
| `halley-1` | OpenRouter | `deepseek/deepseek-v4-flash` | `claude-fable-4` |
| `orion-2` | OpenRouter | `xiaomi/mimo-v2.5` | `claude-opus-4-6` |
| `pegasus-3` | OpenRouter | `minimax/minimax-m3` | `claude-opus-4-9` |
| `lyra-4` | OpenRouter | `tencent/hy3-preview` | `claude-opus-4-7` |
| `vega-5` | OpenRouter | `stepfun/step-3.7-flash` | `claude-sonnet-4-5` |
| `venus-3` | ZAI | `glm-5.2` | `claude-3-5-fable` |
| `pluto-2` | Groq | `qwen/qwen3.6-27b` | `claude-haiku-4-5` |
| `atlas-6` | Groq | `llama-3.3-70b-versatile` | `claude-3-5-sonnet` |
| `titan-7` | Groq | `openai/gpt-oss-120b` | `claude-sonnet-4-7` |
| `nova-1` | xAI | `grok-4.5` | `claude-opus-4-8` |
| `pulsar-2` | xAI | `grok-3.5` | `claude-sonnet-5` |
### Running the proxy locally
```bash
# From repo root
pnpm --filter=@agentproto/inference-endpoint start
# → listens on http://localhost:18090/v1
```
Env vars the proxy reads:
- `MOONSHOT_API_KEY` — Moonshot native `/anthropic` endpoint
- `OPENROUTER_API_KEY` — OpenRouter (OpenAI-compatible adapter)
- `ZHIPUAI_API_KEY` or `ZAI_API_KEY` — ZAI (OpenAI-compatible adapter)
- `GROQ_API_KEY` — Groq (OpenAI-compatible adapter, 128-tool max)
The proxy adapts Anthropic Messages format to OpenAI Chat Completions for
OpenRouter/ZAI/Groq. Moonshot exposes a native `/anthropic` endpoint and needs
no adaptation.
### Client config
```
Base URL: http://localhost:18090/v1 (local), or your own gated public origin
API key: local → any non-empty value (e.g. AAAA); the proxy injects the real
upstream key server-side and, ungated, does not check it
public → Authorization: Bearer <endpoint gate token>
(set LLM_ENDPOINT_ACCESS_TOKENS on the proxy; a missing or
unlisted token returns 401 — the upstream key stays server-side)
```
The proxy accepts `?m=<code>` query param override and `?p=<provider>` override.
## Adapter-specific behaviors
### Claude SDK (`claude-sdk` adapter) — RECOMMENDED for proxy
- **Model validation**: NONE. Passes any model string directly to the SDK.
- **How model is sent**: `--model <value>` CLI arg → SDK `options.model`.
- **Base URL**: Set via `options.base_url` → `ANTHROPIC_BASE_URL` env.
- **Auth**: `ANTHROPIC_API_KEY` env (set via `--auth api-key`).
**Working spawn via MCP:**
```json
{
"adapter": "claude-sdk",
"auth": { "mode": "api-key" },
"model": "neptune-4",
"options": { "base_url": "http://localhost:18090/v1" },
"prompt": "..."
}
```
**Working spawn via CLI:**
```bash
# Primary path — the CLI has --base-url and --auth-token flags:
agentproto sessions start claude-sdk --model neptune-4 --auth api-key \
--base-url http://localhost:18090/v1 --auth-token AAAA --prompt "..."
# Alternative: set env vars before the CLI command
ANTHROPIC_BASE_URL=http://localhost:18090/v1 \
ANTHROPIC_API_KEY=AAAA \
agentproto sessions start claude-sdk --model neptune-4 --prompt "..."
```
### Claude Code (`claude-code` adapter) — LIMITED with proxy
- **Model validation**: STRICT. The `claude` binary validates `ANTHROPIC_MODEL`
against Anthropic's real model list. Proxy codenames (`neptune-4`, etc.) are
**rejected** before any network request.
- **How model is sent**: `ANTHROPIC_MODEL` env var (set via `options.model` →
adapter manifest `env: { ANTHROPIC_MODEL: "{value}" }`). The ACP wrapper
applies it via `session/set_config_option(configId: "model")` with best-effort
fallback to default on rejection.
- **Base URL**: Set via `options.base_url` → `ANTHROPIC_BASE_URL` env.
- **Auth**: `ANTHROPIC_API_KEY` env (api-key mode) or `CLAUDE_CODE_OAUTH_TOKEN`
(subscription mode).
**Result**: `claude-code` with proxy codenames fails with:
> "There's an issue with the selected model (neptune-4). It may not exist or you
> may not have access to it."
**Workaround**: Use `claude-sdk` instead for proxy access. If you must use
`claude-code`, you are limited to the proxy's regex fallback mappings:
| Model you send | Proxy routes to |
| ------------------ | ------------------------------------- |
| `claude-sonnet-5` | `openrouter:deepseek/deepseek-v4-pro` |
| `claude-opus-4-8` | `moonshot:kimi-k2.7-code` |
| `claude-fable-5` | `moonshot:kimi-k2.7-code` |
| `claude-haiku-4-5` | `groq:llama-3.3-70b` |
These are NOT the same as the codename mappings — you lose fine-grained
provider/model selection.
### Why Claude Desktop works with codenames
Claude Desktop (the app) uses the **Claude SDK directly** (not the `claude`
binary), and the SDK does not validate model names before sending. When you
configure a custom endpoint in Claude Desktop settings, the SDK sends the raw
model string to your proxy, which resolves the codename.
Claude Code (the CLI/`claude-code` adapter) uses the `claude` binary which has
strict model validation — this is why the same codename fails there.
## Auth patterns
### Direct Anthropic API (subscription or api-key)
```json
{
"adapter": "claude-code",
"auth": { "mode": "subscription" },
"prompt": "..."
}
```
Subscription mode uses `CLAUDE_CODE_OAUTH_TOKEN` (bearer token from
`claude auth login`). API-key mode uses `ANTHROPIC_API_KEY`.
### Gateway / proxy (Moonshot, OpenRouter, etc.)
```json
{
"adapter": "claude-sdk",
"auth": { "mode": "api-key" },
"model": "neptune-4",
"options": {
"base_url": "http://localhost:18090/v1",
"auth_token": "AAAA"
},
"prompt": "..."
}
```
The `auth_token` option sets `ANTHROPIC_AUTH_TOKEN` → `Authorization: Bearer`.
- **Local** (`localhost:18090`): ungated — any value works (`AAAA`); the proxy
ignores it and uses its own upstream keys.
- **Public** (a gated deployment): when `LLM_ENDPOINT_ACCESS_TOKENS` is set the
proxy enforces a bearer gate, so `auth_token` MUST be one of those gate tokens
— a shared secret you set **once** on the provider/adapter config (an unlisted
token → 401). The upstream provider key is still injected server-side; the gate
token is separate from it. Send it via `auth_token` (→ `Authorization: Bearer`)
or `X-Proxy-Access` — **never `x-api-key`**, which the gate ignores.
### Per-provider API keys
Stored in `~/.agentproto/providers.json` (mode 0600):
```json
{
"openrouter": { "apiKey": "sk-or-v1-...", "updatedAt": "..." },
"moonshot": { "apiKey": "sk-...", "updatedAt": "..." },
"anthropic": { "apiKey": "sk-ant-api03-...", "updatedAt": "..." }
}
```
Set via: `agentproto auth provider set <provider> <api-key> [--base-url <url>]`
or edit the file directly.
### Securing a public deployment (gate env vars)
Opt-in env vars on the proxy (unset = off). Full detail in the package README's
"Securing a public deployment".
- `LLM_ENDPOINT_ACCESS_TOKENS` — app-gate allow-list (comma-separated). Present a
listed token as `Authorization: Bearer <t>` **or** `X-Proxy-Access: <t>`.
**`x-api-key` is NOT accepted.**
- `LLM_ENDPOINT_EDGE_TOKENS` — second, independent layer via `X-Edge-Auth`, meant
to be enforced at the edge (Cloudflare WAF) so unauthenticated traffic never
reaches the origin; also re-checked in-process.
- `LLM_ENDPOINT_PUBLIC_MODELS=1` — exempts ONLY `/v1/models` (`/models`) from the
gates so clients can auto-discover models (e.g. Claude Desktop's launch probe);
pack lists stay gated.
`llm-endpoint print-waf-rule [--host <h>]` emits the Cloudflare custom-rule
expression from the configured token(s) — paste into a **Block** rule (add a
`/v1/models` carve-out when `PUBLIC_MODELS` is on).
## Common failure modes
### "Invalid API key" / "Invalid bearer token"
- Check `~/.agentproto/providers.json` has the right key for the provider.
- For gateway modes, ensure `auth_token` is set (not just `ANTHROPIC_API_KEY`).
- The `claude-code` adapter scrubs `ANTHROPIC_API_KEY` when `base_url` is set
(to prevent leaking your real Anthropic key to a third-party gateway).
### "Model does not exist" / "empty turn"
- `claude-code` adapter: you're using a proxy codename. Use `claude-sdk`
instead.
- `claude-sdk` adapter: check the base URL is actually set (CLI `--base-url`
flag, `ANTHROPIC_BASE_URL` env var, or MCP `options.base_url`).
- Proxy not running: `curl http://localhost:18090/v1/models` should return the
codename list.
### "empty turn — no assistant output, no tool call"
- The proxy received the request but the upstream provider returned nothing.
- Check proxy logs: `tail -f /tmp/llm-endpoint.log`
- Common cause: model name mismatch (proxy regex fallback routed to wrong
provider).
### Double `/v1/v1/` in proxy paths
The proxy base URL should end with `/v1` (e.g., `http://localhost:18090/v1`).
The claude binary appends `/v1/messages`, resulting in `/v1/v1/messages`. The
proxy handles this, but it's noisy in logs.
### Daemon caches old adapter code
The daemon (Node.js) caches `require()`/`import()` results. After editing an
adapter manifest, restart the daemon:
```bash
pkill -f "agentproto serve"
~/.agentproto/start-daemon-local.sh --cli workspace
```
## Quick reference: spawn patterns
| Goal | Adapter | Model | Base URL | Auth |
| ------------------------------- | ------------- | ----------------- | --------------------------- | -------------- |
| Direct Anthropic (subscription) | `claude-code` | (default) | (none) | `subscription` |
| Direct Anthropic (API key) | `claude-code` | (default) | (none) | `api-key` |
| OpenRouter Claude Sonnet 4.6 | `claude-sdk` | `neptune-4` | `http://localhost:18090/v1` | `api-key` |
| Moonshot Kimi K2.7 | `claude-sdk` | `jupiter-7` | `http://localhost:18090/v1` | `api-key` |
| Groq Llama 3.3 70B | `claude-sdk` | `atlas-6` | `http://localhost:18090/v1` | `api-key` |
| Claude Code + proxy (limited) | `claude-code` | `claude-sonnet-5` | `http://localhost:18090/v1` | `api-key` |
## See also
- `agentproto` skill — general daemon/CLI/MCP usage
- `ap-tunnels` / `ap-adapters` skills — tunnel creation, adapter catalog
- `projects/agentproto/ts/packages/inference-endpoint` — proxy source
- `projects/agentproto/ts/adapters/claude-sdk/src/index.ts` — SDK adapter
manifest
- `projects/agentproto/ts/adapters/claude-code/src/index.ts` — Code adapter
manifest