Skip to content
Closed
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
19 changes: 15 additions & 4 deletions src/routing/evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,14 +304,25 @@ export function evaluatePolicyProfile(
const excludedByUnknown = unknown && profile.unknownEvidence.capability === "exclude";
const costLimit = profile.limits.maxEstimatedCostUsd;
const estimatedCost = evidence.cost?.estimatedUsd;
const costEstimateKnown = typeof estimatedCost === "number" && Number.isFinite(estimatedCost);
const overCostLimit = costLimit !== undefined
&& typeof estimatedCost === "number"
&& Number.isFinite(estimatedCost)
&& estimatedCost > costLimit;
&& costEstimateKnown
&& estimatedCost! > costLimit;
if (overCostLimit) {
exclusions.push({ code: "cost-limit", detail: "maxEstimatedCostUsd" });
}
let eligible = !unsatisfied && !excludedByUnknown && !overCostLimit;
// A cap can only be *proven* satisfied when the estimate is known. The live
// routing path often has no usage evidence yet, so the default stays
// "allow" to preserve the documented dry-run contract; operators who need a
// genuine hard ceiling opt into "exclude". The distinct exclusion code lets
// a trace distinguish "known above the cap" from "cost is unknown".
const unknownCostBlocked = costLimit !== undefined
&& !costEstimateKnown
&& profile.limits.onUnknownCost === "exclude";
if (unknownCostBlocked) {
exclusions.push({ code: "cost-limit-unknown", detail: "maxEstimatedCostUsd" });
}
let eligible = !unsatisfied && !excludedByUnknown && !overCostLimit && !unknownCostBlocked;

// Health scoring (RI-06): live hard cooldown is authoritative and
// excludes; unknown health follows the profile's unknownEvidence policy;
Expand Down
11 changes: 10 additions & 1 deletion src/routing/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
OcxConfig,
OcxRoutingProfileConfig,
OcxRoutingUnknownEvidenceMode,
OcxRoutingUnknownCostCapMode,
} from "../types";
import { codexAccountNamespaceEntries } from "../codex/account-namespaces";
import { listComboIds, resolveComboId } from "../combos";
Expand Down Expand Up @@ -60,7 +61,7 @@ export interface NormalizedRoutingProfile {
candidates: Array<{ provider: string; model: string }>;
require: NormalizedRoutingProfileRequirements;
optimize: { latency: number; health: number; cost: number; quota: number };
limits: { maxEstimatedCostUsd?: number };
limits: { maxEstimatedCostUsd?: number; onUnknownCost?: OcxRoutingUnknownCostCapMode };
unknownEvidence: Record<"capability" | "health" | "quota" | "cost", OcxRoutingUnknownEvidenceMode>;
revision: string;
}
Expand Down Expand Up @@ -317,6 +318,11 @@ export function routingProfileIssues(
|| limits.maxEstimatedCostUsd < 0)) {
issues.push({ path: ["limits", "maxEstimatedCostUsd"], message: "maxEstimatedCostUsd must be a non-negative number" });
}
if (limits.onUnknownCost !== undefined
&& limits.onUnknownCost !== "allow"
&& limits.onUnknownCost !== "exclude") {
issues.push({ path: ["limits", "onUnknownCost"], message: 'onUnknownCost must be "allow" or "exclude"' });
}
}
}

Expand Down Expand Up @@ -402,6 +408,9 @@ export function normalizeRoutingProfile(id: string, raw: OcxRoutingProfileConfig
...(raw.limits?.maxEstimatedCostUsd !== undefined
? { maxEstimatedCostUsd: raw.limits.maxEstimatedCostUsd }
: {}),
...(raw.limits?.onUnknownCost !== undefined
? { onUnknownCost: raw.limits.onUnknownCost }
: {}),
},
unknownEvidence: normalizedUnknownEvidence(raw),
};
Expand Down
14 changes: 14 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -955,9 +955,23 @@ export interface OcxRoutingProfileOptimize {
quota?: number;
}

/**
* Policy for the hard cost ceiling when a candidate has no finite cost
* estimate. `"allow"` (default) preserves the documented dry-run contract:
* the cap only excludes evidence known to exceed it. `"exclude"` makes the
* ceiling fail-closed, so a candidate that cannot be proven under the cap is
* ineligible.
*/
export type OcxRoutingUnknownCostCapMode = "allow" | "exclude";

export interface OcxRoutingProfileLimits {
/** Hard per-request estimated-cost ceiling in USD. */
maxEstimatedCostUsd?: number;
/**
* How `maxEstimatedCostUsd` behaves when the estimate is unknown.
* Defaults to `"allow"`; opt in to `"exclude"` for a true hard ceiling.
*/
onUnknownCost?: OcxRoutingUnknownCostCapMode;
}

export interface OcxRoutingProfileUnknownEvidence {
Expand Down
216 changes: 216 additions & 0 deletions tests/cost-cap-unknown-evidence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/**
* Reproduction for issue #1181 — "Routing: define hard cost-cap behavior when
* runtime cost evidence is unknown".
*
* The hard ceiling `limits.maxEstimatedCostUsd` is documented as a hard
* per-request cap. In the live routing path it never fires, because
* `router.ts` assembles cost evidence WITHOUT usage:
*
* costEvidenceForCandidate({ provider, model, limitUsd }) // no `usage`
*
* `costEvidenceForCandidate` then returns `{ limitUsd, incomplete: true }`
* with no `estimatedUsd`, and the evaluator's cap check
* (evaluator.ts:307-310) requires `typeof estimatedCost === "number"`, so an
* unknown estimate silently passes a cap the operator configured as hard.
*
* The existing test in cost-scoring.test.ts only exercises the cap with
* `usage: USAGE` supplied — i.e. on a code path production never takes.
*
* These tests pin both sides of `limits.onUnknownCost`:
* - the default `"allow"`, which preserves the documented dry-run contract
* and lets an unprovable candidate through, and
* - the opt-in `"exclude"`, which makes the ceiling genuinely hard and
* reports the distinct `cost-limit-unknown` exclusion.
*
* The first case was originally written as a failing reproduction before the
* evaluator change landed; it is retained to keep the fail-open default
* asserted rather than assumed.
*/

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 } from "../src/routing/cost";
import { evaluatePolicyProfile } 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-cap-"));
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 });
});

/** Mirrors the live routing path: a cap is configured, usage is NOT available. */
function configWithCap(capUsd: number, overrides: Record<string, unknown> = {}): OcxConfig {
return {
port: 10100,
defaultProvider: "anthropic",
providers: {
anthropic: {
adapter: "anthropic",
baseUrl: "https://api.anthropic.com/v1",
apiKey: "kan",
models: ["claude-opus-5"],
},
},
routingProfiles: {
cost: {
candidates: [{ provider: "anthropic", model: "claude-opus-5" }],
optimize: { cost: 0.8 },
limits: { maxEstimatedCostUsd: capUsd },
unknownEvidence: { capability: "allow", health: "allow", quota: "allow", cost: "allow" },
...overrides,
},
},
} as OcxConfig;
}

describe("issue #1181 — hard cost cap under unknown evidence", () => {
test("default allow: live-path evidence carries no estimate, so the cap does not fire", async () => {
// Exactly how router.ts:515-519 builds it — no `usage` argument.
const evidence = costEvidenceForCandidate({
provider: "anthropic",
model: "claude-opus-5",
limitUsd: 0.000001, // an absurdly low cap; nothing should realistically pass
});

// The evidence is explicitly unknown, and correctly so.
expect(evidence.estimatedUsd).toBeUndefined();
expect(evidence.incomplete).toBe(true);
expect(evidence.limitUsd).toBe(0.000001);

const result = evaluatePolicyProfile(configWithCap(0.000001), "cost", {}, [
{
provider: "anthropic",
model: "claude-opus-5",
capability: { contextWindow: 200000 },
cost: evidence,
},
]);

// Current behaviour: the candidate is eligible and selected despite a cap
// of $0.000001. No `cost-limit` exclusion is recorded, and nothing in the
// trace distinguishes "known below cap" from "cost unknown".
expect(result.candidates[0]!.eligible).toBe(true);
expect(
result.candidates[0]!.exclusions.some(e => e.code === "cost-limit"),
).toBe(false);
expect(result.selectedIndex).toBe(0);
});

test("opt-in exclude: fail-closed cap excludes unknown-cost candidates", async () => {
const evidence = costEvidenceForCandidate({
provider: "anthropic",
model: "claude-opus-5",
limitUsd: 0.000001,
});

// Proposed opt-in policy: limits.onUnknownCost = "exclude".
const result = evaluatePolicyProfile(
configWithCap(0.000001, { limits: { maxEstimatedCostUsd: 0.000001, onUnknownCost: "exclude" } }),
"cost",
{},
[
{
provider: "anthropic",
model: "claude-opus-5",
capability: { contextWindow: 200000 },
cost: evidence,
},
],
);

expect(result.candidates[0]!.eligible).toBe(false);
// Distinct code so operators can tell "over a known cap" from "cost unknown".
expect(
result.candidates[0]!.exclusions.some(e => e.code === "cost-limit-unknown"),
).toBe(true);
expect(result.selectedIndex).toBeNull();
});

test("cap policy and unknownEvidence.cost are distinct mechanisms", async () => {
// unknownEvidence.cost governs SCORING of an unknown-cost candidate;
// limits.onUnknownCost governs whether the hard CEILING applies to it.
// They must produce distinct exclusion codes so a trace stays diagnosable.
const evidence = costEvidenceForCandidate({
provider: "anthropic",
model: "claude-opus-5",
limitUsd: 0.000001,
});

const scoringExcluded = evaluatePolicyProfile(
configWithCap(0.000001, {
unknownEvidence: { capability: "allow", health: "allow", quota: "allow", cost: "exclude" },
}),
"cost",
{},
[{ provider: "anthropic", model: "claude-opus-5", capability: { contextWindow: 200000 }, cost: evidence }],
);

const codes = scoringExcluded.candidates[0]!.exclusions.map(e => e.code);
expect(codes).toContain("unknown-price");
expect(codes).not.toContain("cost-limit-unknown");
expect(scoringExcluded.candidates[0]!.eligible).toBe(false);
});

test("no cap configured — onUnknownCost is inert", async () => {
const evidence = costEvidenceForCandidate({ provider: "anthropic", model: "claude-opus-5" });
const noCap = {
port: 10100,
defaultProvider: "anthropic",
providers: {
anthropic: { adapter: "anthropic", baseUrl: "https://api.anthropic.com/v1", apiKey: "kan", models: ["claude-opus-5"] },
},
routingProfiles: {
cost: {
candidates: [{ provider: "anthropic", model: "claude-opus-5" }],
optimize: { cost: 0.8 },
limits: { onUnknownCost: "exclude" }, // no maxEstimatedCostUsd
unknownEvidence: { capability: "allow", health: "allow", quota: "allow", cost: "allow" },
},
},
} as unknown as OcxConfig;

const result = evaluatePolicyProfile(noCap, "cost", {}, [
{ provider: "anthropic", model: "claude-opus-5", capability: { contextWindow: 200000 }, cost: evidence },
]);

// Without a ceiling there is nothing to fail closed against.
expect(result.candidates[0]!.eligible).toBe(true);
expect(
result.candidates[0]!.exclusions.some(e => e.code === "cost-limit-unknown"),
).toBe(false);
});

test("default stays allow — the documented contract is unchanged", async () => {
const evidence = costEvidenceForCandidate({
provider: "anthropic",
model: "claude-opus-5",
limitUsd: 0.000001,
});

// No onUnknownCost configured → must behave exactly as today (fail-open),
// so existing deployments do not lose every live route on upgrade.
const result = evaluatePolicyProfile(configWithCap(0.000001), "cost", {}, [
{
provider: "anthropic",
model: "claude-opus-5",
capability: { contextWindow: 200000 },
cost: evidence,
},
]);

expect(result.candidates[0]!.eligible).toBe(true);
expect(result.selectedIndex).toBe(0);
});
});
Loading