fokemal / server /src /lib /served-model.ts
automindy's picture
Sync to upstream v0.13.7, keep HF Space adaptations
dcedb25
Raw History Blame Contribute Delete
4.22 kB
// Served-model drift guard (#534 follow-up).
//
// The gateway deliberately overwrites the upstream `model` field with the
// routed model id before the caller sees it β€” the public contract exposes the
// model that was ROUTED, because provider metadata is unreliable (Reka returns
// the literal placeholder "default" for concrete models, #568). That overwrite
// also destroys the only evidence when a provider silently serves a DIFFERENT
// model than requested (verified live: kilo-auto/small serving Gemma;
// kilo-auto/free already drifted once). This module receives the raw upstream
// value β€” captured at the proxy boundary BEFORE the overwrite β€” compares it
// against the routed id ignoring cosmetic spelling differences, and reports
// genuine drift two ways:
// - a structured `[ServedModel]` warn log, once per (platform, requested,
// served) tuple per process, so a busy drifted route cannot spam the log;
// - the raw served id for persistence in requests.served_model (NULL when
// it matches, which keeps the column empty in the healthy case).
// The response contract is untouched: callers keep seeing the routed model.
// Cosmetic differences a provider may legitimately introduce when echoing the
// model id back. Anything that survives normalization on BOTH sides and still
// differs is treated as drift.
// 1. case-insensitive;
// 2. one trailing pricing/routing tier tag (":free"-style β€” OpenRouter and
// friends: :free, :beta, :extended, :nitro, :standard);
// 3. provider/org namespacing β€” only the last '/'-segment identifies the
// model ("meta-llama/llama-3.3-70b" echoes "llama-3.3-70b");
// 4. separator spelling β€” runs of whitespace/underscores/hyphens are one
// separator (mirrors services/model-groups.ts normalizeGroupKey, which
// applies the same rule to display names).
export function normalizeModelIdForDrift(id: string): string {
let s = (id ?? '').trim().toLowerCase();
s = s.replace(/:(free|beta|extended|nitro|standard)$/, '');
const lastSlash = s.lastIndexOf('/');
if (lastSlash >= 0) s = s.slice(lastSlash + 1);
return s.replace(/[\s_-]+/g, '-').trim();
}
// Upstream values that name no model at all β€” comparing them proves nothing.
// "default" is Reka's documented placeholder; empty/unknown guard against
// sloppy OpenAI-compatible shims.
const PLACEHOLDER_MODEL_VALUES = new Set(['', 'default', 'unknown']);
// Once-per-tuple warn cache. Bounded: a hostile/broken upstream that invents a
// fresh id per response must not grow this set without limit β€” past the cap we
// keep persisting drift but stop adding (and therefore logging) new tuples.
const MAX_OBSERVED_TUPLES = 1000;
const observedTuples = new Set<string>();
/** Test hook: clear the once-per-tuple warn cache. */
export function resetServedModelObservations(): void {
observedTuples.clear();
}
/**
* Compare the raw upstream-reported model against the routed id.
* Returns the raw served id when it GENUINELY differs (persist it), or null
* when it matches, differs only cosmetically, or names no model at all
* (store NULL β€” keeps the requests table small). Logs a structured warn once
* per (platform, requested, served) tuple per process.
*/
export function observeServedModel(opts: {
platform: string;
requestedModel: string;
servedModel: string | null | undefined;
}): string | null {
const { platform, requestedModel, servedModel } = opts;
if (typeof servedModel !== 'string') return null;
const served = servedModel.trim();
if (PLACEHOLDER_MODEL_VALUES.has(served.toLowerCase())) return null;
if (normalizeModelIdForDrift(requestedModel) === normalizeModelIdForDrift(served)) return null;
const tuple = `${platform}${requestedModel}${served}`;
if (!observedTuples.has(tuple) && observedTuples.size < MAX_OBSERVED_TUPLES) {
observedTuples.add(tuple);
console.warn(
`[ServedModel] platform=${platform} requested=${requestedModel} served=${served} ` +
`β€” upstream reported a different model than the routed id (#534). ` +
`Response contract unchanged (clients still see the routed model); drift recorded in requests.served_model.`,
);
}
return served;
}