3.1 KiB
ADR 0017: Agent Runtime — Claude Code SDK via OpenRouter
Status
Accepted.
Context
The original ADR-0017 mandated a provider-agnostic agent layer: any
OpenAI-compatible model via OpenRouter, custom agent loop built on Vercel AI
SDK's streamText. The motivation was to avoid vendor lock-in.
In practice, the hand-rolled agent loop lacked capabilities that a production agent needs: context compaction (long conversations), tool approval flows, partial-message streaming, and a battle-tested multi-turn loop. Building these ourselves meant re-implementing what the Claude Code SDK already provides.
The Feishu integration also proved costly to hand-roll — card schema quirks, patch limitations, file support — each API quirk cost a round-trip.
Decision
Adopt @anthropic-ai/claude-agent-sdk as the agent runtime, routed through
OpenRouter's "Anthropic Skin" (https://openrouter.ai/api).
OpenRouter exposes an Anthropic Messages API-compatible endpoint. Claude Code
SDK speaks its native protocol directly to OpenRouter — no proxy, no format
conversion. The SDK's query() owns the agent loop (tool dispatch, compaction,
streaming). Built-in tools (Read, Write, Bash, Glob, Grep) cover the entire
tool surface — no custom tools needed. cph check / cph build run via Bash.
The Hub AgentSession is provider/role/model-bound, and it must persist the
provider runtime cursor needed to continue a conversation. For Claude Code SDK,
that cursor is the result.session_id; store it in AgentSession.metadata as
claudeSessionId and pass it back to the next query() call as
options.resume. Role is part of the session binding because role prompts and
tool surfaces can differ even when the underlying model is the same; /draft
and /review must not resume the same Claude runtime cursor by accident.
Environment variables:
ANTHROPIC_BASE_URL=https://openrouter.ai/api
ANTHROPIC_AUTH_TOKEN=<OpenRouter API key>
ANTHROPIC_API_KEY="" # must be explicitly empty
Model routing: ANTHROPIC_DEFAULT_SONNET_MODEL etc. accept OpenRouter model
IDs (e.g. z-ai/glm-4.7, anthropic/claude-sonnet-4-20250514). This gives
limited OpenRouter-level model routing, but the runtime is still Claude Agent
SDK-shaped: non-Claude models are best-effort and may not support every
Claude Code feature.
Consequences
- Provider-agnosticism is not fully preserved. OpenRouter can route to GLM, Claude, GPT, etc., but the runtime protocol and agent loop remain Claude Agent SDK-bound.
- The agent loop is Claude Code SDK's (compaction, tool approval, partial message streaming) — not hand-rolled.
- Custom tools (read_file, write_file, cph_check, cph_build) are replaced by the SDK's built-in Read/Write/Bash/Glob/Grep.
- The SSE event format matches Claude-to-IM's expected protocol natively.
- Per-run model selection works via role→model mapping; models are OpenRouter IDs, not Anthropic-only aliases.
- OpenRouter recommends setting Anthropic as the top-priority provider for Claude Code; non-Anthropic models may have reduced compatibility with some SDK features (e.g. thinking blocks).