Skip to content

Apex — API Reference

Anthropic Messages API endpoints, authentication, request/response shapes for the Apex LLM gateway.

Apex is the CMMD LLM gateway. It exposes a stable, brand-controlled API surface at apex.cmmd.ai and routes per-model traffic to upstream providers (DeepSeek, Moonshot, or local Ollama on Mac Studio) without exposing those upstreams to consumers.

UseURL
Canonicalhttps://apex.cmmd.ai
Legacy alias (same backend; do not use for new code)https://apex-api.cmmd.ai

POST /v1/messages — Anthropic Messages API (canonical)

Section titled “POST /v1/messages — Anthropic Messages API (canonical)”
  • Native Anthropic Messages spec, no translation.
  • Streaming: stream: true → Anthropic SSE event types (message_start, content_block_start, content_block_delta, content_block_stop, message_delta, message_stop).
  • Tool use: native Anthropic tool_use / tool_result content blocks.
  • Auth required (Bearer or x-api-key).

POST /v1/chat/completions — OpenAI-compatible (local Ollama only)

Section titled “POST /v1/chat/completions — OpenAI-compatible (local Ollama only)”
  • Routes only to local apex-core on Mac Studio.
  • Cloud models (apex-flash / apex-general / apex-reason) won’t tool-call cleanly through this path — shape mismatch with Anthropic-native upstreams.
  • Discouraged for new code; use Anthropic instead.
  • Open (no auth required; used by health checks).
  • Returns OpenAI-style {"object":"list","data":[...]} with the 4 apex slugs.

Send one of:

Authorization: Bearer <APEX_API_KEY>

OR

x-api-key: <APEX_API_KEY>

The shim accepts both. Anthropic SDK uses x-api-key when constructed with apiKey:. Do NOT pass both apiKey AND authToken to the Anthropic SDK constructor — it sends both headers and is messy.

Cloudflare WAF — custom User-Agent required

Section titled “Cloudflare WAF — custom User-Agent required”

Cloudflare WAF blocks the default OpenAI/JS AND Anthropic/JS SDK user-agents at the edge (returns 403 — Your request was blocked before the request reaches the shim).

Override the UA in the SDK config:

const apex = new Anthropic({
apiKey: process.env.APEX_API_KEY,
baseURL: "https://apex.cmmd.ai",
defaultHeaders: {
"User-Agent": "<your-product>/<version>",
},
});

UAs known to bypass:

  • cmmd-apex-provider/1.0 (CMMD platform)
  • watchtower/cmmd-blackbox (Watchtower probes)
  • forge/0.x (Forge)
  • curl/8.x (manual testing)
  • claude-code-cli/* (Claude Code via ANTHROPIC_BASE_URL)
SlugUpstreamp95 latencyBest for
apex-coreLocal Ollama on Mac Studio (qwen3.5-122b-a10b)5–8sPrivate/sensitive (no data leaves CMMD), long context
apex-flashDeepSeek /anthropic (deepseek-v4-flash)~0.7sChat, low-latency UX
apex-generalMoonshot /anthropic (kimi-k2.6)~0.8sRAG, summarization, long context
apex-reasonDeepSeek /anthropic (deepseek-v4-pro thinking)~1.3sTool-heavy agents, deep reasoning

All four support streaming and tool use natively in Anthropic format.

Three tokens are active in the shim allowlist:

Token idConsumerStorage location
cmmd-prodCMMD platform on RailwayRailway env APEX_API_KEY
forge-prodForge cloud / desktopForge env APEX_API_KEY
githubGitHub Actions across CMMD-CenterGitHub org secret APEX_API_KEY

Token format: sk-apex-<id>-<32-char-secret>

Source of truth: ~/.apex/think-shim/tokens.db on Mac Studio. Only sha256 hashes are stored; plaintext exists only on the machine that issued the token. Treat tokens as one-write: paste into the secret store on issue, retrieve from there forever after.

Terminal window
# List all tokens (IDs + status + last_used, never plaintext)
sqlite3 ~/.apex/think-shim/tokens.db \
'SELECT id, status, last_used FROM tokens;'
# Issue a new token (prints plaintext ONCE)
~/.apex/think-shim/apex-token issue --id <consumer-name>
# Revoke
~/.apex/think-shim/apex-token revoke <consumer-name>
# Rotate (revoke + reissue same id, prints new plaintext ONCE)
~/.apex/think-shim/apex-token rotate <consumer-name>
# Reload tokens without restarting the shim
kill -HUP $(pgrep -f think-shim/shim.py)

After issuing or rotating, paste the new value into the consumer’s env immediately. If the consumer is on Railway, redeploy the service so the new env value takes effect.

POST /v1/messages HTTP/1.1
Host: apex.cmmd.ai
Authorization: Bearer <APEX_API_KEY>
Content-Type: application/json
anthropic-version: 2023-06-01
User-Agent: <product>/<version>
Terminal window
curl -sS https://apex.cmmd.ai/v1/messages \
-H "Authorization: Bearer $APEX_API_KEY" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-H "User-Agent: cmmd-smoke/1.0" \
--data '{"model":"apex-flash","max_tokens":20,"messages":[{"role":"user","content":"ping"}]}'

Expect 200 + JSON with content[0].text.

import Anthropic from "@anthropic-ai/sdk";
const apex = new Anthropic({
apiKey: process.env.APEX_API_KEY,
baseURL: "https://apex.cmmd.ai", // NO /v1 suffix
defaultHeaders: {
"User-Agent": "your-app/1.0", // bypass WAF
},
});
// non-streaming
const res = await apex.messages.create({
model: "apex-flash",
max_tokens: 1024,
messages: [{ role: "user", content: "hello" }],
});
// streaming
const stream = await apex.messages.stream({
model: "apex-flash",
max_tokens: 1024,
messages: [{ role: "user", content: "hello" }],
});
for await (const event of stream) {
// event.type: message_start | content_block_start | content_block_delta | ...
}
const final = await stream.finalMessage();
const res = await apex.messages.create({
model: "apex-reason", // tool-heavy workflows prefer thinking
max_tokens: 4096,
tools: [{
name: "get_user_by_id",
description: "Look up a CMMD user record",
input_schema: {
type: "object",
properties: { id: { type: "string" } },
required: ["id"],
},
}],
messages: [{ role: "user", content: "Find user 42" }],
});
// res.content may contain { type: "tool_use", id, name, input }
// reply with: { role: "user", content: [{ type: "tool_result", tool_use_id, content: "..." }] }
  • ❌ Don’t use OpenAI SDK or /v1/chat/completions for cloud models (apex-flash / apex-general / apex-reason) — tool-call shape mismatch.
  • ❌ Don’t reference upstream model IDs (deepseek-v4-pro, kimi-k2.6, etc.) in code. Always use apex-* slugs.
  • ❌ Don’t include /v1 in the baseURL — the SDK appends it.
  • ❌ Don’t pass both apiKey AND authToken to the Anthropic SDK constructor — sends duplicate headers.
  • ❌ Don’t send the default OpenAI/JS or Anthropic/JS UA — CF WAF blocks it.
  • ❌ Don’t try apex-*-lab slugs — LAB tier is currently paused.
  • ❌ Don’t hard-code token values in repos — pull from env.

Consumer → Cloudflare (apex.cmmd.ai) → cloudflared tunnel → Mac Studio shim (apex-think-shim on :11500) → upstream. The shim:

  1. Validates Bearer / x-api-key against SQLite allowlist.
  2. Looks up body.model in the in-process route table.
  3. Rewrites body.model to the upstream’s canonical ID (e.g. apex-reasondeepseek-v4-pro).
  4. Forwards the request to the upstream’s /anthropic/v1/messages (cloud) or local Ollama at /v1/messages (apex-core).
  5. Returns the response unchanged.

DeepSeek and Moonshot both expose /v1/messages natively in Anthropic format — there is no protocol translation in the path. That is why the shim is fast and simple.

See mac-studio/think-shim/shim.py for the implementation.