Skip to content

OpenRouter addition#463

Open
benreceveur wants to merge 3 commits intogetpaseo:mainfrom
benreceveur:feat/openrouter-provider
Open

OpenRouter addition#463
benreceveur wants to merge 3 commits intogetpaseo:mainfrom
benreceveur:feat/openrouter-provider

Conversation

@benreceveur
Copy link
Copy Markdown

Summary

Adds a new workspace package @getpaseo/openrouter-agent — an ACP-compatible agent that bridges Paseo to OpenRouter's chat completions API, unlocking 200+ models (Claude, GPT-4o, Gemini, Llama 4, DeepSeek R1, etc.) as Paseo providers. Server-side, threads two new optional ACP provider override fields through the provider registry so agents that advertise slash commands asynchronously get proper UI sequencing.

Opened from my fork benreceveur/paseo — happy to discuss scope, naming, or split this into smaller PRs if that fits your review process better.

Motivation

Paseo currently supports Claude Code, Codex, and OpenCode. OpenRouter provides a unified API that routes to hundreds of model providers (many free-tier open-weight models like DeepSeek R1, Llama 4). Adding it as an ACP provider gives users one integration point for the long tail of models without Paseo needing to own each provider SDK individually.

What's in this PR

New package: packages/openrouter-agent/

  • ACP over stdio JSON-RPC — session lifecycle, tool calls, permission modes matching Claude Code conventions
  • Streaming with 30s connect / 60s idle timeouts and SSE parse-error threshold
  • Silent-turn fallback — always emits agent_message_chunk when the model ends turn with no content; surfaces captured reasoning_content for reasoning models (DeepSeek R1, o1-class, reasoner)
  • Tool-loop guard — early-exit after 5 consecutive content-less rounds instead of burning the full 25-round budget; reports tool-call frequency and captured reasoning in the fallback message
  • Tool result size cap (50KB per result) to prevent context bloat
  • Human-readable API errors — parses OpenRouter's nested error JSON (429/401/403/402/404/5xx) into markdown with remedy suggestions instead of raw JSON in the chat pane
  • macOS Keychain fallback for OPENROUTER_API_KEY via the security CLI (service com.paseo.openrouter-agent, account default). execFileSync avoids shell-injection risk; -w prints only the secret; no -g flag means no GUI prompts from the daemon subprocess
  • Hardened .gitignore denies .env*, *.pem/*.key/*.p12, secrets.json, credentials.json, logs, editor directories

Server wiring: packages/server/src/server/agent/

  • provider-launch-config.ts — adds optional waitForInitialCommands and initialCommandsWaitTimeoutMs to the ACP provider override schema
  • provider-registry.ts, providers/generic-acp-agent.ts — thread those options through to GenericACPAgentClient so agents that advertise slash commands asynchronously (openrouter-agent does during skill scan) can have the UI wait briefly before showing an empty command list

These changes are additive and opt-in — existing providers (Claude Code, Codex, OpenCode) are unaffected; the new fields are optional.

Config example

// ~/.paseo/config.json
{
  "agents": {
    "providers": {
      "openrouter": {
        "extends": "acp",
        "label": "OpenRouter",
        "command": ["openrouter-agent"],
        "waitForInitialCommands": true,
        "initialCommandsWaitTimeoutMs": 2000,
        "env": {
          "OPENROUTER_MODEL": "anthropic/claude-sonnet-4"
          // OPENROUTER_API_KEY can live here OR in macOS Keychain
        },
        "models": [
          { "id": "anthropic/claude-sonnet-4", "label": "Claude Sonnet 4", "isDefault": true },
          { "id": "openai/gpt-4o", "label": "GPT-4o" },
          { "id": "google/gemini-2.5-pro-preview", "label": "Gemini 2.5 Pro" },
          { "id": "meta-llama/llama-4-maverick", "label": "Llama 4 Maverick" },
          { "id": "deepseek/deepseek-r1", "label": "DeepSeek R1" }
        ]
      }
    }
  }
}

Seeding the key into macOS Keychain (recommended — avoids plain-text key on disk):

security add-generic-password -s "com.paseo.openrouter-agent" -a "default" \
  -w "sk-or-v1-..." -U

Design notes / open questions

  • Cross-platform keychain fallback: I implemented macOS Keychain only. Happy to make this symmetric with Linux (libsecret via secret-tool) and Windows (Credential Manager via PowerShell) if you'd like — held off to keep this PR focused.
  • Publishing: Package versioned to 0.1.56 to align with the monorepo sync script. Not added to release:publish yet — maintainer's call whether this should be on npm or remain internal/source-only.
  • Tool-loop guard threshold: The "5 consecutive content-less rounds" number was tuned against DeepSeek R1 behavior; configurable if you want it adjustable.

Test plan

  • npm run typecheck --workspace=@getpaseo/openrouter-agent — passes
  • npm run build --workspace=@getpaseo/openrouter-agent — builds clean
  • npm run typecheck --workspace=@getpaseo/server — passes with provider changes
  • npm run format — clean after auto-format (Biome)
  • Fresh Paseo session with Claude Sonnet 4 via openrouter provider completes normally
  • DeepSeek R1 silent-end-turn emits fallback message with captured reasoning
  • Llama 4 Maverick 429 returns markdown-formatted "upstream rate-limited" message (not raw JSON)
  • Agent boots with OPENROUTER_API_KEY unset in env — reads from keychain, logs using API key from macOS Keychain... to daemon.log

🤖 Generated with Claude Code

benreceveur and others added 3 commits April 15, 2026 13:36
Fix two TypeScript compilation errors in claude-agent.ts that blocked
the Electron desktop build:
- Add intermediate `unknown` cast for BetaRawMessageStreamEvent
- Add ImageMediaType alias to satisfy literal union type constraint

Disable auto-opening DevTools in detached mode during desktop dev,
which was spawning an unwanted separate window on every launch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: TypeScript errors in claude-agent and disable auto DevTools
Adds a new workspace package `@getpaseo/openrouter-agent` — an
ACP-compatible agent bridging Paseo to OpenRouter's chat completions
API. Users install the binary (npm link or publish) and wire it
into `~/.paseo/config.json` as an `extends: "acp"` provider.

packages/openrouter-agent (new workspace):
  - ACP agent over stdio JSON-RPC; session lifecycle, tool calls,
    permission modes matching Claude Code conventions.
  - Streaming with 30s connect / 60s idle timeouts and SSE
    parse-error threshold.
  - Silent-turn fallback: always emits agent_message_chunk when the
    model ends its turn with no content. Surfaces captured
    reasoning_content for reasoning models (DeepSeek R1, o1, reasoner).
  - Tool-loop guard: early-exit after 5 consecutive content-less
    rounds instead of burning the full 25-round budget. Reports tool
    call frequency and captured reasoning in the fallback message.
  - Tool result size cap (50KB) to prevent context bloat; CLAUDE.md
    injection size logged to stderr when > 4KB.
  - Human-readable formatting for OpenRouter API errors
    (429/401/403/402/404/5xx) with remedy suggestions.
  - macOS Keychain fallback for OPENROUTER_API_KEY via the security
    CLI (service com.paseo.openrouter-agent, account default). No
    shell invocation (execFileSync), no GUI prompts from daemon.
  - Hardened .gitignore denies .env*, certs/keys, secrets files.

packages/server:
  - provider-launch-config.ts: add optional waitForInitialCommands
    and initialCommandsWaitTimeoutMs to the ACP provider override
    schema.
  - provider-registry.ts, providers/generic-acp-agent.ts: thread
    those options through to GenericACPAgentClient so agents that
    advertise slash commands asynchronously (openrouter-agent does)
    can have the UI wait briefly before showing an empty command list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant