add openrouter remote backend; refactor engine-slot files#29
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
REMOTE_ENABLED=trueflag (mutually exclusive withLOCAL_ENABLEDat 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 streamingusage.costchunk lands inaudit.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.local-*→engine-*for the mode-agnostic runner, with newengine-driver.tsshared abstraction andremote-driver.tsfor OpenRouter. Structural-only, zero behavior change, zero env-var change, zero DB schema change. The original work landed onlocal-*files because the runner is mode-polymorphic; this follow-up un-lies the naming so OpenRouter ships clean. Git blame preserved viagit mv.· $X.XXXXwhen remote mode reported a cost, e.g.<i>✅ remote:openrouter:openai/gpt-4o-mini · 1.2s · $0.0042</i>. Mirrors the Claude-tier$X.XXXXfooter 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
local-driver.ts::createOpenrouterDriveris a lie.local.ts::buildLocalCapabilityNote({mode: "remote", ...})is a lie. After the rename, each file's name describes what it owns;driver.modeis the single discriminator.Zero-behavior-change guarantee (refactor portion)
audit.modelliterals (local:%/remote:%unchanged — DB-format compat).LOCAL_*namespace kept;REMOTE_*mirrors it)./clear localsemantics (now wipes engine slot regardless of mode).tsc --noEmitclean.Operator impact
local.*→engine.*(runner-level) andlocal.<backend>_*→<backend>.*(driver-internal). Mapping table inCHANGELOG.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.$X.XXXXcost chip. Local-mode replies unchanged. Documented indocs/USAGE.md.LOCAL_*knobs still drive the local-mode path.REMOTE_*knobs added perdocs/CONFIG.md.Anti-goals intact
0.2.119. No new runtime deps. No HTTP framework. No webhook. No Bedrock / Vertex / Docker / MarkdownV2.disallowedTools: ["Agent","Task"]+ policy mirror +ENGINE_DENY_TOOLS).Test plan
cd solrac && LOCAL_BACKEND=ollama LOCAL_MODEL=gemma4:e4b npm run smoke:local. Expect pass for every phase; audit-tag rows readlocal:ollama:<model>.cd solrac && LOCAL_BACKEND=openrouter LOCAL_MODEL=openai/gpt-4o-mini REMOTE_API_KEY=sk-or-… npm run smoke:local. Expectaudit.model = remote:openrouter:…+ non-zeroaudit.cost_usd. Hard-skips with exit 0 whenREMOTE_API_KEYis unset.npm run devagainst staging withLOCAL_ENABLED=true. No-prefix message: 💻 stub → throttled edit → footer✅ local:ollama:<model> · Ns(no cost chip).@hi/!histill route to Claude tiers. Tailjournalctlforengine.boot,engine.done,engine.boot_health_okunder the new event names..envtoREMOTE_ENABLED=true REMOTE_BACKEND=openrouter REMOTE_MODEL=openai/gpt-4o-mini REMOTE_API_KEY=sk-or-…, restart. Confirm footer includes· $X.XXXX;/helpshows "remote (openrouter)";/statusshows the remote backend; audit rows tagremote:%; hourly cost cap window includes remote spend./clear localcross-engine semantics — on a chat with mixedremote: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.LOCAL_ENABLED=true && REMOTE_ENABLED=truethrows with the mutex error;SOLRAC_DEFAULT_ENGINE=localwith both flags off throws with the "needs LOCAL or REMOTE" error.REMOTE_API_KEYin the spawnedclaudesubprocess env. TrustsanitizedSubprocessEnv()+ existing scrub tests, or inspect SDK subprocess env.