Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
47 changes: 37 additions & 10 deletions packages/core/src/config/plugin/model-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,12 @@ function configuredSchedules(entries: readonly Entry[]) {
.flatMap((entry) => entry.info.experimental?.model_route_schedules ?? [])
}

/** A named window that cannot be applied is dropped, but never silently (#734): a typo'd or
* unreadable window name would otherwise remove a target's eligibility gate or a schedule rule
* with no signal anywhere. The warning names the window and where it was referenced. */
const warnDroppedWindow = (name: string, site: string, reason: string) =>
console.warn(`model-route window ${JSON.stringify(name)} ${reason} (${site}); it is dropped, not applied`)

/** The named windows defined in config, parsed once so schedules and target gates can reference
* them by name. An entry with an unreadable window is dropped rather than applied. */
export function configuredWindows(entries: readonly Entry[]) {
Expand All @@ -180,30 +186,43 @@ export function configuredWindows(entries: readonly Entry[]) {
.forEach((entry) => {
Object.entries(entry.info.experimental?.model_route_windows ?? {}).forEach(([name, window]) => {
const parsed = ModelRouteSchedule.parse(window)
if (parsed) windows.set(name, parsed)
if (parsed) {
windows.set(name, parsed)
return
}
warnDroppedWindow(name, "experimental.model_route_windows", "is defined but cannot be parsed")
})
})
return windows
}

/** A schedule's window: inline (parsed as before), or the name of a configured window.
* An unknown name resolves to undefined, which drops the rule. */
* An unknown name resolves to undefined, which drops the rule — with a warning naming it. */
const resolveWindow = (
when: ConfigModelRoutes.Schedule["when"] | ConfigModelRoutes.GlobalSchedule["when"],
windows: ReadonlyMap<string, ModelRouteSchedule.Window>,
): ModelRouteSchedule.Window | undefined =>
typeof when === "string" ? windows.get(when) : ModelRouteSchedule.parse(when)
site: string,
): ModelRouteSchedule.Window | undefined => {
if (typeof when !== "string") return ModelRouteSchedule.parse(when)
const window = windows.get(when)
if (!window) warnDroppedWindow(when, site, "is not a defined window")
return window
}

/** A target's eligibility gate, resolved from its `window` against the configured windows.
* An unknown name drops the gate, leaving the target always eligible. */
* An unknown name drops the gate, leaving the target always eligible — with a warning naming it. */
const gateFor = (
window: string | { name: string; invert?: boolean } | undefined,
windows: ReadonlyMap<string, ModelRouteSchedule.Window>,
site: string,
): ModelRoute.Gate | undefined => {
if (window === undefined) return undefined
const name = typeof window === "string" ? window : window.name
const parsed = windows.get(name)
if (!parsed) return undefined
if (!parsed) {
warnDroppedWindow(name, site, "is not a defined window")
return undefined
}
return { name, window: parsed, ...(typeof window !== "string" && window.invert ? { invert: true } : {}) }
}

Expand All @@ -215,7 +234,11 @@ export function routeWeightRules(
): ModelRoute.WeightRule[] {
return (route.schedules ?? []).flatMap((schedule) => {
if (schedule.factor === undefined && schedule.weight === undefined) return []
const window = resolveWindow(schedule.when, windows)
const window = resolveWindow(
schedule.when,
windows,
schedule.id ? `route schedule ${JSON.stringify(schedule.id)}` : "a route schedule",
)
if (!window) return []
const targets = (schedule.targets ?? []).map((target) => target.trim()).filter(Boolean)
return [
Expand All @@ -239,7 +262,11 @@ export function weightRulesAcross(
): ModelRoute.WeightRule[] {
return schedules.flatMap((schedule) => {
if (schedule.factor === undefined && schedule.weight === undefined) return []
const window = resolveWindow(schedule.when, windows)
const window = resolveWindow(
schedule.when,
windows,
schedule.id ? `global schedule ${JSON.stringify(schedule.id)}` : "a global schedule",
)
if (!window) return []
const targets = (schedule.targets ?? []).map((target) => target.trim()).filter(Boolean)
const providers = (schedule.providers ?? []).map((provider) => provider.trim()).filter(Boolean)
Expand Down Expand Up @@ -312,7 +339,7 @@ export function expand(
if (ref.providerID === ModelRoute.PROVIDER_ID) {
const child = visit(ref.id, ref.variant ?? inheritedVariant, nextTrail)
if (child !== undefined) {
const gate = gateFor("window" in spec ? spec.window : undefined, windows)
const gate = gateFor("window" in spec ? spec.window : undefined, windows, `route ${JSON.stringify(routeID)}`)
attach({ node: child, ...(gate ? { gate } : {}) }, `${ModelRoute.PROVIDER_ID}/${ref.id}`)
}
continue
Expand Down Expand Up @@ -341,7 +368,7 @@ export function expand(
const key = `${resolved.providerID}/${resolved.id}${resolved.variant ? `#${resolved.variant}` : ""}`
if (attached.has(key)) continue
attached.add(key)
const gate = gateFor("window" in spec ? spec.window : undefined, windows)
const gate = gateFor("window" in spec ? spec.window : undefined, windows, `route ${JSON.stringify(routeID)}`)
const leafKey = `${resolved.providerID}/${resolved.id}`
const existing = leafByKey.get(key)
if (existing !== undefined) {
Expand Down
71 changes: 70 additions & 1 deletion packages/core/test/model-routes-expand.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { describe, expect, test } from "bun:test"
import { expand, routeVariants, routeWeightRules, weightRulesAcross } from "@opencode/core/config/plugin/model-routes"
import {
configuredWindows,
expand,
routeVariants,
routeWeightRules,
weightRulesAcross,
} from "@opencode/core/config/plugin/model-routes"
import type { Entry } from "@opencode/schema/config"
import { Model } from "@opencode/core/model"
import { Provider } from "@opencode/core/provider"
import type { ConfigModelRoutes } from "@opencode/schema/config/model-routes"
Expand Down Expand Up @@ -279,6 +286,21 @@ describe("pool weight schedules", () => {
})
})

/** Runs `run` with console.warn captured, returning the warning messages (#734). */
const warnings = (run: () => void) => {
const original = console.warn
const messages: string[] = []
console.warn = (...args: unknown[]) => {
messages.push(args.map(String).join(" "))
}
try {
run()
} finally {
console.warn = original
}
return messages
}

describe("named windows", () => {
const windows = new Map([["peak", { daysOfWeek: [1, 5], start: "09:00", end: "17:00" }]])

Expand Down Expand Up @@ -365,6 +387,53 @@ describe("named windows", () => {
expect(nodes[0].children[0].gate).toBeUndefined()
})

test("an unknown window name in a schedule or gate warns, naming the window and the site (#734)", () => {
const scheduleWarnings = warnings(() =>
routeWeightRules(
{
targets: [],
schedules: [{ id: "peak-rule", targets: ["*"], when: "nope", factor: 3 }],
} as unknown as ConfigModelRoutes.Route,
windows,
),
)
expect(scheduleWarnings).toHaveLength(1)
expect(scheduleWarnings[0]).toContain('"nope"')
expect(scheduleWarnings[0]).toContain("peak-rule")

const gateWarnings = warnings(() =>
expand("r", route([{ model: "anthropic/claude-opus-4-1", window: "nope" }]), definitions, windows),
)
expect(gateWarnings).toHaveLength(1)
expect(gateWarnings[0]).toContain('"nope"')
expect(gateWarnings[0]).toContain('"r"')
})

test("configuredWindows drops a defined-but-unparseable window with a warning naming it (#734)", () => {
const entries = [
{
type: "document",
info: {
experimental: {
model_route_windows: {
bad: { start: "25:99", end: "17:00" },
peak: { daysOfWeek: [1, 5], start: "09:00", end: "17:00" },
},
},
},
},
] as unknown as Entry[]
let parsed: ReturnType<typeof configuredWindows> | undefined
const messages = warnings(() => {
parsed = configuredWindows(entries)
})
expect(parsed?.has("bad")).toBe(false)
expect(parsed?.has("peak")).toBe(true)
expect(messages).toHaveLength(1)
expect(messages[0]).toContain('"bad"')
expect(messages[0]).toContain("model_route_windows")
})

test("a route referenced twice by one parent keeps both group nodes, sharing leaf indexes", () => {
const routes = new Map<string, ConfigModelRoutes.Route>([
[
Expand Down
Loading