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
Original file line number Diff line number Diff line change
Expand Up @@ -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? } }`.

Expand Down
12 changes: 6 additions & 6 deletions docs-site/src/content/docs/reference/configuration/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>`. |
| `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.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. |

Expand Down Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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) {
Expand Down
77 changes: 77 additions & 0 deletions src/routing/cost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* 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 } 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;
}

/**
* 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 estimate = estimateRequestCost({
provider: input.provider,
model: input.model,
usage: input.usage,
usageStatus: input.usageStatus ?? "estimated",
...(input.serviceTier ? { serviceTier: input.serviceTier } : {}),
});
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 } : {}),
};
}
Comment thread
Wibias marked this conversation as resolved.

/**
* 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 === undefined || !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));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
24 changes: 22 additions & 2 deletions src/routing/evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ 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;
/** Unknown health under "allow": neutral midpoint of the [0,1] health scale. */
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). */
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand Down
2 changes: 0 additions & 2 deletions src/routing/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ export interface RouteCostEvidence {
priceSource?: string;
incomplete?: boolean;
limitUsd?: number;
excludedByLimit?: boolean;
}

export interface RouteScoreEvidence {
Expand Down Expand Up @@ -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;
}

Expand Down
178 changes: 178 additions & 0 deletions tests/cost-scoring.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
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> = {}): 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(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);

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);

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);
});
Comment thread
Wibias marked this conversation as resolved.

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();
});
});
Loading
Loading