From ca001a4f1b859179cec9cb63426124272c17b88f Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:08:35 +0200 Subject: [PATCH 1/6] =?UTF-8?q?feat(routing):=20add=20cost-aware=20policy?= =?UTF-8?q?=20scoring=20and=20limits=20(RI-08)=20=E2=80=94=20synced=20onto?= =?UTF-8?q?=20latest=20dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/router.ts | 6 ++ src/routing/cost.ts | 87 ++++++++++++++++++++ src/routing/evaluator.ts | 24 +++++- tests/cost-scoring.test.ts | 163 +++++++++++++++++++++++++++++++++++++ 4 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 src/routing/cost.ts create mode 100644 tests/cost-scoring.test.ts diff --git a/src/router.ts b/src/router.ts index 873271d40..599ec9bb9 100644 --- a/src/router.ts +++ b/src/router.ts @@ -35,6 +35,7 @@ import { evaluatePolicyProfile, type PolicyRequestEvidence } from "./routing/eva import { candidateCapabilityEvidence } from "./routing/capability"; import { policyCandidateHealthEvidence } from "./routing/health"; import { quotaEvidenceForCandidate } from "./routing/quota"; +import { costEvidenceForCandidate } from "./routing/cost"; export class NoEligiblePolicyCandidateError extends Error { /** Evaluation trace (with per-candidate exclusions) when nothing qualified. */ @@ -531,6 +532,11 @@ function routeModelInternal( ? getAccountSet("anthropic")?.activeAccountId : undefined, }), + cost: costEvidenceForCandidate({ + provider: candidate.provider, + model: candidate.model, + limitUsd: profile.limits.maxEstimatedCostUsd, + }), })); const evaluation = evaluatePolicyProfile(config, policyId, policyEvidence ?? {}, candidateEvidence, now); if (evaluation.selectedIndex === null) { diff --git a/src/routing/cost.ts b/src/routing/cost.ts new file mode 100644 index 000000000..70f07d788 --- /dev/null +++ b/src/routing/cost.ts @@ -0,0 +1,87 @@ +/** + * Cost-aware policy scoring and limits (RI-08). + * + * Reuses the canonical price/cost normalization (`src/usage/cost.ts`). + * Evidence distinguishes estimated vs authoritative usage and registry vs + * expected prices. Unknown prices stay unknown (never free); the profile's + * `unknownEvidence.cost` policy decides how unknown evidence is handled. + * + * No billing, invoicing, or hidden budgets - a hard per-request ceiling + * (`limits.maxEstimatedCostUsd`) is evaluated deterministically. + */ + +import type { OcxUsage, OcxProviderConfig } from "../types"; +import type { UsageStatus } from "../usage/log"; +import { estimateRequestCost, type ServiceTierInput } from "../usage/cost"; +import type { RouteCostEvidence } from "./trace"; + +/** Reference cost (USD) for the relative cost score when no limit is set. */ +export const COST_SCORE_REFERENCE_USD = 1.0; + +export interface CostEvidenceInput { + provider: string; + model: string; + usage?: OcxUsage; + usageStatus?: UsageStatus; + serviceTier?: ServiceTierInput; + limitUsd?: number; + providerConfig?: OcxProviderConfig; +} + +/** + * Assemble cost evidence from the canonical price model. Returns unknown-ish + * evidence (`incomplete: true`, no estimate) when usage or a price is + * missing - never a fabricated zero. + */ +export function costEvidenceForCandidate(input: CostEvidenceInput): RouteCostEvidence { + const limitUsd = input.limitUsd; + if (!input.usage) { + return { + ...(limitUsd !== undefined ? { limitUsd } : {}), + incomplete: true, + }; + } + const tier = input.serviceTier + ?? (input.providerConfig + ? { + requestedServiceTier: input.providerConfig.supportsServiceTier === true ? "priority" : undefined, + } + : undefined); + const estimate = estimateRequestCost({ + provider: input.provider, + model: input.model, + usage: input.usage, + usageStatus: input.usageStatus ?? "estimated", + ...(tier ? { serviceTier: tier } : {}), + }); + if (!estimate) { + return { + ...(limitUsd !== undefined ? { limitUsd } : {}), + incomplete: true, + priceSource: "unmatched", + }; + } + const priceSource = estimate.price?.source ?? "registry"; + return { + estimatedUsd: estimate.cost.total, + priceSource, + incomplete: estimate.estimated || priceSource === "expected", + ...(limitUsd !== undefined ? { limitUsd } : {}), + ...(limitUsd !== undefined + ? { excludedByLimit: estimate.cost.total > limitUsd } + : {}), + }; +} + +/** + * Deterministic cost score in [0,1]: cheaper is better, relative to + * `COST_SCORE_REFERENCE_USD` (or the profile limit when set). Unknown + * estimates return null so the profile's unknownEvidence policy applies. + */ +export function costScore(evidence: RouteCostEvidence | undefined): number | null { + if (!evidence?.estimatedUsd || !Number.isFinite(evidence.estimatedUsd)) return null; + const reference = typeof evidence.limitUsd === "number" && evidence.limitUsd > 0 + ? evidence.limitUsd + : COST_SCORE_REFERENCE_USD; + return Math.max(0, Math.min(1, 1 - evidence.estimatedUsd / reference)); +} diff --git a/src/routing/evaluator.ts b/src/routing/evaluator.ts index 6f427713d..c1b3796d9 100644 --- a/src/routing/evaluator.ts +++ b/src/routing/evaluator.ts @@ -23,6 +23,7 @@ import { import { getRoutingProfile, policyModelId, type NormalizedRoutingProfile } from "./profile"; import { healthScore } from "./health"; import { quotaScore } from "./quota"; +import { costScore } from "./cost"; /** Unknown health under "penalize": a low-but-not-zero deterministic floor. */ export const HEALTH_UNKNOWN_PENALTY_SCORE = 0.3; @@ -30,6 +31,8 @@ export const HEALTH_UNKNOWN_PENALTY_SCORE = 0.3; export const HEALTH_UNKNOWN_NEUTRAL_SCORE = 0.5; /** Unknown quota under "penalize": deterministic low floor. */ export const QUOTA_UNKNOWN_PENALTY_SCORE = 0.3; +/** Unknown cost under "penalize": deterministic low floor. */ +export const COST_UNKNOWN_PENALTY_SCORE = 0.3; export interface PolicyRequestEvidence { /** Required context window for this request (tokens). */ @@ -342,17 +345,30 @@ export function evaluatePolicyProfile( } } + // Cost scoring (RI-08): the hard per-request ceiling was already checked + // above; unknown cost follows the profile's unknownEvidence policy. + const cost = evidence.cost; + let costValue = cost ? costScore(cost) : null; + if (costValue === null && profile.unknownEvidence.cost === "exclude") { + exclusions.push({ code: "unknown-price" }); + eligible = false; + } else if (costValue === null && profile.unknownEvidence.cost === "penalize") { + costValue = COST_UNKNOWN_PENALTY_SCORE; + } + const priorityScore = configuredPriorityScore(index, profile.candidates.length); const healthWeight = profile.optimize.health; const quotaWeight = profile.optimize.quota; + const costWeight = profile.optimize.cost; // Only spend a dimension's weight when a value is actually present: - // "allow" leaves missing health/quota components null, so subtracting + // "allow" leaves missing health/quota/cost components null, so subtracting // their weights would shrink the priority share for evidence the profile // explicitly permits to be absent. Renormalize those weights back into // priority instead of silently changing the ranking semantics. const spentHealth = healthValue !== null ? healthWeight : 0; const spentQuota = quotaValue !== null ? quotaWeight : 0; - const priorityWeight = Math.max(0, 1 - spentHealth - spentQuota); + const spentCost = costValue !== null ? costWeight : 0; + const priorityWeight = Math.max(0, 1 - spentHealth - spentQuota - spentCost); const components: RouteScoreEvidence["components"] = { configuredPriority: priorityScore }; let total = priorityWeight * priorityScore; if (healthWeight > 0 && healthValue !== null) { @@ -363,6 +379,10 @@ export function evaluatePolicyProfile( total += quotaWeight * quotaValue; components.quota = quotaValue; } + if (costWeight > 0 && costValue !== null) { + total += costWeight * costValue; + components.cost = costValue; + } const score: RouteScoreEvidence = { total, components }; const evaluated: PolicyEvaluationCandidate = { provider: evidence.provider, diff --git a/tests/cost-scoring.test.ts b/tests/cost-scoring.test.ts new file mode 100644 index 000000000..c3a23ddb4 --- /dev/null +++ b/tests/cost-scoring.test.ts @@ -0,0 +1,163 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { costEvidenceForCandidate, costScore } from "../src/routing/cost"; +import { evaluatePolicyProfile, COST_UNKNOWN_PENALTY_SCORE } from "../src/routing/evaluator"; +import type { OcxConfig } from "../src/types"; + +let testDir = ""; +let previousHome: string | undefined; + +beforeEach(() => { + previousHome = process.env.OPENCODEX_HOME; + testDir = mkdtempSync(join(tmpdir(), "ocx-cost-")); + process.env.OPENCODEX_HOME = testDir; +}); + +afterEach(() => { + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousHome; + if (testDir) rmSync(testDir, { recursive: true, force: true }); +}); + +function config(overrides: Partial = {}): OcxConfig { + return { + port: 10100, + defaultProvider: "a", + providers: { + a: { adapter: "openai-chat", baseUrl: "https://a.example/v1", apiKey: "ka", models: ["m1", "m2"] }, + anthropic: { adapter: "anthropic", baseUrl: "https://api.anthropic.com/v1", apiKey: "kan", models: ["claude-opus-5", "claude-sonnet-5"] }, + }, + routingProfiles: { + cost: { + candidates: [ + { provider: "anthropic", model: "claude-opus-5" }, + { provider: "anthropic", model: "claude-sonnet-5" }, + ], + optimize: { cost: 0.8 }, + }, + }, + ...overrides, + }; +} + +const USAGE = { inputTokens: 1000, outputTokens: 100, estimated: true }; + +describe("cost-aware scoring (RI-08)", () => { + test("known price produces an estimate with provenance", async () => { + const evidence = costEvidenceForCandidate({ + provider: "anthropic", + model: "claude-opus-5", + usage: { inputTokens: 1000, outputTokens: 100 }, + usageStatus: "reported", + }); + expect(evidence.estimatedUsd).toBeGreaterThan(0); + // Canonical jawcode pricing is authoritative; the expected-price overlay + // is the fallback. Both carry a stable source code. + expect(["jawcode", "expected"]).toContain(evidence.priceSource); + expect(evidence.incomplete).toBe(evidence.priceSource === "expected"); + expect(evidence.excludedByLimit).toBeUndefined(); + expect(costScore(evidence)).not.toBeNull(); + }); + + test("unknown price and missing usage stay unknown - never zero", async () => { + const noPrice = costEvidenceForCandidate({ provider: "a", model: "m1", usage: USAGE }); + expect(noPrice.estimatedUsd).toBeUndefined(); + expect(noPrice.priceSource).toBe("unmatched"); + expect(noPrice.incomplete).toBe(true); + expect(costScore(noPrice)).toBeNull(); + + const noUsage = costEvidenceForCandidate({ provider: "anthropic", model: "claude-opus-5" }); + expect(noUsage.estimatedUsd).toBeUndefined(); + expect(noUsage.incomplete).toBe(true); + expect(costScore(noUsage)).toBeNull(); + }); + + test("hard maximum estimated cost excludes candidates and records the limit", async () => { + const limited = config({ + routingProfiles: { + cost: { + candidates: [{ provider: "anthropic", model: "claude-opus-5" }], + optimize: { cost: 0.8 }, + limits: { maxEstimatedCostUsd: 0.000001 }, + unknownEvidence: { capability: "allow", health: "penalize", quota: "penalize", cost: "penalize" }, + }, + }, + }); + const evidence = costEvidenceForCandidate({ + provider: "anthropic", + model: "claude-opus-5", + usage: USAGE, + limitUsd: 0.000001, + }); + expect(evidence.limitUsd).toBe(0.000001); + expect(evidence.excludedByLimit).toBe(true); + + const result = evaluatePolicyProfile(limited, "cost", {}, [ + { provider: "anthropic", model: "claude-opus-5", capability: { contextWindow: 200000 }, cost: evidence }, + ]); + expect(result.candidates[0]!.eligible).toBe(false); + expect(result.candidates[0]!.exclusions.some(exclusion => exclusion.code === "cost-limit")).toBe(true); + expect(result.selectedIndex).toBeNull(); + }); + + test("unknown cost follows the profile policy (exclude / penalize / allow)", async () => { + const strict = config({ + routingProfiles: { + c: { + candidates: [{ provider: "a", model: "m1" }], + unknownEvidence: { capability: "allow", health: "penalize", quota: "penalize", cost: "exclude" }, + }, + }, + }); + const excluded = evaluatePolicyProfile(strict, "c", {}, [ + { provider: "a", model: "m1", capability: { contextWindow: 200000 } }, + ]); + expect(excluded.candidates[0]!.eligible).toBe(false); + expect(excluded.candidates[0]!.exclusions.some(exclusion => exclusion.code === "unknown-price")).toBe(true); + + const penalizing = config({ + routingProfiles: { + c: { + candidates: [{ provider: "a", model: "m1" }], + unknownEvidence: { capability: "allow", health: "penalize", quota: "penalize", cost: "penalize" }, + }, + }, + }); + const penalized = evaluatePolicyProfile(penalizing, "c", {}, [ + { provider: "a", model: "m1", capability: { contextWindow: 200000 } }, + ]); + expect(penalized.candidates[0]!.eligible).toBe(true); + expect(penalized.candidates[0]!.score!.components.cost).toBe(COST_UNKNOWN_PENALTY_SCORE); + }); + + test("cheaper candidates win when cost is weighted", async () => { + const expensive = costEvidenceForCandidate({ + provider: "anthropic", + model: "claude-opus-5", + usage: { inputTokens: 200_000, outputTokens: 20_000, estimated: true }, + }); + const cheap = costEvidenceForCandidate({ + provider: "anthropic", + model: "claude-sonnet-5", + usage: { inputTokens: 2_000, outputTokens: 200, estimated: true }, + }); + const result = evaluatePolicyProfile(config(), "cost", {}, [ + { provider: "anthropic", model: "claude-opus-5", capability: { contextWindow: 200000 }, cost: expensive }, + { provider: "anthropic", model: "claude-sonnet-5", capability: { contextWindow: 200000 }, cost: cheap }, + ]); + expect(result.selectedIndex).toBe(1); + expect(result.candidates[1]!.score!.components.cost).toBeGreaterThan(result.candidates[0]!.score!.components.cost!); + }); + + test("trace carries cost evidence and the cost component", async () => { + const evidence = costEvidenceForCandidate({ provider: "anthropic", model: "claude-opus-5", usage: USAGE }); + const result = evaluatePolicyProfile(config(), "cost", {}, [ + { provider: "anthropic", model: "claude-opus-5", capability: { contextWindow: 200000 }, cost: evidence }, + { provider: "anthropic", model: "claude-sonnet-5", capability: { contextWindow: 200000 } }, + ]); + expect(result.trace.candidates[0]!.cost).toBeDefined(); + expect(result.trace.candidates[0]!.score!.components.cost).toBeDefined(); + }); +}); From d085779a3fdad3c6976be12f3300997c57e10574 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:30:08 +0200 Subject: [PATCH 2/6] test(routing): fold RI-08 unknown-cost penalty into score assertions; docs: cost is a score dimension --- .../content/docs/reference/configuration/routing.md | 12 ++++++------ tests/policy-execution.test.ts | 8 ++++---- tests/routing-profile.test.ts | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs-site/src/content/docs/reference/configuration/routing.md b/docs-site/src/content/docs/reference/configuration/routing.md index 318edd275..8017756b6 100644 --- a/docs-site/src/content/docs/reference/configuration/routing.md +++ b/docs-site/src/content/docs/reference/configuration/routing.md @@ -104,7 +104,7 @@ namespace, or reserved bare native families (`gpt-*`, `o1-*`, `o3-*`, `o4-*`, `c | `candidates` | `{ provider: string; model: string }[]` | required | Explicit allowlist of `provider/model` refs. No implicit expansion. | | `alias?` | `string` | — | Optional public model id in place of `policy/`. | | `require?` | object | `{}` | Hard capability requirements evaluated before scoring (see below). | -| `optimize?` | object | latency 0.55, health 0.25, cost 0.10, quota 0.10 | Scoring weights, normalized deterministically. Only `health` and `quota` have score dimensions; the configured-priority share is `1 - health - quota` (default 0.65), and `latency`/`cost` fold into that priority share rather than scoring independently. | +| `optimize?` | object | latency 0.55, health 0.25, cost 0.10, quota 0.10 | Scoring weights, normalized deterministically. `health`, `quota`, and `cost` have score dimensions; the configured-priority share is `1 - health - quota - cost` (default 0.65), and `latency` folds into that priority share rather than scoring independently. | | `limits?` | object | — | Hard limits, e.g. `maxEstimatedCostUsd` (enforced by the dry-run evaluator when candidate cost evidence is known). | | `unknownEvidence?` | object | capability `exclude`, health/quota/cost `penalize` | How unknown evidence is treated per dimension: `allow`, `penalize`, or `exclude`. Unknown never becomes zero. | @@ -171,11 +171,11 @@ candidate evidence: `candidates[].codexAccountId` (Codex pool, provider `openai` requirements filter first, then deterministic scoring ranks the survivors. Both are virtual namespaces with aliases and collision validation; they differ in *how* a candidate -is chosen. Profile scoring combines the configured-priority component with the health (RI-06) and -quota (RI-07) score dimensions where evidence is present; the `latency` and `cost` weights fold into -the priority share rather than scoring independently, and cost is also enforced through the -`limits.maxEstimatedCostUsd` cap. Per-request route-decision traces are recorded when a policy -profile executes. +is chosen. Profile scoring combines the configured-priority component with the health (RI-06), +quota (RI-07), and cost (RI-08) score dimensions where evidence is present; the `latency` weight +folds into the priority share rather than scoring independently. Cost is also enforced through the +`limits.maxEstimatedCostUsd` cap (a candidate whose estimated cost exceeds the cap is excluded). +Per-request route-decision traces are recorded when a policy profile executes. ### Catalog eligibility diff --git a/tests/policy-execution.test.ts b/tests/policy-execution.test.ts index 30386dd7d..b416b0285 100644 --- a/tests/policy-execution.test.ts +++ b/tests/policy-execution.test.ts @@ -99,11 +99,11 @@ describe("policy execution (RI-05)", () => { expect(trace.candidates[1]!.exclusions[0]!.code).toBe("capability-unsatisfied"); expect(trace.selected.provider).toBe("a"); expect(trace.selected.model).toBe("m1"); - // RI-06: unknown health under the default "penalize" policy folds a - // penalized health floor into the score. + // RI-06/07/08: unknown health/quota/cost under the default "penalize" + // policy folds penalized floors into the score. expect(trace.candidates[0]!.score).toMatchObject({ - total: 0.755, - components: { configuredPriority: 1, health: 0.3, quota: 0.3 }, + total: 0.685, + components: { configuredPriority: 1, health: 0.3, quota: 0.3, cost: 0.3 }, }); }); diff --git a/tests/routing-profile.test.ts b/tests/routing-profile.test.ts index 148ca4d5f..074aa4e38 100644 --- a/tests/routing-profile.test.ts +++ b/tests/routing-profile.test.ts @@ -318,11 +318,11 @@ describe("routing profiles (RI-04)", () => { { provider: "b", model: "m2", capability: { contextWindow: 5000 } }, ]); expect(result.selectedIndex).toBe(0); - // RI-06: unknown health under the default "penalize" policy folds a - // penalized health floor into the score. + // RI-06/07/08: unknown health/quota/cost under the default "penalize" + // policy folds penalized floors into the score. expect(result.trace.candidates[0]!.score).toMatchObject({ - total: 0.755, - components: { configuredPriority: 1, health: 0.3, quota: 0.3 }, + total: 0.685, + components: { configuredPriority: 1, health: 0.3, quota: 0.3, cost: 0.3 }, }); }); From 18c4c3d161f3626c07494232f17d18fed71ec6cc Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:43:44 +0200 Subject: [PATCH 3/6] refactor(routing): drop unused providerConfig tier-inference from cost evidence (RI-08) --- src/routing/cost.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/routing/cost.ts b/src/routing/cost.ts index 70f07d788..cdf3ea4f0 100644 --- a/src/routing/cost.ts +++ b/src/routing/cost.ts @@ -10,7 +10,7 @@ * (`limits.maxEstimatedCostUsd`) is evaluated deterministically. */ -import type { OcxUsage, OcxProviderConfig } from "../types"; +import type { OcxUsage } from "../types"; import type { UsageStatus } from "../usage/log"; import { estimateRequestCost, type ServiceTierInput } from "../usage/cost"; import type { RouteCostEvidence } from "./trace"; @@ -25,7 +25,6 @@ export interface CostEvidenceInput { usageStatus?: UsageStatus; serviceTier?: ServiceTierInput; limitUsd?: number; - providerConfig?: OcxProviderConfig; } /** @@ -41,18 +40,12 @@ export function costEvidenceForCandidate(input: CostEvidenceInput): RouteCostEvi incomplete: true, }; } - const tier = input.serviceTier - ?? (input.providerConfig - ? { - requestedServiceTier: input.providerConfig.supportsServiceTier === true ? "priority" : undefined, - } - : undefined); const estimate = estimateRequestCost({ provider: input.provider, model: input.model, usage: input.usage, usageStatus: input.usageStatus ?? "estimated", - ...(tier ? { serviceTier: tier } : {}), + ...(input.serviceTier ? { serviceTier: input.serviceTier } : {}), }); if (!estimate) { return { From 2e9a5fe30939bbe840e7a6e17d848c0477758621 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:15:21 +0200 Subject: [PATCH 4/6] fix(routing): address CodeRabbit round on cost scoring (RI-08) --- src/routing/cost.ts | 2 +- tests/cost-scoring.test.ts | 17 +++++++++++++++++ tests/routing-profile.test.ts | 8 ++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/routing/cost.ts b/src/routing/cost.ts index cdf3ea4f0..14352aac2 100644 --- a/src/routing/cost.ts +++ b/src/routing/cost.ts @@ -72,7 +72,7 @@ export function costEvidenceForCandidate(input: CostEvidenceInput): RouteCostEvi * estimates return null so the profile's unknownEvidence policy applies. */ export function costScore(evidence: RouteCostEvidence | undefined): number | null { - if (!evidence?.estimatedUsd || !Number.isFinite(evidence.estimatedUsd)) return null; + if (evidence?.estimatedUsd === undefined || !Number.isFinite(evidence.estimatedUsd)) return null; const reference = typeof evidence.limitUsd === "number" && evidence.limitUsd > 0 ? evidence.limitUsd : COST_SCORE_REFERENCE_USD; diff --git a/tests/cost-scoring.test.ts b/tests/cost-scoring.test.ts index c3a23ddb4..cf614da27 100644 --- a/tests/cost-scoring.test.ts +++ b/tests/cost-scoring.test.ts @@ -130,6 +130,23 @@ describe("cost-aware scoring (RI-08)", () => { ]); expect(penalized.candidates[0]!.eligible).toBe(true); expect(penalized.candidates[0]!.score!.components.cost).toBe(COST_UNKNOWN_PENALTY_SCORE); + + const allowing = config({ + routingProfiles: { + c: { + candidates: [{ provider: "a", model: "m1" }], + unknownEvidence: { capability: "allow", health: "penalize", quota: "penalize", cost: "allow" }, + }, + }, + }); + const allowed = evaluatePolicyProfile(allowing, "c", {}, [ + { provider: "a", model: "m1", capability: { contextWindow: 200000 } }, + ]); + expect(allowed.candidates[0]!.eligible).toBe(true); + // Allowed-unknown cost: no cost component, and the unspent cost weight + // folds back into priority instead of scoring unknown as zero. + expect(allowed.candidates[0]!.score!.components.cost).toBeUndefined(); + expect(allowed.candidates[0]!.score!.total).toBeGreaterThan(0); }); test("cheaper candidates win when cost is weighted", async () => { diff --git a/tests/routing-profile.test.ts b/tests/routing-profile.test.ts index 074aa4e38..53aacdb8e 100644 --- a/tests/routing-profile.test.ts +++ b/tests/routing-profile.test.ts @@ -167,6 +167,14 @@ describe("routing profiles (RI-04)", () => { expect(namespaceIssues.length).toBe(1); expect(providerNamespaceCollision.length).toBe(1); + const accountNamespaceCollision = routingProfileIssues("p", { + candidates: [{ provider: "a", model: "m1" }], + alias: "work/anything", + }, config); + expect(accountNamespaceCollision.some( + issue => issue.message.includes("codex account namespace"), + )).toBe(true); + const siblingCollision = routingProfileIssues("p", { candidates: [{ provider: "a", model: "m1" }], alias: "ocx/fast", From aee44717f9a1100bef4dab825c502514d3b0c3e6 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:32:42 +0200 Subject: [PATCH 5/6] refactor(routing): drop unused excludedByLimit from cost evidence (RI-08 simplify) --- devlog/_plan/260804_router_intelligence/000_master_plan.md | 3 +-- src/routing/cost.ts | 3 --- src/routing/trace.ts | 2 -- tests/cost-scoring.test.ts | 2 -- 4 files changed, 1 insertion(+), 9 deletions(-) diff --git a/devlog/_plan/260804_router_intelligence/000_master_plan.md b/devlog/_plan/260804_router_intelligence/000_master_plan.md index b0461191d..3566f012c 100644 --- a/devlog/_plan/260804_router_intelligence/000_master_plan.md +++ b/devlog/_plan/260804_router_intelligence/000_master_plan.md @@ -194,8 +194,7 @@ Evidence shapes: incompleteStreamRate?, recentLatencyMs?, sampleCount?, recencyWeight? }`. - `quota`: `{ known, headroomTokens?, exhausted?, resetAtMs?, reauthOrCooling?, reservedHeadroomTokens?, source }`. -- `cost`: `{ estimatedUsd?, priceSource?, incomplete?, limitUsd?, - excludedByLimit? }`. +- `cost`: `{ estimatedUsd?, priceSource?, incomplete?, limitUsd? }`. - `score`: `{ total, components: { capability?, health?, quota?, cost?, latency?, configuredPriority? } }`. diff --git a/src/routing/cost.ts b/src/routing/cost.ts index 14352aac2..65675f300 100644 --- a/src/routing/cost.ts +++ b/src/routing/cost.ts @@ -60,9 +60,6 @@ export function costEvidenceForCandidate(input: CostEvidenceInput): RouteCostEvi priceSource, incomplete: estimate.estimated || priceSource === "expected", ...(limitUsd !== undefined ? { limitUsd } : {}), - ...(limitUsd !== undefined - ? { excludedByLimit: estimate.cost.total > limitUsd } - : {}), }; } diff --git a/src/routing/trace.ts b/src/routing/trace.ts index 0b67f2aa6..169400d8d 100644 --- a/src/routing/trace.ts +++ b/src/routing/trace.ts @@ -83,7 +83,6 @@ export interface RouteCostEvidence { priceSource?: string; incomplete?: boolean; limitUsd?: number; - excludedByLimit?: boolean; } export interface RouteScoreEvidence { @@ -533,7 +532,6 @@ function parseCost(raw: unknown, caps: ParseCaps): RouteCostEvidence | undefined } if (typeof raw.incomplete === "boolean") out.incomplete = raw.incomplete; if (finiteNumber(raw.limitUsd)) out.limitUsd = raw.limitUsd; - if (typeof raw.excludedByLimit === "boolean") out.excludedByLimit = raw.excludedByLimit; return Object.keys(out).length > 0 ? out : undefined; } diff --git a/tests/cost-scoring.test.ts b/tests/cost-scoring.test.ts index cf614da27..af5b14721 100644 --- a/tests/cost-scoring.test.ts +++ b/tests/cost-scoring.test.ts @@ -57,7 +57,6 @@ describe("cost-aware scoring (RI-08)", () => { // is the fallback. Both carry a stable source code. expect(["jawcode", "expected"]).toContain(evidence.priceSource); expect(evidence.incomplete).toBe(evidence.priceSource === "expected"); - expect(evidence.excludedByLimit).toBeUndefined(); expect(costScore(evidence)).not.toBeNull(); }); @@ -92,7 +91,6 @@ describe("cost-aware scoring (RI-08)", () => { limitUsd: 0.000001, }); expect(evidence.limitUsd).toBe(0.000001); - expect(evidence.excludedByLimit).toBe(true); const result = evaluatePolicyProfile(limited, "cost", {}, [ { provider: "anthropic", model: "claude-opus-5", capability: { contextWindow: 200000 }, cost: evidence }, From 24e705ee1acb817ce61ef2d45c1ee5b86b0fc585 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:37:26 +0200 Subject: [PATCH 6/6] Update docs-site/src/content/docs/reference/configuration/routing.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- docs-site/src/content/docs/reference/configuration/routing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-site/src/content/docs/reference/configuration/routing.md b/docs-site/src/content/docs/reference/configuration/routing.md index 8017756b6..1c7b143f9 100644 --- a/docs-site/src/content/docs/reference/configuration/routing.md +++ b/docs-site/src/content/docs/reference/configuration/routing.md @@ -104,7 +104,7 @@ namespace, or reserved bare native families (`gpt-*`, `o1-*`, `o3-*`, `o4-*`, `c | `candidates` | `{ provider: string; model: string }[]` | required | Explicit allowlist of `provider/model` refs. No implicit expansion. | | `alias?` | `string` | — | Optional public model id in place of `policy/`. | | `require?` | object | `{}` | Hard capability requirements evaluated before scoring (see below). | -| `optimize?` | object | latency 0.55, health 0.25, cost 0.10, quota 0.10 | Scoring weights, normalized deterministically. `health`, `quota`, and `cost` have score dimensions; the configured-priority share is `1 - health - quota - cost` (default 0.65), and `latency` folds into that priority share rather than scoring independently. | +| `optimize?` | object | latency 0.55, health 0.25, cost 0.10, quota 0.10 | Scoring weights, normalized deterministically. `health`, `quota`, and `cost` have score dimensions; the configured-priority share is `1 - health - quota - cost` (default 0.55), and `latency` folds into that priority share rather than scoring independently. | | `limits?` | object | — | Hard limits, e.g. `maxEstimatedCostUsd` (enforced by the dry-run evaluator when candidate cost evidence is known). | | `unknownEvidence?` | object | capability `exclude`, health/quota/cost `penalize` | How unknown evidence is treated per dimension: `allow`, `penalize`, or `exclude`. Unknown never becomes zero. |