Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 74 additions & 2 deletions packages/opencode/src/cli/cmd/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import { Filesystem } from "../../util/filesystem"
import { createOpencodeClient, type Message, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
import { Server } from "../../server/server"
import { Provider } from "../../provider/provider"
// altimate_change start — verifier-gated router (run cheap, verify, escalate)
import { Router } from "../../router/router"
import { Verifier } from "../../router/verifier"
import { Verdict } from "../../router/verdict"
import { Policy } from "../../router/policy"
// altimate_change end
import { Agent } from "../../agent/agent"
import { PermissionNext } from "../../permission/next"
import { Tool } from "../../tool/tool"
Expand Down Expand Up @@ -866,6 +872,67 @@ You are speaking to a non-technical business executive. Follow these rules stric
}
}

// altimate_change start — verifier-gated router orchestration
// Deterministic-verify the dbt workspace in cwd (`dbt build`, judged by Verifier).
// Only gates real dbt projects; with nothing to prove it returns ok (no escalation).
async function verifyWorkspace(): Promise<Verifier.Verdict> {
const root = process.cwd()
if (!(await Filesystem.exists(path.join(root, "dbt_project.yml"))))
return { ok: true, unverifiable: true, reason: "no dbt project to verify", checks: [] }
try {
const proc = Bun.spawn(["dbt", "build"], { cwd: root, stdout: "pipe", stderr: "pipe" })
// Hard timeout so a hung dbt (lock, prompt, runaway query) can't stall the run.
let timedOut = false
const timer = setTimeout(() => {
timedOut = true
proc.kill()
}, 300_000)
const out = (await new Response(proc.stdout).text()) + (await new Response(proc.stderr).text())
const code = await proc.exited
clearTimeout(timer)
if (timedOut) return { ok: false, reason: "dbt build timed out after 300s", checks: [] }
return Verifier.fromDbt(out, code)
} catch (e) {
// dbt binary missing / spawn failure → can't verify; mark unverifiable (fail-open, but honest).
return { ok: true, unverifiable: true, reason: `verify skipped: ${String(e)}`, checks: [] }
}
}

// Run the tier ladder: cheap → verify → escalate with failing-check context, stop at first pass.
// Each tier re-invokes the existing single-run path with that model (and the escalation note
// prepended) in the SAME workspace, so a later tier fixes the prior attempt rather than restarting.
async function runRouted(sdk: OpencodeClient) {
// Only route when the workspace is verifiable. Without a deterministic gate, routing
// would accept the cheapest tier with no way to verify or escalate — silently
// downgrading quality. In a non-dbt project, run once with the user's model instead.
if (!(await Filesystem.exists(path.join(process.cwd(), "dbt_project.yml")))) {
await execute(sdk)
return
}
const baseMessage = message
const policy = Policy.resolve()
const tiers = await policy.tiers({ prompt: baseMessage })
const result = await Router.route({
tiers,
runAgent: async (model, note) => {
args.model = model
message = note ? `${note}\n\n${baseMessage}` : baseMessage
await execute(sdk)
},
verify: verifyWorkspace,
})
message = baseMessage
const envelope = Verdict.build(result, { now: new Date().toISOString() })
if (args.format === "json") {
process.stdout.write(JSON.stringify({ type: "verdict", timestamp: Date.now(), ...envelope }) + EOL)
} else {
const tag = envelope.solved ? `✓ verified by ${envelope.solvedBy}` : "✗ unverified after all tiers"
UI.println(UI.Style.TEXT_INFO_BOLD + `~ router: ${tag} (policy: ${policy.source})`)
}
await Policy.reportOutcome(envelope)
}
// altimate_change end

if (args.attach) {
const headers = (() => {
const password = args.password ?? process.env.OPENCODE_SERVER_PASSWORD
Expand All @@ -875,7 +942,9 @@ You are speaking to a non-technical business executive. Follow these rules stric
return { Authorization: auth }
})()
const sdk = createOpencodeClient({ baseUrl: args.attach, directory, headers })
return await execute(sdk)
// altimate_change start — route when enabled, else single run
return Router.enabled() ? await runRouted(sdk) : await execute(sdk)
// altimate_change end
}

await bootstrap(process.cwd(), async () => {
Expand All @@ -884,7 +953,10 @@ You are speaking to a non-technical business executive. Follow these rules stric
return Server.Default().fetch(request)
}) as typeof globalThis.fetch
const sdk = createOpencodeClient({ baseUrl: "http://altimate-code.internal", fetch: fetchFn })
await execute(sdk)
// altimate_change start — route when enabled, else single run
if (Router.enabled()) await runRouted(sdk)
else await execute(sdk)
// altimate_change end
})
},
})
49 changes: 49 additions & 0 deletions packages/opencode/src/router/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Verifier-gated router

Run a cheap model first, verify the result deterministically, and escalate to a
stronger model only when verification fails. Most runs finish at the cheap tier;
the rest get a stronger attempt that receives the exact failing checks as context.
Flag-gated (`ALTIMATE_ROUTER`), default off — the normal single-model path is unchanged.

## Modules (pure, unit-tested)
- **`verifier.ts`** — `Verifier`: a deterministic `Verdict` from `dbt build`/`dbt test`
output (`fromDbt`, `parseDbtSummary`, `failingNodes`). `Impl` is the pluggable
verifier interface; the default `dbtVerifier(run)` shells dbt (runner injected,
fail-open). `ALLOW_ALL` passes everything when no verifier is configured.
- **`router.ts`** — `Router`: the escalation mechanism. `route({tiers, runAgent, verify})`
runs each tier, verifies, escalates on a failed verdict with the failing checks
(`escalationContext`), stops at the first pass. `DEFAULT_LADDER` is ordered
cheapest → strongest; override via `ALTIMATE_ROUTER_LADDER`.
- **`policy.ts`** — `Policy`: where the ladder comes from. `STATIC` is the built-in
default; `altimate(key)` fetches a per-context ladder from the altimate API when
`ALTIMATE_API_KEY` is set (degrades to static on any failure); `resolve()` picks
between them; `reportOutcome()` posts verified outcomes back (key-gated, best-effort).
`sanitizeTiers` validates + caps any ladder from the API.
- **`verdict.ts`** — `Verdict.Envelope`: a machine-checkable record of the result
(which tier, per-attempt history, checks, evidence hash, timestamp, optional signature).

## Configuration
- `ALTIMATE_ROUTER=1` — enable routing (default off).
- `ALTIMATE_ROUTER_LADDER` — comma-separated `provider/model` ids to override the default ladder.
- `ALTIMATE_API_KEY` / `ALTIMATE_API_URL` — use the altimate API for the routing policy
and outcome reporting instead of the static ladder.

## Integration
`src/cli/cmd/run.ts` (`RunCommand`): when `Router.enabled()`, the run resolves a policy,
runs each tier by re-invoking the existing run path with that model (escalation note
prepended) in the same workspace, verifies with `dbt build` between tiers, and emits a
verdict envelope. The default (non-router) path is untouched.

## Tests
- **Unit** — `test/router/{verifier,router,verdict,policy}.test.ts`. Pure logic, incl.
adversarial cases (dbt summary-line injection, ANSI/huge/multi-summary output,
endpoint response validation/capping).
- **E2E** (`test/router/*.e2e.test.ts`, env-gated — require docker + a dbt image +
network, excluded from default CI):
- `verifier.e2e` — real `dbt build` (pass / compile-error / failing-test) and that a
model emitting a fake summary does not change the verdict. `E2E_IMG=<image> bun test verifier.e2e`.
- `router.e2e` — real model calls + real dbt: cheap tier solves; an unsatisfiable
workspace escalates through tiers, caps, and threads failing-check context.
`OPENROUTER_API_KEY=… E2E_IMG=… bun test router.e2e`.
- `policy.e2e` — real network: live local server (incl. error/malformed/oversized
responses) and an unreachable endpoint, all degrade gracefully. `bun test policy.e2e`.
131 changes: 131 additions & 0 deletions packages/opencode/src/router/policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* Routing policy — the decision of WHAT to route to, kept separate from the
* mechanism that executes it.
*
* Two sources:
* - `STATIC`: the built-in default ladder, always available.
* - `altimate(key)`: when an altimate API key is configured, the routing policy is
* fetched per-context from the altimate API and used instead, and verified
* outcomes are reported back so the policy can be tuned over time.
*
* The client executes whatever policy it is handed. The SaaS policy activates only
* when `ALTIMATE_API_KEY` is present, otherwise the static ladder is used.
* Network/transport failures degrade to STATIC.
*/
import { Router } from "./router"
import type { Verdict } from "./verdict"

export namespace Policy {
/** Signals available for routing decisions (extended over time). */
export interface RoutingContext {
prompt?: string
projectType?: string
taskId?: string
}

export interface RoutingPolicy {
source: "static" | "altimate"
tiers(ctx: RoutingContext): Promise<Router.Tier[]>
}

type Fetch = typeof globalThis.fetch

/** Defensive cap: a bad/compromised policy endpoint must not inject a cost-bomb ladder. */
export const MAX_TIERS = 8

/**
* Validate + cap a ladder returned by the policy endpoint. Keeps only entries with a
* non-empty string `model`, derives a label when missing, caps to MAX_TIERS. Returns
* null if nothing usable (caller falls back to the static ladder).
*/
/** A model id must look like `provider/model[/...]` — plain chars only, no whitespace/control. */
const MODEL_RE = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+$/

export function sanitizeTiers(raw: unknown): Router.Tier[] | null {
if (!Array.isArray(raw)) return null
const out: Router.Tier[] = []
for (const t of raw) {
const model = (t as any)?.model
if (typeof model !== "string") continue
const m = model.trim()
if (!m || m.length > 200 || !MODEL_RE.test(m)) continue
const rawLabel = typeof (t as any)?.label === "string" && (t as any).label ? (t as any).label : m.split("/").pop() || m
// Strip non-printable/ANSI — the label is printed to the terminal.
const label = String(rawLabel).replace(/[^\x20-\x7E]/g, "").slice(0, 100) || m
out.push({ model: m, label })
if (out.length >= MAX_TIERS) break
}
return out.length ? out : null
}

export function apiKey(): string | undefined {
return process.env["ALTIMATE_API_KEY"] || undefined
}

export function baseUrl(): string {
return process.env["ALTIMATE_API_URL"] || "https://api.altimate.ai"
}

/** Built-in default ladder (env-overridable via ALTIMATE_ROUTER_LADDER). */
export const STATIC: RoutingPolicy = {
source: "static",
async tiers() {
return Router.ladder()
},
}

/**
* Customer routing policy served by the altimate API. Resolves the per-context
* ladder for this account; degrades to the static ladder if the service is
* unreachable or returns nothing usable.
*/
export function altimate(key: string, base: string = baseUrl(), fetchImpl: Fetch = fetch): RoutingPolicy {
return {
source: "altimate",
async tiers(ctx: RoutingContext): Promise<Router.Tier[]> {
try {
const res = await fetchImpl(`${base}/v1/router/policy`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
body: JSON.stringify(ctx),
signal: AbortSignal.timeout(3000),
})
if (!res.ok) return Router.ladder()
const data = (await res.json()) as { tiers?: unknown }
return sanitizeTiers(data?.tiers) ?? Router.ladder()
} catch {
return Router.ladder()
}
},
}
}

/** The active policy: customer policy when an altimate key is set, else the static ladder. */
export function resolve(fetchImpl: Fetch = fetch): RoutingPolicy {
const key = apiKey()
return key ? altimate(key, baseUrl(), fetchImpl) : STATIC
}

/**
* Report a verified outcome back to the altimate service so the customer's policy
* improves. Best-effort and key-gated — a no-op without a key, and never throws.
*/
export async function reportOutcome(
envelope: Verdict.Envelope,
base: string = baseUrl(),
fetchImpl: Fetch = fetch,
): Promise<void> {
const key = apiKey()
if (!key) return
try {
await fetchImpl(`${base}/v1/router/outcomes`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
body: JSON.stringify(envelope),
signal: AbortSignal.timeout(3000),
})
} catch {
/* best-effort: outcome reporting must never break the run */
}
}
}
109 changes: 109 additions & 0 deletions packages/opencode/src/router/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* Verifier-gated model router — the escalation ladder.
*
* Run the CHEAP tier first; verify the workspace deterministically (Verifier);
* if the verdict is not ok, escalate to the next stronger tier, handing it the
* exact failing checks so it fixes rather than restarts blind. Stop at the first
* passing verdict (or the top of the ladder).
*
* Because the cheap tier handles most tasks, escalation is rare. The default ladder
* is ordered cheapest → strongest and can be overridden per deployment.
*
* Pure orchestration: `runAgent` + `verify` are injected → unit-testable without
* a live model or dbt. Flag-gated (`ALTIMATE_ROUTER`); default off.
*/
import { Verifier } from "./verifier"

export namespace Router {
export interface Tier {
model: string
label: string
}

/**
* Default ladder, ordered cheapest → strongest. A tier is only reached when the
* previous tier's output fails verification, so most runs complete at the cheap tier.
* Override per deployment via `ALTIMATE_ROUTER_LADDER` or an injected policy.
*/
export const DEFAULT_LADDER: Tier[] = [
{ model: "openrouter/deepseek/deepseek-v4-flash", label: "deepseek-v4-flash" },
{ model: "openrouter/z-ai/glm-5.1", label: "glm-5.1" },
{ model: "openrouter/anthropic/claude-opus-4.8", label: "claude-opus-4.8" },
]

export function enabled(): boolean {
return process.env["ALTIMATE_ROUTER"] === "1"
}

/** Ladder from `ALTIMATE_ROUTER_LADDER` (comma-separated provider/model ids) or the default. */
export function ladder(): Tier[] {
const env = process.env["ALTIMATE_ROUTER_LADDER"]
if (!env) return DEFAULT_LADDER
const tiers = env
.split(",")
.map((s) => s.trim())
.filter(Boolean)
.map((model) => ({ model, label: model.split("/").pop() || model }))
return tiers.length ? tiers : DEFAULT_LADDER
}

/** Escalate iff the verdict failed AND a stronger tier remains. */
export function shouldEscalate(verdict: Verifier.Verdict, tierIndex: number, tiers: Tier[]): boolean {
return !verdict.ok && tierIndex < tiers.length - 1
}

/** The note handed to the next tier — names the exact failing checks so it fixes them. */
export function escalationContext(prev: Tier, verdict: Verifier.Verdict): string {
const failing = verdict.checks.filter((c) => !c.ok).map((c) => c.name)
const lines = [
`A previous attempt (by ${prev.label}) did not pass verification.`,
verdict.reason ? `Verifier reason: ${verdict.reason}` : "",
failing.length ? `Failing checks to fix: ${failing.join(", ")}.` : "",
`The prior changes are in the workspace — fix these specific failures; do not start over.`,
]
return lines.filter(Boolean).join("\n")
}

export interface Attempt {
tier: Tier
verdict: Verifier.Verdict
}

export interface RouteResult {
solved: boolean
solvedBy?: Tier
attempts: Attempt[]
}

/**
* Drive the ladder: run each tier, verify, escalate on failure with context,
* stop at the first ok verdict. `runAgent(model, escalationNote?)` performs the
* agent run in the shared workspace; `verify()` judges the post-run workspace.
*/
export async function route(params: {
tiers?: Tier[]
runAgent: (model: string, escalationNote?: string) => Promise<void>
verify: () => Promise<Verifier.Verdict>
}): Promise<RouteResult> {
const tiers = params.tiers ?? ladder()
const attempts: Attempt[] = []
let note: string | undefined
for (let i = 0; i < tiers.length; i++) {
const tier = tiers[i]
// A thrown agent/verify error is treated as a failed attempt so the ladder can
// escalate, rather than aborting the whole run on a transient failure in one tier.
let verdict: Verifier.Verdict
try {
await params.runAgent(tier.model, note)
verdict = await params.verify()
} catch (e) {
verdict = { ok: false, reason: `tier error: ${String(e)}`, checks: [] }
}
attempts.push({ tier, verdict })
if (verdict.ok) return { solved: true, solvedBy: tier, attempts }
if (!shouldEscalate(verdict, i, tiers)) break
note = escalationContext(tier, verdict)
}
return { solved: false, attempts }
}
}
Loading
Loading