Orchestrata Ultimate › Plans › openrouter-tool-design

Design — a Claude-invokable OpenRouter tool (cross-model calls + scoped context)

Source: plans/openrouter-tool-design.md at 8a2cb42ad · last changed 2026-07-27 · first committed 2026-07-05 · 108 lines.

Prose pending.

Plan text

Design — a Claude-invokable OpenRouter tool (cross-model calls + scoped context)

Task #26. Grounded in the read-only investigation (agent a2262…, 2026-07-05). Status: DESIGN + decisions. Implementation is gated on user approval — this tool sends project context to third-party models (outward-facing / privacy-sensitive), so it is not auto-built.

Goal

Give Claude Code (the orchestrator) and its swarms a way to call ANY OpenRouter-hosted model (GPT/Gemini/Kimi/Qwen/DeepSeek/…) with scoped context — for cross-vendor second opinions, diverse judge panels, and cheap delegation — distinct from the internal volk OpenRouter routing (which stays as-is).

Recommendation: a Bash CLI (scripts/ask_openrouter.py) + a thin skill

Chosen over an MCP server because the repo has zero MCP infrastructure (no .mcp.json, no mcpServers, no mcp/fastmcp dep) and its actual convention is script-CLIs Claude runs via Bash — with a direct precedent in rtm/scripts/perplexity_search.py (a third-party model CLI) and the ast-toolkit skill (a skill = a doc that teaches Claude to run a Bash CLI).

Reuse surface (from the investigation — do NOT rebuild the client)

OpenRouterAdapter (src/orchestrata_ultimate/providers/adapters/openrouter_adapter.py) is directly usable with just a ProviderConfig:

from orchestrata_ultimate.core.protocols import Message, ProviderConfig   # see NOTE
from orchestrata_ultimate.providers.adapters.openrouter_adapter import OpenRouterAdapter

cfg = ProviderConfig(api_key=KEY, base_url="https://openrouter.ai/api/v1", timeout_seconds=180.0)
adapter = OpenRouterAdapter(cfg)
resp = await adapter.complete(
    messages=[Message(role="system", content=sys), Message(role="user", content=user)],
    model="anthropic/claude-sonnet-4-6",
    parameters={"temperature": 0.2, "max_tokens": 4000})
text, tin, tout = resp.content, resp.input_tokens, resp.output_tokens

Wrap in asyncio.run(...). NOTE: import Message/ProviderConfig from the canonical provider module — that's exactly what the split-brain audit (#24) is resolving (core.protocols.providers vs core.interfaces.providers). The adapter uses core.protocols.providers; finalize the import once #24 lands. (cross-ref [[provider-protocol-reconciliation]])

Credentials: resolve OPENROUTER_API_KEY from process env, then .env (reuse client._load_dotenv() order). It is present today. Missing key → clear error, exit non-zero.

Two capabilities the adapter LACKS (add as small standalone helpers in the script, not in the adapter, to keep the wire minimal):

CLI interface (scripts/ask_openrouter.py)

ask_openrouter.py "PROMPT"            # or read prompt from stdin
  --model anthropic/claude-sonnet-4-6 # required; --list-models to browse
  --system "..."                      # optional system message
  --context-file PATH [--context-file PATH ...]   # OPT-IN context (redacted, capped)
  --max-context-tokens 8000           # hard cap; truncate+note if exceeded
  --temperature 0.2  --max-tokens 4000
  --stream                            # SSE to stdout
  --json                              # structured {content, model, input_tokens, output_tokens}
  --list-models [--free]              # print catalog (+pricing/context) and exit

Default = prompt-only, no project context unless --context-file/--context is passed.

Context-sharing + privacy design (the load-bearing part)

External models are third-party; the whole payload leaves the trust boundary.

How it plugs into the swarm patterns

Decisions to confirm (before build)

  1. Surface: CLI + skill (recommended) vs MCP server (defer). → recommend CLI+skill.
  2. Context sharing: explicit opt-in only (recommended) vs auto-attach session context.
  3. Redaction policy: the block/strip rules above — accept, or tighten (e.g. allowlist-only)?
  4. Streaming: include --stream now (low cost) or skip until needed?
  5. Model exposure: expose the full catalog via --list-models, or pin a curated default set (+ a default model)?
  6. Audit logging: log size+hash of external sends (recommended) — acceptable, or none?
  7. Build gate: because this exfiltrates context to third parties, confirm go/no-go before I implement scripts/ask_openrouter.py + .claude/skills/ask-openrouter/SKILL.md + tests.

Build sketch (once approved)

scripts/ask_openrouter.py (typer): arg parse → resolve key (env/.env) → assemble messages (+ redacted, capped context) → asyncio.run(adapter.complete(...)) or stream → print text/--json. Plus --list-models (httpx GET) and a .claude/skills/ask-openrouter skill documenting invocation, model list, and the redaction contract. Tests: key-resolution, redaction pass, token-cap truncation, catalog parse, arg→payload mapping (mock the LLM boundary only — the single sanctioned double).