diff --git a/docs/adr/0017-agent-session-is-provider-bound.md b/docs/adr/0017-agent-session-is-provider-bound.md index 331cf3a..47805b4 100644 --- a/docs/adr/0017-agent-session-is-provider-bound.md +++ b/docs/adr/0017-agent-session-is-provider-bound.md @@ -1,8 +1,8 @@ -# ADR 0017: Agent Runtime — Claude Code SDK +# ADR 0017: Agent Runtime — Claude Code SDK via OpenRouter ## Status -Accepted — supersedes the original ADR-0017 (provider-agnostic session). +Accepted. ## Context @@ -16,38 +16,42 @@ 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. The -Claude-to-IM library (2.7K stars) has already solved these, but its -LLMProvider interface expects Claude Code SDK's SSE event format. Using it with -a generic provider would require a conversion layer. +patch limitations, file support — each API quirk cost a round-trip. ## Decision -Adopt `@anthropic-ai/claude-agent-sdk` as the agent runtime. The SDK's `query()` -function 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. +Adopt `@anthropic-ai/claude-agent-sdk` as the agent runtime, routed through +OpenRouter's **"Anthropic Skin"** (`https://openrouter.ai/api`). -This means: -- The agent layer is **Anthropic-specific**, not provider-agnostic. Claude Code - SDK speaks the Anthropic Messages API and cannot be pointed at OpenRouter - (which is OpenAI-compatible). -- An Anthropic API key (or OAuth via Claude Pro/Max) is required. -- Model selection is within the Anthropic model family (Sonnet, Haiku, Opus), - not across vendors. +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. + +Environment variables: +``` +ANTHROPIC_BASE_URL=https://openrouter.ai/api +ANTHROPIC_AUTH_TOKEN= +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 means +**provider-agnosticism is preserved** — any OpenRouter model that supports tool +use can be used, not just Anthropic models. ## Consequences -- We lose the ability to route to GLM or other non-Anthropic models via - OpenRouter. This is the accepted cost of getting a mature agent loop. -- The Vercel AI SDK dependency is dropped. +- Provider-agnosticism preserved via OpenRouter — GLM, Claude, GPT, etc. all + work as long as the model supports tool use. +- 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 — - adopting Claude-to-IM for Feishu integration is a direct fit, no conversion. -- Per-run model selection still works (role→model mapping), but models are - Anthropic aliases, not OpenRouter routing IDs. -- If provider-agnosticism becomes a hard requirement again, the migration path - is: replace `query()` with a Vercel AI SDK `streamText` call + an SSE format - converter (~100 lines). The rest of the architecture (trigger, lock, - permission, Feishu client) is provider-agnostic already. +- 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). diff --git a/hub/.env.example b/hub/.env.example index 62ee565..73d9d30 100644 --- a/hub/.env.example +++ b/hub/.env.example @@ -1,28 +1,31 @@ # Hub runtime configuration. Copy to .env and fill in. -# -# For local dev: `cp .env.example .env` then edit. -# For tests: integration tests read from .env via dotenv. # PostgreSQL connection string. DATABASE_URL="postgresql://paradigm:paradigm@127.0.0.1:5432/paradigm" -# OpenRouter (or any OpenAI-compatible) API key. +# OpenRouter API key — used as ANTHROPIC_AUTH_TOKEN for Claude Code SDK. +# OpenRouter's "Anthropic Skin" (https://openrouter.ai/api) is Anthropic-API +# compatible, so Claude Code SDK speaks its native protocol directly. # Get one at https://openrouter.ai/keys -# The agent layer is provider-agnostic (ADR-0017); OpenRouter routes to any model. OPENROUTER_API_KEY="" -# Optional: override the base URL (e.g. direct vendor API, local gateway). -# OPENROUTER_BASE_URL="https://openrouter.ai/api/v1" +# Claude Code SDK auth — derived from OpenRouter. Do NOT set ANTHROPIC_API_KEY +# to a real value; it must be explicitly empty to avoid auth conflicts. +ANTHROPIC_BASE_URL="https://openrouter.ai/api" +ANTHROPIC_AUTH_TOKEN="" # Set to same value as OPENROUTER_API_KEY +ANTHROPIC_API_KEY="" -# Feishu (lark) bot credentials. Create a self-built app at -# https://open.feishu.cn/app, add the "Bot" capability, subscribe to -# im.message.receive_v1, then fill these in. +# Model override (optional). Defaults to Claude Sonnet. To use GLM: +# ANTHROPIC_DEFAULT_SONNET_MODEL="z-ai/glm-4.7" +# ANTHROPIC_DEFAULT_OPUS_MODEL="z-ai/glm-4.7" +# ANTHROPIC_DEFAULT_HAIKU_MODEL="z-ai/glm-4.6" + +# Feishu (lark) bot credentials. FEISHU_APP_ID="" FEISHU_APP_SECRET="" FEISHU_BOT_OPEN_ID="" -# Path to the `cph` binary (the Courseware-side checker, ADR-0016). -# Defaults to `cph` on PATH if unset. +# Path to the `cph` binary (ADR-0016). Defaults to `cph` on PATH. # CPH_BIN="/usr/local/bin/cph" # Hub server port. Defaults to 8788. diff --git a/hub/src/server.ts b/hub/src/server.ts index 54e6c23..56b183a 100644 --- a/hub/src/server.ts +++ b/hub/src/server.ts @@ -29,6 +29,14 @@ function requireEnv(name: string): string { async function main(): Promise { const databaseUrl = requireEnv("DATABASE_URL"); void databaseUrl; + + // Claude Code SDK auth via OpenRouter's Anthropic Skin. The SDK reads + // ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN from env. ANTHROPIC_API_KEY + // must be explicitly empty to avoid auth conflicts. + const openrouterKey = requireEnv("OPENROUTER_API_KEY"); + process.env["ANTHROPIC_BASE_URL"] ??= "https://openrouter.ai/api"; + process.env["ANTHROPIC_AUTH_TOKEN"] ??= openrouterKey; + process.env["ANTHROPIC_API_KEY"] ??= ""; const feishuAppId = requireEnv("FEISHU_APP_ID"); const feishuAppSecret = requireEnv("FEISHU_APP_SECRET"); const feishuBotOpenId = requireEnv("FEISHU_BOT_OPEN_ID");