cambium ▸ docs/superpowers/specs/2026-06-15-claude-subscription-llm-provider-design.md
updated 2026-06-16

Route Cambium LLM calls to the Claude subscription

Date: 2026-06-15 Status: Approved (design) Author: Joshua + Claude

Problem

Workflow run 58f786ff-96e1-4eb8-a773-ef54e9863d19 (intent: “Gather AI headlines every day at 6am…”) failed at task process_0 (the summarize step) with:

LLM call failed: fetch failed

Root cause: the summarize step runs the hardcoded Ollama LlmCallExecutor (packages/hypha/src/task-runner.ts), which fetches http://localhost:11434/api/generate. Ollama is not running on this machine (nothing listening on :11434), so the connection is refused and the whole run fails. The fetches and merge before it all succeeded.

Two structural facts make this more than a “start Ollama” fix:

  1. The workflow’s llm_call strategy is served by the hardcoded Ollama executor.
  2. The auth-aware AuthenticatedLlmCaller (packages/providers) — which already has an AnthropicApiAdapter and a working Codex/ChatGPT subscription integration — is constructed in apps/loom/src/lib/services.ts but is never wired into the workflow llm_call. It is only used by cortex (heartbeat/personas/intent decomposition).

Goal

Move Cambium’s workflow LLM calls off Ollama and onto the user’s Claude subscription (Claude Code OAuth credentials at ~/.claude/.credentials.json), replacing Ollama as the default llm_call provider.

Decisions (confirmed with user)

Design

The Codex/ChatGPT subscription is already integrated through a repeatable pattern. This work mirrors that pattern 1:1 for Claude, then closes the gap that prevents the workflow task runner from using it.

A. Claude subscription provider (mirror of Codex)

Piece Codex (exists) Claude (new)
Local-creds resolver packages/providers/src/codex-auth-resolver.ts reads ~/.codex/auth.json packages/providers/src/claude-auth-resolver.ts reads ~/.claude/.credentials.json (claudeAiOauth.{accessToken,refreshToken,expiresAt})
Refresh OpenAI OAuth token endpoint, writes back to file Anthropic OAuth token endpoint (https://console.anthropic.com/v1/oauth/token, Claude Code client_id), writes accessToken/refreshToken/expiresAt back to file
Exports getCodexAccessToken, isCodexAuthAvailable getClaudeAccessToken, isClaudeAuthAvailable (export from packages/providers/src/index.ts)
Caller hook resolveCredentialAsync: if profile.id==='codex-oauth'getCodexAccessToken() add if profile.id==='claude-oauth'getClaudeAccessToken()
Seed seed.ts registers “OpenAI (Codex)” provider + codex-oauth auth profile register “Claude (subscription)” provider + claude-oauth auth profile, gated on isClaudeAuthAvailable()

Claude provider seed config:

{
  name: 'Claude (subscription)',
  kind: 'llm',
  endpoint: 'https://api.anthropic.com',
  capabilities: ['text_generation', 'reasoning', 'analysis', 'summarize_text'],
  config: {
    default_model: 'claude-sonnet-4-6',
    api_format: 'anthropic',
    auth_header: { header: 'Authorization', value_template: 'Bearer {{credential}}' },
  },
  cost_per_call: 0, // subscription, not per-token billing
}

Note: the stored token is currently expired (expiresAt ≈ Apr 2026), so the refresh path must work on the first call — it is not an edge case. Resolver caches the access token in-process and refreshes on expiry (with a small buffer), exactly like codex-auth-resolver.

B. Wire the workflow llm_call to Claude

Register an llm_call executor in apps/loom/src/lib/services.ts (which already constructs authenticatedCaller) that routes through AuthenticatedLlmCaller targeting the Claude provider. This replaces the default hardcoded Ollama LlmCallExecutor for workflow LLM tasks via taskRunner.registerExecutor('llm_call', …).

The executor lives in the loom app wiring (not hypha) because hypha must not depend on @cambium/providers (package boundary). It:

  1. Resolves the Claude provider + claude-oauth profile + fresh credential.
  2. Builds the prompt from task input (content/text/JSON of input), honoring an optional prompt_template with {{input}}, matching the existing LlmCallExecutor contract.
  3. Calls authenticatedCaller.call(provider, profile, credential, prompt, {model, …}).
  4. Returns { output: { content }, metadata: { provider_used, tokens_used } }.

Two OAuth-specific requirements (load-bearing)

  1. Headers. Subscription OAuth uses Authorization: Bearer <token> + anthropic-version: 2023-06-01 + anthropic-beta: oauth-2025-04-20 — not x-api-key. buildAuthHeaders (and/or the anthropic branch of AuthenticatedLlmCaller.call) must emit the version + oauth-beta headers for this provider. The provider’s auth_header template supplies the Bearer line; the version + beta headers are added for api_format === 'anthropic'.

  2. System prompt. OAuth tokens minted for Claude Code are rejected (401) unless the first system block is the Claude Code identity: "You are Claude Code, Anthropic's official CLI for Claude." The anthropic branch of AuthenticatedLlmCaller.call currently sends no system. It must inject the identity block as the first system block when the provider is the OAuth Claude provider; the actual task instruction stays in the user message. This is the primary implementation risk and will be validated with one live call before the rest is built.

Why claude-sonnet-4-6 specifically

The anthropic branch of AuthenticatedLlmCaller.call sends temperature. claude-sonnet-4-6 accepts it; claude-opus-4-8/4-7 would return 400 on temperature. The chosen model needs no change to the temperature handling.

Components & boundaries

Error handling

Testing

Out of scope