mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 21:32:20 +03:00
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:
1
changelog.d/fixes/12763-free-model-metadata-discovery.md
Normal file
1
changelog.d/fixes/12763-free-model-metadata-discovery.md
Normal 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
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
33
tests/unit/model-discovery-free-metadata.test.ts
Normal file
33
tests/unit/model-discovery-free-metadata.test.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user