auto-model-router 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,14 +7,14 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "auto-model-router: a local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter",
10
- "version": "0.24.0",
10
+ "version": "0.25.0",
11
11
  "pluginRoot": "."
12
12
  },
13
13
  "plugins": [
14
14
  {
15
15
  "name": "auto-model-router",
16
16
  "description": "Local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter. Runs in-process, routes per turn by price and task complexity, with budget caps, mid-stream escalation, and cache-aware hysteresis.",
17
- "version": "0.24.0",
17
+ "version": "0.25.0",
18
18
  "author": {
19
19
  "name": "drewappling",
20
20
  "email": "drewappling@gmail.com"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-model-router",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "private": false,
5
5
  "description": "Local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter",
6
6
  "type": "module",
@@ -254,20 +254,33 @@ export function buildCandidates(args: BuildCandidatesArgs): { candidates: Candid
254
254
  // Price ceilings at the ACTUAL prompt size: long-context overrides can
255
255
  // push a model over the ceiling exactly when conversations get long.
256
256
  // Catalog prices are per-token; ceilings are per million tokens.
257
+ //
258
+ // The ceiling is compared against the BIASED price, because that is what the turn
259
+ // actually costs this deployment: capacity already paid for — an Ollama plan's
260
+ // included credits, a Claude Pro/Max subscription — carries the list price of the
261
+ // twin it is priced from, and a subscription model at $2-5/Mtok list would be
262
+ // thrown out here on every cheap tier before `costBias` was ever consulted. That
263
+ // made the bias silently inert: setting it to 0.00001 changed no decision at all.
257
264
  const price = priceAt(model, Math.max(1, features.promptTokens));
258
- if (!relaxPrice && priceCeiling !== undefined && price.prompt * 1e6 > priceCeiling) {
265
+ const providerBias =
266
+ snapshot.providerBias?.[model.provider] ??
267
+ (model.provider === "ollama" ? cfg.ollama.costBias : (cfg.upstreams.find((u) => u.id === model.provider)?.costBias ?? 1));
268
+ const biasedPrompt = price.prompt * providerBias;
269
+ const biasedCompletion = price.completion * providerBias;
270
+ const biasNote = providerBias === 1 ? "" : ` (×${providerBias} bias on $${(price.prompt * 1e6).toFixed(2)} list)`;
271
+ if (!relaxPrice && priceCeiling !== undefined && biasedPrompt * 1e6 > priceCeiling) {
259
272
  rejected.push({
260
273
  slug,
261
274
  reason: "over_price_ceiling",
262
- detail: `input $${(price.prompt * 1e6).toFixed(2)}/Mtok > ceiling $${priceCeiling.toFixed(2)}`,
275
+ detail: `input $${(biasedPrompt * 1e6).toFixed(2)}/Mtok > ceiling $${priceCeiling.toFixed(2)}${biasNote}`,
263
276
  });
264
277
  continue;
265
278
  }
266
- if (!relaxPrice && tierCfg.maxOutputPerMtok !== undefined && price.completion * 1e6 > tierCfg.maxOutputPerMtok) {
279
+ if (!relaxPrice && tierCfg.maxOutputPerMtok !== undefined && biasedCompletion * 1e6 > tierCfg.maxOutputPerMtok) {
267
280
  rejected.push({
268
281
  slug,
269
282
  reason: "over_price_ceiling",
270
- detail: `output $${(price.completion * 1e6).toFixed(2)}/Mtok > ceiling $${tierCfg.maxOutputPerMtok}`,
283
+ detail: `output $${(biasedCompletion * 1e6).toFixed(2)}/Mtok > ceiling $${tierCfg.maxOutputPerMtok}${biasNote}`,
271
284
  });
272
285
  continue;
273
286
  }
@@ -357,14 +370,9 @@ export function buildCandidates(args: BuildCandidatesArgs): { candidates: Candid
357
370
  filters.escalationCostWeight > 0 && args.escalationUsdPerPromptToken !== undefined
358
371
  ? filters.escalationCostWeight * escalationRate * args.escalationUsdPerPromptToken * features.promptTokens
359
372
  : 0;
360
- // Provider bias: capacity already paid for an Ollama plan's included credits, a
361
- // Claude Pro/Max subscription — is money already spent, so an operator may value it
362
- // below list price in ranking. The ledger still records list price.
363
- // The snapshot carries the LIVE bias (credit-aware, and each named upstream's own);
364
- // the static config value is the fallback for snapshots built without one.
365
- const providerBias =
366
- snapshot.providerBias?.[model.provider] ??
367
- (model.provider === "ollama" ? cfg.ollama.costBias : (cfg.upstreams.find((u) => u.id === model.provider)?.costBias ?? 1));
373
+ // `providerBias` is computed with the price ceilings above capacity already paid for
374
+ // is money already spent, so it is valued below list in ranking AND against the
375
+ // ceilings. The ledger still records list price either way.
368
376
  const effectiveUsd = (fc.expectedUsd / Math.max(trustScore, 0.5) + escalationUsd) * latencyMult * providerBias;
369
377
  // Score is assigned in a SECOND PASS below: both qualityNormalization and
370
378
  // capabilityFloorUsd are properties of the candidate SET, not of one
@@ -358,6 +358,22 @@ describe("adaptive price ceilings", () => {
358
358
  expect(on.candidates.map((c) => c.model.slug)).not.toContain("a/4");
359
359
  expect(on.rejected.some((r) => r.slug === "a/4" && r.reason === "over_price_ceiling")).toBe(true);
360
360
  });
361
+
362
+ test("a price ceiling judges the BIASED price, so prepaid capacity is not thrown out on list", () => {
363
+ // A subscription upstream inherits its OpenRouter twin's list price ($4/Mtok here) and is
364
+ // discounted by `costBias` because the capacity is already paid for. Judging the ceiling on
365
+ // list threw it out before the bias was ever read, which made the bias entirely inert.
366
+ const snap = snapshot(priced);
367
+ const biased = { ...snap, providerBias: { [snap.models[0]!.provider]: 0.1 } };
368
+ const run = (s: typeof snap) =>
369
+ buildCandidates({ req, features, tier: "moderate", task: "coding", snapshot: s, ledger: null, cfg: { ...BASE, adaptivePriceCeilings: true }, expectedCompletionTokens: 512, warmSlug: null });
370
+ // Unbiased: the band tightens moderate to $3 and a/4 is over it.
371
+ expect(run(snap).rejected.some((r) => r.slug === "a/4" && r.reason === "over_price_ceiling")).toBe(true);
372
+ // Biased ×0.1: $4 list is $0.40 to this deployment, so it clears the same ceiling.
373
+ const on = run(biased);
374
+ expect(on.candidates.map((c) => c.model.slug)).toContain("a/4");
375
+ expect(on.rejected.some((r) => r.slug === "a/4")).toBe(false);
376
+ });
361
377
  });
362
378
 
363
379
  describe("quality normalization and capability floor (benchmark findings 4/6)", () => {