Crate's backend uses the Claude Agent SDK (@anthropic-ai/claude-agent-sdk) to power its music research agent. The Agent SDK works by spawning Claude Code as a subprocess via the query() function. This architecture is fundamentally incompatible with:
- Vercel serverless functions — Serverless environments don't support long-running subprocesses. Functions have execution time limits (60s default) and no persistent process state.
- Railway containers — Even with a full Docker container running Node.js, the Claude Code subprocess fails with
exit code 1. The CLI requires a specific environment setup (authentication, session management) that doesn't work in headless container environments. - Any standard deployment — The Agent SDK is designed for local CLI use where Claude Code is installed and authenticated interactively.
- Result:
Error: Claude Code process exited with code 1 - The Agent SDK's
query()spawns a Claude Code subprocess. Vercel's serverless runtime cannot run persistent subprocesses.
- Vercel offers a "Sandbox" product specifically for the Claude Agent SDK, but it requires enterprise access and has different constraints.
- Built a separate Express server deployed to Railway at
https://crate-agent-production.up.railway.app - Architecture: Vercel signs a JWT containing the user's API keys → browser sends JWT to Railway → Railway runs CrateAgent → streams SSE back
- Added
gitand@anthropic-ai/claude-codeto the Docker image - Result: Same
exit code 1. The Claude Code CLI subprocess fails even in a Docker container with the CLI installed.
- The JWT initially contained the full prompt suffix (~15KB). Moved prompt construction server-side to shrink the token.
- Also discovered trailing
\nin Vercel env vars (fromecho "value" | npx vercel env add) causing JWT signature mismatches. Fixed withprintf '%s'.
Replace the Agent SDK entirely with the direct Anthropic SDK (@anthropic-ai/sdk). Instead of spawning a subprocess, we:
- Convert tool definitions: The MCP tools in
crate-cliusetool()from the Agent SDK with Zod schemas. We convert these Zod schemas to JSON Schema format for the Anthropic Messages API'stoolsparameter. - Build a manual agentic loop: Send user message → if Claude responds with
tool_useblocks, execute the tool handlers directly in-process → send results back → repeat until Claude responds withend_turn. - Stream everything: Use the Anthropic SDK's streaming mode to emit
CrateEvents (answer_token, tool_start, tool_end, done, error) as SSE to the frontend.
- No subprocess: The Anthropic SDK makes HTTP calls to the Messages API. No CLI, no child processes, no special environment.
- Vercel-compatible: Standard HTTP requests fit perfectly in serverless functions. The streaming response keeps the connection alive within the 60s limit (configurable up to 300s on Pro plans).
- OpenRouter-compatible: The Anthropic SDK accepts a
baseURLparameter. Setting it tohttps://openrouter.ai/api/v1routes through OpenRouter with zero code changes. - Tools run in-process: The MCP tool handlers are plain async functions that make HTTP calls to external APIs (MusicBrainz, Discogs, etc.). They don't need a subprocess — they run directly in the Node.js runtime.
Before (Agent SDK + Railway):
Browser → /api/agent-token (Vercel, signs JWT)
→ Railway /agent/research (runs CrateAgent subprocess)
← SSE stream back to browser
After (Direct Anthropic SDK):
Browser → /api/chat (Vercel)
→ Anthropic Messages API (with tools)
→ Tool handlers run in-process
← SSE stream back to browser
server/directory (entire Railway Express server)src/app/api/agent-token/(JWT signing endpoint)src/lib/agent-token.ts(JWT signing utility).vercelignore(was excluding server/ from Vercel)AGENT_SIGNING_SECRETandRAILWAY_AGENT_URLenv varsjosedependency (JWT library)
src/lib/tool-adapter.ts— ConvertsSdkMcpToolDefinition(Zod schemas + handlers) to Anthropic API tool format (JSON Schema) and provides a tool executorsrc/lib/agentic-loop.ts— The manual agentic loop: message → tool_use → execute → loop, emitting CrateEvents as an async generator- Rewritten
src/app/api/chat/route.ts— Handles ALL tiers (chat + agent) using the direct SDK
The direct Anthropic SDK supports custom base URLs:
import Anthropic from "@anthropic-ai/sdk";
// Direct Anthropic
const client = new Anthropic({ apiKey: anthropicKey });
// Via OpenRouter — same API, different endpoint
const client = new Anthropic({
apiKey: openRouterKey,
baseURL: "https://openrouter.ai/api/v1",
});All tool_use, streaming, and the agentic loop work identically through OpenRouter since it's wire-compatible with the Anthropic Messages API.
- Keep
crate-clias a dependency — We importgetActiveTools()for tool definitions,getSystemPrompt()for the system prompt, andclassifyQuery()for tier routing. We just skip theCrateAgentclass and itsquery()calls. - Tools run in the API route process — The tool handlers are async functions that make HTTP calls. They run directly in the Vercel serverless function, no subprocess needed.
- Set env vars before tool resolution —
getActiveTools()checksprocess.envfor API keys to determine which servers are active. We set env vars from the user's resolved keys before calling it, and restore them after. - SSE format stays identical — The frontend already parses
CrateEventSSE. The new backend emits the exact same event types. - Increase maxDuration — Research queries can involve 10-25 tool calls. We increase the Vercel function timeout from 60s to 300s.
- Zod 4 native JSON Schema — crate-cli uses Zod 4 which has built-in
z.toJSONSchema(). No need forzod-to-json-schemathird-party lib. - Non-streaming for tool turns — The agentic loop uses non-streaming API calls during tool-use turns (where latency is dominated by tool execution), and chunks text for SSE delivery.
Completed:
src/lib/tool-adapter.ts— ConvertsSdkMcpToolDefinitionZod schemas to Anthropic API JSON Schema format, provides tool executorsrc/lib/agentic-loop.ts— Manual agentic loop with CrateEvent emissionsrc/lib/openui-prompt.ts— Copied from server/src/lib/ (OpenUI Lang system prompt)src/app/api/chat/route.ts— Rewritten to handle both chat-tier (fast) and agent-tier (agentic loop with tools)src/components/workspace/chat-panel.tsx— Simplified from two-step agent-token flow to single/api/chatendpoint- Build passing (TypeScript clean, Next.js build clean)
Ready to delete (after testing):
server/directory (Railway Express server)src/app/api/agent-token/route.ts(JWT signing endpoint)src/lib/agent-token.ts(JWT signing utility)src/hooks/use-crate-agent.ts(unused hook).vercelignore(was excluding server/)- Railway deployment at
https://crate-agent-production.up.railway.app - Vercel env vars:
AGENT_SIGNING_SECRET,RAILWAY_AGENT_URL josedependency (JWT library, check if used elsewhere first)