diff --git a/packages/opencode/src/altimate/auto/routing-signals.ts b/packages/opencode/src/altimate/auto/routing-signals.ts new file mode 100644 index 0000000000..145c691798 --- /dev/null +++ b/packages/opencode/src/altimate/auto/routing-signals.ts @@ -0,0 +1,109 @@ +// Auto Mode client routing signals. +// +// The altimate-backend gateway reads `extra_body.routing` and escalates one tier +// on error recovery. The signals are schema-free hints derived from the message +// history the request already carries; any datum that cannot be determined is +// omitted so the backend falls back to its own heuristics. +// +// Transport: @ai-sdk/openai-compatible spreads `providerOptions[]` +// into the top-level POST body (minus its own typed keys), so +// `providerOptions["altimate-backend"].extra_body = { routing }` is sent as a +// literal top-level `extra_body` field. +import type { ModelMessage } from "ai" + +export namespace RoutingSignals { + export const PROVIDER_ID = "altimate-backend" + const TIERS = ["auto", "fast", "max"] as const + const ROLES = ["plan", "build"] as const + // Only the tail of the conversation reflects the current state of the turn. + const RECENT_MESSAGES = 6 + const ERROR_PATTERN = /\b(error|exception|traceback|stack trace|failed|failure|panic)\b/i + const CODE_FENCE = /```/ + const CODE_TOOLS = new Set(["read", "edit", "write", "patch", "multiedit", "apply_patch"]) + + export type Routing = { + tier?: (typeof TIERS)[number] + last_tool_ok?: boolean + retry_count?: number + has_error_ctx?: boolean + has_code_ctx?: boolean + agent_role?: (typeof ROLES)[number] + } + + type ToolResult = { toolName?: string; output?: { type?: string; value?: unknown } } + + export type Input = { + providerID: string + modelID: string + agentName?: string + messages: ModelMessage[] + } + + function tierOf(modelID: string): Routing["tier"] { + const tier = modelID.startsWith("altimate-") ? modelID.slice("altimate-".length) : undefined + return TIERS.find((t) => t === tier) + } + + function toolResults(msg: ModelMessage): ToolResult[] { + if (msg.role !== "tool" || !Array.isArray(msg.content)) return [] + return (msg.content as unknown as Array<{ type?: string }>).filter((p) => p?.type === "tool-result") as ToolResult[] + } + + const failed = (r: ToolResult) => r.output?.type === "error-text" || r.output?.type === "error-json" + + function textOf(msg: ModelMessage): string { + if (typeof msg.content === "string") return msg.content + if (!Array.isArray(msg.content)) return "" + return (msg.content as unknown as Array>) + .map((p) => { + if (typeof p?.text === "string") return p.text + const v = p?.output?.value + return typeof v === "string" ? v : "" + }) + .join("\n") + } + + export function compute(input: Input): Routing | undefined { + if (input.providerID !== PROVIDER_ID) return undefined + const routing: Routing = {} + const tier = tierOf(input.modelID) + if (tier) routing.tier = tier + const role = ROLES.find((r) => r === input.agentName) + if (role) routing.agent_role = role + + const results = input.messages.flatMap(toolResults) + const last = results[results.length - 1] + if (last) { + routing.last_tool_ok = !failed(last) + let streak = 0 + for (let i = results.length - 1; i >= 0 && failed(results[i]); i--) streak++ + routing.retry_count = streak + } + + const recent = input.messages.slice(-RECENT_MESSAGES) + if (recent.length > 0) { + routing.has_error_ctx = recent.some( + (m) => toolResults(m).some(failed) || (m.role === "tool" && ERROR_PATTERN.test(textOf(m))), + ) + routing.has_code_ctx = recent.some( + (m) => CODE_FENCE.test(textOf(m)) || toolResults(m).some((r) => CODE_TOOLS.has(String(r.toolName))), + ) + } + return routing + } + + /** Merge routing signals into the provider options for altimate-backend; identity for other providers. */ + export function attach(options: Record, input: Input): Record { + try { + const routing = compute(input) + if (!routing || Object.keys(routing).length === 0) return options + const existing = options[PROVIDER_ID] ?? {} + return { + ...options, + [PROVIDER_ID]: { ...existing, extra_body: { ...existing.extra_body, routing } }, + } + } catch { + return options + } + } +} diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 62804161a3..bca730aec3 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -24,6 +24,9 @@ import { // altimate_change end // altimate_change start — tool retrieval import { Retrieval } from "@/tool/retrieval" +// altimate_change start — Auto Mode client routing signals +import { RoutingSignals } from "@/altimate/auto/routing-signals" +// altimate_change end // altimate_change end import { Config } from "@/config/config" import { Instance } from "@/project/instance" @@ -312,7 +315,12 @@ export namespace LLM { topP: params.topP, topK: params.topK, // altimate_change start — use the reasoning options reconciled with the final output reservation - providerOptions: ProviderTransform.providerOptions(input.model, requestOptions), + providerOptions: RoutingSignals.attach(ProviderTransform.providerOptions(input.model, requestOptions), { + providerID: input.model.providerID, + modelID: input.model.id, + agentName: input.agent.name, + messages: input.messages, + }), // altimate_change end activeTools: Object.keys(tools).filter((x) => x !== "invalid"), tools, diff --git a/packages/opencode/test/altimate/auto-routing-signals.test.ts b/packages/opencode/test/altimate/auto-routing-signals.test.ts new file mode 100644 index 0000000000..9ecfa8bb74 --- /dev/null +++ b/packages/opencode/test/altimate/auto-routing-signals.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from "bun:test" +import type { ModelMessage } from "ai" +import { RoutingSignals } from "../../src/altimate/auto/routing-signals" + +const toolMsg = (toolName: string, type: string, value: string): ModelMessage => + ({ + role: "tool", + content: [{ type: "tool-result", toolCallId: "c", toolName, output: { type, value } }], + }) as unknown as ModelMessage + +const base = { providerID: "altimate-backend", modelID: "altimate-auto", agentName: "build" } + +describe("RoutingSignals", () => { + test("is a no-op for other providers", () => { + const opts = { openai: { a: 1 } } + expect(RoutingSignals.compute({ ...base, providerID: "openai", messages: [] })).toBeUndefined() + expect(RoutingSignals.attach(opts, { ...base, providerID: "openai", messages: [] })).toBe(opts) + }) + + test("omits tool signals when no tool result exists", () => { + const r = RoutingSignals.compute({ ...base, messages: [{ role: "user", content: "hi" }] })! + expect(r).toEqual({ tier: "auto", agent_role: "build", has_error_ctx: false, has_code_ctx: false }) + }) + + test("counts consecutive trailing tool failures", () => { + const r = RoutingSignals.compute({ + ...base, + messages: [toolMsg("bash", "text", "ok"), toolMsg("bash", "error-text", "boom"), toolMsg("bash", "error-text", "boom")], + })! + expect(r.last_tool_ok).toBe(false) + expect(r.retry_count).toBe(2) + expect(r.has_error_ctx).toBe(true) + }) + + test("success resets streak; code context from edit tool", () => { + const r = RoutingSignals.compute({ + ...base, + modelID: "altimate-max", + agentName: "plan", + messages: [toolMsg("bash", "error-text", "x"), toolMsg("edit", "text", "done")], + })! + expect(r).toMatchObject({ tier: "max", agent_role: "plan", last_tool_ok: true, retry_count: 0, has_code_ctx: true }) + }) + + test("ignores unknown tiers and roles", () => { + const r = RoutingSignals.compute({ ...base, modelID: "altimate-default", agentName: "explore", messages: [] })! + expect(r).toEqual({}) + }) + + test("attach nests routing under extra_body and preserves existing options", () => { + const out = RoutingSignals.attach( + { "altimate-backend": { extra_body: { foo: 1 }, keep: true } }, + { ...base, messages: [{ role: "user", content: "hi" }] }, + ) + expect(out["altimate-backend"].keep).toBe(true) + expect(out["altimate-backend"].extra_body.foo).toBe(1) + expect(out["altimate-backend"].extra_body.routing.tier).toBe("auto") + }) +})