Skip to content

add openrouter remote backend; refactor engine-slot files#29

Merged
cjus merged 2 commits into
mainfrom
carlos/solrac-openrouter-remote-backend
May 17, 2026
Merged

add openrouter remote backend; refactor engine-slot files#29
cjus merged 2 commits into
mainfrom
carlos/solrac-openrouter-remote-backend

Conversation

@cjus
Copy link
Copy Markdown
Owner

@cjus cjus commented May 17, 2026

Summary

  • Adds OpenRouter as a third option for the no-prefix engine slot, alongside on-host Ollama and LMStudio. New REMOTE_ENABLED=true flag (mutually exclusive with LOCAL_ENABLED at boot) wires the engine slot to OpenRouter — for hosts that can't run a local LLM but still want a default-engine option that isn't Claude. Per-token cost from OpenRouter's streaming usage.cost chunk lands in audit.cost_usd, so the existing per-chat (HOURLY_COST_CAP_USD) and global (GLOBAL_HOURLY_COST_CAP_USD) hourly caps gate remote burn automatically — no new cost-cap knob. Claude tiers (@, !) unaffected.
  • Refactor: rename local-*engine-* for the mode-agnostic runner, with new engine-driver.ts shared abstraction and remote-driver.ts for OpenRouter. Structural-only, zero behavior change, zero env-var change, zero DB schema change. The original work landed on local-* files because the runner is mode-polymorphic; this follow-up un-lies the naming so OpenRouter ships clean. Git blame preserved via git mv.
  • Footer cost chip in remote mode — engine-slot Telegram footer now appends · $X.XXXX when remote mode reported a cost, e.g. <i>✅ remote:openrouter:openai/gpt-4o-mini · 1.2s · $0.0042</i>. Mirrors the Claude-tier $X.XXXX footer so operators get cost visibility on both surfaces. Gated by the same logic as the audit write (engine.ts::formatFooterCost) so UI ≡ audit.cost_usd.

Full details and rename mapping live in CHANGELOG.md's Unreleased section.

Motivation

  • OpenRouter unlocks Claude-free deploys that still get a meaningful default engine. Frontier models without an on-host GPU; cost gated by the existing cap infrastructure.
  • Naming honestylocal-driver.ts::createOpenrouterDriver is a lie. local.ts::buildLocalCapabilityNote({mode: "remote", ...}) is a lie. After the rename, each file's name describes what it owns; driver.mode is the single discriminator.

Zero-behavior-change guarantee (refactor portion)

  • Same code paths, same audit rows, same audit.model literals (local:% / remote:% unchanged — DB-format compat).
  • Same env vars (LOCAL_* namespace kept; REMOTE_* mirrors it).
  • Same /clear local semantics (now wipes engine slot regardless of mode).
  • 798 / 798 tests pass; tsc --noEmit clean.

Operator impact

  • Log-event renameslocal.*engine.* (runner-level) and local.<backend>_*<backend>.* (driver-internal). Mapping table in CHANGELOG.md. If you grep on the old names (local.done, local.boot, etc.), update queries before deploying. v0.8.0 audit rows on disk keep their original event names — only post-deploy events use the new names.
  • Telegram footer — remote-mode replies now include a $X.XXXX cost chip. Local-mode replies unchanged. Documented in docs/USAGE.md.
  • No env-var migrationLOCAL_* knobs still drive the local-mode path. REMOTE_* knobs added per docs/CONFIG.md.

Anti-goals intact

  • SDK pin still 0.2.119. No new runtime deps. No HTTP framework. No webhook. No Bedrock / Vertex / Docker / MarkdownV2.
  • Sub-agents still disabled (disallowedTools: ["Agent","Task"] + policy mirror + ENGINE_DENY_TOOLS).

Test plan

  • Local-mode smokecd solrac && LOCAL_BACKEND=ollama LOCAL_MODEL=gemma4:e4b npm run smoke:local. Expect pass for every phase; audit-tag rows read local:ollama:<model>.
  • Remote-mode smoke (billed)cd solrac && LOCAL_BACKEND=openrouter LOCAL_MODEL=openai/gpt-4o-mini REMOTE_API_KEY=sk-or-… npm run smoke:local. Expect audit.model = remote:openrouter:… + non-zero audit.cost_usd. Hard-skips with exit 0 when REMOTE_API_KEY is unset.
  • Live Telegram round-trip (local mode)npm run dev against staging with LOCAL_ENABLED=true. No-prefix message: 💻 stub → throttled edit → footer ✅ local:ollama:<model> · Ns (no cost chip). @hi / !hi still route to Claude tiers. Tail journalctl for engine.boot, engine.done, engine.boot_health_ok under the new event names.
  • Live deploy on remote mode — flip .env to REMOTE_ENABLED=true REMOTE_BACKEND=openrouter REMOTE_MODEL=openai/gpt-4o-mini REMOTE_API_KEY=sk-or-…, restart. Confirm footer includes · $X.XXXX; /help shows "remote (openrouter)"; /status shows the remote backend; audit rows tag remote:%; hourly cost cap window includes remote spend.
  • /clear local cross-engine semantics — on a chat with mixed remote:openrouter:% + claude:primary:% rows, run /clear local. Next remote turn doesn't see cleared remote history; next Claude turn's cross-engine bridge doesn't recite cleared remote rows.
  • Boot validation rejects misconfigLOCAL_ENABLED=true && REMOTE_ENABLED=true throws with the mutex error; SOLRAC_DEFAULT_ENGINE=local with both flags off throws with the "needs LOCAL or REMOTE" error.
  • Subprocess scrub — Claude-tier turn must not see REMOTE_API_KEY in the spawned claude subprocess env. Trust sanitizedSubprocessEnv() + existing scrub tests, or inspect SDK subprocess env.
  • Log-event grep migration — if any operator dashboards / alert rules grep on old event names, update per the CHANGELOG mapping table.

cjus added 2 commits May 17, 2026 09:18
Feature: openrouter as a third backend for the no-prefix engine slot,
mutually exclusive with LOCAL_* at boot. Per-token cost from the
streaming usage.cost field is written to audit.cost_usd so the existing
hourly cost caps (HOURLY_COST_CAP_USD, GLOBAL_HOURLY_COST_CAP_USD) gate
remote burn — no new cap knob.

Refactor (structural-only, zero behavior / env / DB change): split
local.ts → engine.ts + engine-tools.ts (via git mv to preserve blame);
add engine-driver.ts (shared EngineDriver abstraction) and
remote-driver.ts (openrouter). driver.mode is the single source of
truth for local vs remote; the LocalEngineMode type and the parallel
mode field on EngineRunDeps are deleted. Capability notes split into
buildLocalCapabilityNote ("free") + buildRemoteCapabilityNote
("per-token via OpenRouter"). Runner log events renamed local.* →
engine.*; driver log events use per-backend prefixes (ollama.*,
lmstudio.*, openrouter.*).

Full file / type / log-event mapping tables: CHANGELOG.md under
Unreleased.

Operator impact: env vars unchanged, slash commands unchanged, DB
schema unchanged. Boot-log event grep patterns break against new
logs; v0.8.0 audit rows on disk keep their old event names.

Verified: tsc --noEmit clean; bun test 798 pass 0 fail across 31 files
(-2 from the deleted buildToolCapabilityNote wrapper, coverage
preserved by the underlying per-mode builder tests).

Anti-goals: no new runtime deps, no HTTP framework, no sub-agents,
no SDK pin bump (still 0.2.119).
mirrors the claude-tier `$X.XXXX` footer chip. gated by
`mode === "remote" && costUsd !== null` so local mode is unchanged and
`remote.cost_missing` rows render without a misleading `$0.0000`.

threaded `costUsd` into renderFinal + renderToolLoopFinal; new
`formatFooterCost` helper centralizes the gate so it tracks
resolveAuditCost's matrix. doc updates in CHANGELOG + USAGE.
@cjus cjus merged commit f0a5008 into main May 17, 2026
1 check passed
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