Apex — Tool Use & Claude Code CLI Verification
End-to-end test results for tool calling and Claude Code CLI compatibility, per Apex model. Repeat this any time a new model is added.
Status as of 2026-05-19:
apex-coreandlab-flashpass tool use end-to-end.lab-generalis broken for tool use (abliterated parser mismatch). Claude Code CLI works againstapex-corevia LAN-direct localhost; Cloudflare-tunneled endpoints fail with 524 due to 100s gateway timeout.
Results matrix
Section titled “Results matrix”| Alias | Backing model | Box | /v1/messages + tools | Claude Code CLI (LAN) | Claude Code CLI (via CF) |
|---|---|---|---|---|---|
apex-core | qwen3.5:122b-a10b | Mac Studio | ✅ tool_use block | ✅ Verified end-to-end | ❌ CF 524 (cold) |
lab-flash | haervwe/GLM-4.6V-Flash-9B | Ryzen | ✅ tool_use block | Expected ✅ (not exercised) | ❌ CF 524 likely |
lab-general | huihui_ai/Qwen3.6-abliterated:27b | Ryzen | ❌ Emits raw text in thinking, parser misses it | ❌ Not viable | ❌ Also CF 524 + 4 min/turn |
Test 1: Direct /v1/messages curl with tools array
Section titled “Test 1: Direct /v1/messages curl with tools array”This is the protocol-level test. If this fails, nothing else can work.
apex-core — ✅ PASS
Section titled “apex-core — ✅ PASS”export KEY="$LITELLM_MASTER_KEY"curl -sS https://apex-api.cmmd.ai/v1/messages \ -H "Authorization: Bearer $KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{"model":"apex-core","max_tokens":256, "tools":[{"name":"get_weather","description":"Get current weather for a city", "input_schema":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}], "messages":[{"role":"user","content":"What is the weather in Tokyo? Use the get_weather tool."}]}'Response (truncated):
{ "stop_reason": "tool_use", "content": [ { "type": "thinking", "thinking": "..." }, { "type": "text", "text": "" }, { "type": "tool_use", "name": "get_weather", "input": { "city": "Tokyo" } } ]}stop_reason: tool_use confirms the model properly emitted a function call. Anthropic SDK clients will pick this up and execute the tool.
lab-flash — ✅ PASS
Section titled “lab-flash — ✅ PASS”Same curl, model lab-flash, endpoint https://lab-apex-api.cmmd.ai. Response:
{ "stop_reason": "tool_use", "content": [ { "type": "thinking", "thinking": "" }, { "type": "text", "text": "I will call the get_weather function to find out the current weather in Tokyo." }, { "type": "tool_use", "name": "get_weather", "input": { "city": "Tokyo" } } ]}GLM-4.6V’s native multimodal function calling works as advertised. Even better, it emits a useful preamble text before the tool call.
lab-general — ❌ FAIL
Section titled “lab-general — ❌ FAIL”Same curl, model lab-general. After 4 min 24 s (most weights running on CPU because 27B doesn’t fit Ryzen’s 8GB GPU):
{ "stop_reason": "max_tokens", "content": [ { "type": "thinking", "thinking": "Thinking Process:\n1. Identify the user's request: weather in Tokyo.\n2. Identify the available tool: get_weather...\n... `{\"name\": \"get_weather\", \"parameters\": {\"city\": \"Tokyo\"}}` ..." }, { "type": "text", "text": "" } ]}The model knew it should call the tool and wrote the tool call as plain text inside the thinking block. But:
- The call uses OpenAI-style
"parameters"key instead of Qwen-native"arguments" - There are no
<tool_call>...</tool_call>markers around it - Ollama’s
qwen3.5parser doesn’t extract this as a structuredtool_useblock - The model hits
max_tokens: 256still inthinkingmode before any structured output
Net: abliteration corrupted the tool-call template. Even with more tokens, the parser would not pick this up.
Test 2: Claude Code CLI end-to-end (apex-core via LAN-direct)
Section titled “Test 2: Claude Code CLI end-to-end (apex-core via LAN-direct)”The protocol test only proves LiteLLM ↔ Ollama plumbing. The real test is: does Claude Code CLI itself work?
# On Mac Studio (LAN-direct, bypass Cloudflare)mkdir -p /tmp/claude-apex-local && cd /tmp/claude-apex-localtouch hello.txt world.txt readme.md
export ANTHROPIC_BASE_URL="http://localhost:4000"export ANTHROPIC_AUTH_TOKEN="$LITELLM_MASTER_KEY"export ANTHROPIC_MODEL="apex-core"export ANTHROPIC_SMALL_FAST_MODEL="apex-core"unset ANTHROPIC_API_KEY # critical — otherwise Claude Code tries anthropic.com
claude --print --permission-mode bypassPermissions \ "List the files in this directory using the LS tool. Just list filenames. After listing them, say DONE."Output (after 8 min 23 s):
hello.txtreadme.mdworld.txt
DONEThis proves the full loop:
- Claude Code CLI sent a request with its system prompt + ~15 built-in tools (LS, Read, Write, Bash, Grep, Glob, Edit…)
- LiteLLM proxied the Anthropic-format request to Ollama’s
/api/chatwithtools: [...] - Qwen 3.5 122B-A10B emitted a
tool_useblock callingLSwith the cwd - LiteLLM translated back to Anthropic format
- Claude Code CLI received the
tool_use, executed the LS tool locally, captured the result - Sent the
tool_resultback as a follow-up message - Model received the tool output, generated final assistant text including “DONE”
- Loop terminated cleanly
This is the canonical proof that apex-core works as a Claude Code CLI backend.
Test 3: Claude Code CLI via Cloudflare endpoint — fails
Section titled “Test 3: Claude Code CLI via Cloudflare endpoint — fails”# Same setup but using the public endpointexport ANTHROPIC_BASE_URL="https://apex-api.cmmd.ai"# (everything else identical)claude --print --permission-mode bypassPermissions "List files..."Result:
API Error: 524 — The origin web server did not return a complete responsewithin the 120-second Proxy Read Timeout window.Root cause: Cloudflare Pro plan has a 100s proxy read timeout. The first turn of a Claude Code session sends a ~5–10k token system prompt + tool schemas. On a cold model, processing exceeds 100s before the first response byte. Cloudflare 524s the connection.
Workarounds (in priority order):
- LAN-direct from inside the network — set
ANTHROPIC_BASE_URL="http://mac-studio:4000"if your laptop can reach Mac Studio over the office LAN or via Tailscale/WireGuard. No CF in the path = no timeout. - Pre-warm the model before invoking Claude Code:
curl https://apex-api.cmmd.ai/v1/messages -d '{"model":"apex-core","max_tokens":10,"messages":[{"role":"user","content":"hi"}]}'. Onceollama psshows the model resident, subsequent requests start faster. - Enable streaming — verify LiteLLM is configured to stream upstream from Ollama. Cloudflare’s 524 only fires when the origin is silent for 100s; if bytes are flowing the connection lives indefinitely.
- Cloudflare Enterprise — extends timeout to 600s. ~$200+/mo. Not needed if streaming works.
Repeat-this-test recipe
Section titled “Repeat-this-test recipe”When you add a new Apex model alias, run all three tests:
# Test 1: protocol-level tool useKEY="$LITELLM_MASTER_KEY"MODEL="<new-alias>"ENDPOINT="<https://apex-api.cmmd.ai or https://lab-apex-api.cmmd.ai>"
curl -sS "$ENDPOINT/v1/messages" \ -H "Authorization: Bearer $KEY" \ -H "anthropic-version: 2023-06-01" \ --max-time 360 \ -d "{\"model\":\"$MODEL\",\"max_tokens\":256, \"tools\":[{\"name\":\"get_weather\",\"description\":\"Get weather\", \"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}}], \"messages\":[{\"role\":\"user\",\"content\":\"Weather in Tokyo? Use the tool.\"}]}" \ | jq '{stop_reason, content: [.content[] | {type, name, text: (.text // "")[:80]}]}'
# Pass criteria: stop_reason == "tool_use" AND content contains a tool_use block
# Test 2: full Claude Code CLI loop (LAN-direct from the host running LiteLLM)ssh <host> ' cd /tmp && mkdir -p ct && cd ct && touch a.txt b.txt export ANTHROPIC_BASE_URL="http://localhost:4000" export ANTHROPIC_AUTH_TOKEN="'"$KEY"'" export ANTHROPIC_MODEL="'"$MODEL"'" unset ANTHROPIC_API_KEY claude --print --permission-mode bypassPermissions "List files in this directory with LS, then say DONE."'
# Pass criteria: output contains the filenames AND "DONE"Drop the results into the Models page under the alias’s row.
Why we test all three layers
Section titled “Why we test all three layers”- Ollama capabilities (
ollama show) — necessary but not sufficient. A model can declaretoolscapability but still fail in practice (lab-general). - Direct curl — proves LiteLLM + Ollama plumbing works at the protocol level.
- Claude Code CLI end-to-end — proves the real user-facing surface works, including the full tool-call → tool-execute → tool-result → continue loop. This is the only test that catches things like “tool name collision” or “schema validation rejection” that protocol tests miss.
Related
Section titled “Related”- Models — per-alias capabilities matrix with verification status
- API Reference — endpoint surface
- Architecture — physical topology + Cloudflare tunnels
- ADR-0002 — Anthropic-Compat Endpoints — why we chose this protocol path
- ADR-0003 — Apex Model Choices — which models and why