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).
- Zero new dependencies —
httpx/typer/click/openaiare already declared. - Reuses the settled adapter with no volk entanglement (call path below).
- The skill is complementary, not an alternative: the script does the work; the skill makes Claude discover it + invoke it with the right flags, model list, and redaction rules.
- MCP stays a future option only if many external-model tools are planned (revisit then).
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):
GET /api/v1/modelscatalog →--list-models(surfacesid,context_length,pricing.prompt/.completion; no hardcoding).- SSE streaming (
"stream": true,data:deltas atchoices[].delta.content,[DONE]) → optional--streamprinting to stdout.
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.
- Opt-in only. Never auto-slurp the repo. Context enters solely via explicit
--context-file/--contextflags. - Redaction pass before send (mandatory): refuse
.env/dotfiles; strip lines matching(API_KEY|SECRET|TOKEN|PASSWORD|Bearer ); skip.git. Print a one-line "redacted N lines" notice. - Token cap: enforce
--max-context-tokens; truncate + annotate rather than silently overflow the model'scontext_length. - Audit: append
{ts, model, prompt_sha256, context_files, bytes_sent, tokens}to a local log (not the payload itself) so external sends are traceable. - Attribution headers already identify the project to OpenRouter (
X-Title: Orchestrata,HTTP-Referer) — acceptable, noted.
How it plugs into the swarm patterns
- Cross-model judge panel / verify: a workflow verify-stage (or I) shells out to
ask_openrouter.py --model google/gemini-3.1-pro-preview --json "…refute this finding…"to add a cross-vendor vote alongside the Claude verifiers — diversity the single-vendor panel can't get. - Cheap delegation: route a shallow, well-scoped extraction to a cheaper OpenRouter model and verify with the deterministic gate (same propose-then-verify discipline as the Haiku tier).
Decisions to confirm (before build)
- Surface: CLI + skill (recommended) vs MCP server (defer). → recommend CLI+skill.
- Context sharing: explicit opt-in only (recommended) vs auto-attach session context.
- Redaction policy: the block/strip rules above — accept, or tighten (e.g. allowlist-only)?
- Streaming: include
--streamnow (low cost) or skip until needed? - Model exposure: expose the full catalog via
--list-models, or pin a curated default set (+ a default model)? - Audit logging: log size+hash of external sends (recommended) — acceptable, or none?
- 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).