fix(models): preserve free-model metadata from discovery (#12763)

* fix(models): preserve live free economics in synced discovery

* docs(changelog): add fragment for free-model metadata discovery fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
KeelTrace
2026-09-18 08:22:26 -07:00
committed by GitHub
parent 5c305680ac
commit 95d3b164a5
4 changed files with 70 additions and 0 deletions

View File

@@ -0,0 +1 @@
- **fix(models):** preserve free-model metadata (`isFree`) discovered live from a provider through synced-model normalization, so free models no longer lose that flag before reaching the UI/consumers ([#12763](https://github.com/diegosouzapw/OmniRoute/pull/12763)) — thanks @keeltrace

View File

@@ -22,6 +22,8 @@ export interface SyncedAvailableModel {
alwaysThinking?: boolean;
supportsTools?: boolean;
supportsVideo?: boolean;
/** Discovery payload supplied free-economics evidence for this model. */
isFree?: boolean;
// #4264: image-input capability captured at sync time (e.g. OpenRouter
// `architecture.input_modalities`/`modality`) so the catalog can surface vision.
supportsVision?: boolean;
@@ -95,6 +97,7 @@ function normalizeSyncedAvailableModel(model: unknown): SyncedAvailableModel | n
...(record.alwaysThinking === true ? { alwaysThinking: true } : {}),
...(typeof record.supportsTools === "boolean" ? { supportsTools: record.supportsTools } : {}),
...(typeof record.supportsVideo === "boolean" ? { supportsVideo: record.supportsVideo } : {}),
...(record.isFree === true ? { isFree: true } : {}),
...(record.supportsVision === true ? { supportsVision: true } : {}),
};
}

View File

@@ -19,6 +19,26 @@ function toNonEmptyString(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function isZeroPrice(value: unknown): boolean {
if (typeof value === "number") return value === 0;
if (typeof value !== "string" || value.trim().length === 0) return false;
const parsed = Number(value);
return Number.isFinite(parsed) && parsed === 0;
}
function hasLiveFreeEvidence(
id: string,
record: JsonRecord,
promptPrice: string | number | undefined,
completionPrice: string | number | undefined
): boolean {
return (
record.isFree === true ||
id.endsWith(":free") ||
(isZeroPrice(promptPrice) && isZeroPrice(completionPrice))
);
}
/**
* Resolve a positive integer token limit from a list of candidate values.
* Used to fall back across the differently-named context/output fields that
@@ -472,6 +492,18 @@ export function normalizeDiscoveredModels(
// models reached the catalog with no vision flag and vision-capable models
// (which work at request time) showed up as non-vision after import.
const supportsVision = detectVisionInput(record);
const pricing = asRecord(record.pricing);
const promptPrice =
typeof pricing.prompt === "string" || typeof pricing.prompt === "number"
? pricing.prompt
: undefined;
const completionPrice =
typeof pricing.completion === "string" || typeof pricing.completion === "number"
? pricing.completion
: undefined;
// Persist only evidence present in this discovery payload. Static catalog
// membership is intentionally not evidence about this connection's economics.
const isFree = hasLiveFreeEvidence(id, record, promptPrice, completionPrice);
deduped.set(id, {
id,
@@ -507,6 +539,7 @@ export function normalizeDiscoveredModels(
...(record.alwaysThinking === true ? { alwaysThinking: true } : {}),
...(typeof record.supportsTools === "boolean" ? { supportsTools: record.supportsTools } : {}),
...(typeof record.supportsVideo === "boolean" ? { supportsVideo: record.supportsVideo } : {}),
...(isFree ? { isFree: true } : {}),
...(supportsVision ? { supportsVision: true } : {}),
});
}

View File

@@ -0,0 +1,33 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { normalizeSyncedAvailableModels } from "@/lib/db/models/synced";
import { normalizeDiscoveredModels } from "@/lib/providerModels/modelDiscovery";
test("normalizeDiscoveredModels records only free evidence present in discovery payloads", () => {
const models = normalizeDiscoveredModels(
[
{ id: "declared-free", isFree: true },
{ id: "zero-priced", pricing: { prompt: "0", completion: 0 } },
{ id: "rotating-model:free" },
{ id: "blank-price", pricing: { prompt: "", completion: "" } },
{ id: "paid", pricing: { prompt: "1", completion: "2" } },
],
"example-provider"
);
const byId = new Map(models.map((model) => [model.id, model]));
assert.equal(byId.get("declared-free")?.isFree, true);
assert.equal(byId.get("zero-priced")?.isFree, true);
assert.equal(byId.get("rotating-model:free")?.isFree, true);
assert.equal(byId.get("blank-price")?.isFree, undefined);
assert.equal(byId.get("paid")?.isFree, undefined);
});
test("normalizeSyncedAvailableModels preserves discovery free metadata", () => {
const [model] = normalizeSyncedAvailableModels([
{ id: "live-free", name: "Live Free", source: "imported", isFree: true },
]);
assert.equal(model?.isFree, true);
});