Spaces:
Runtime error
Runtime error
File size: 5,780 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | import {
getSyncedAvailableModelsForConnection,
replaceSyncedAvailableModelsForConnection,
type SyncedAvailableModel,
} from "@/lib/db/models";
type JsonRecord = Record<string, unknown>;
function asRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
}
function toNonEmptyString(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
/**
* Resolve a positive integer token limit from a list of candidate values.
* Used to fall back across the differently-named context/output fields that
* upstream catalogs expose (e.g. OpenRouter uses `context_length` /
* `top_provider.context_length` instead of `inputTokenLimit`). See #3202.
*/
function firstPositiveNumber(...candidates: unknown[]): number | undefined {
for (const candidate of candidates) {
if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0) {
return candidate;
}
}
return undefined;
}
function modalitiesIncludeImage(value: unknown): boolean {
return (
Array.isArray(value) &&
value.some((entry) => toNonEmptyString(entry)?.toLowerCase() === "image")
);
}
/**
* #4264: detect image-input (vision) capability from a discovered model record.
* Handles the common upstream shapes: an explicit `supportsVision` flag, the
* OpenRouter `architecture.input_modalities` array and string `architecture.modality`
* ("text+image->text" — the input side is everything before "->"), and a top-level
* `input_modalities` array. Returns false when the upstream exposes no modality info.
*/
export function detectVisionInput(record: JsonRecord): boolean {
if (record.supportsVision === true) return true;
const architecture = asRecord(record.architecture);
if (modalitiesIncludeImage(architecture.input_modalities)) return true;
if (modalitiesIncludeImage(record.input_modalities)) return true;
const modality = toNonEmptyString(architecture.modality) || toNonEmptyString(record.modality);
if (modality) {
const [inputPart] = modality.toLowerCase().split("->");
if ((inputPart || "").includes("image")) return true;
}
return false;
}
export function isAutoFetchModelsEnabled(providerSpecificData: unknown): boolean {
return asRecord(providerSpecificData).autoFetchModels !== false;
}
export function normalizeDiscoveredModels(models: unknown): SyncedAvailableModel[] {
const items = Array.isArray(models) ? models : [];
const deduped = new Map<string, SyncedAvailableModel>();
for (const item of items) {
const record = asRecord(item);
const id =
toNonEmptyString(record.id) ||
toNonEmptyString(record.name) ||
toNonEmptyString(record.model);
if (!id) continue;
const name =
toNonEmptyString(record.name) ||
toNonEmptyString(record.displayName) ||
toNonEmptyString(record.model) ||
id;
const supportedEndpoints = Array.isArray(record.supportedEndpoints)
? Array.from(
new Set(
record.supportedEndpoints
.map((endpoint) => toNonEmptyString(endpoint))
.filter((endpoint): endpoint is string => Boolean(endpoint))
)
).sort()
: undefined;
const topProvider = asRecord(record.top_provider);
// OpenRouter (and similar passthrough catalogs) report the context window as
// `context_length` / `top_provider.context_length`, not `inputTokenLimit`.
// Fall back across those names so synced models carry a real window instead
// of the provider default (128K). Explicit `inputTokenLimit` still wins. #3202
const inputTokenLimit = firstPositiveNumber(
record.inputTokenLimit,
record.context_length,
record.contextLength,
topProvider.context_length
);
const outputTokenLimit = firstPositiveNumber(
record.outputTokenLimit,
topProvider.max_completion_tokens
);
// #4264: capture image-input (vision) capability at sync time. OpenRouter (and
// similar passthrough catalogs) declare it via `architecture.input_modalities`
// (e.g. ["text","image"]) or the string `architecture.modality` ("text+image->text");
// some providers expose a top-level `input_modalities`. Without this, synced
// 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);
deduped.set(id, {
id,
name,
source: "imported",
...(toNonEmptyString(record.apiFormat)
? { apiFormat: toNonEmptyString(record.apiFormat)! }
: {}),
...(supportedEndpoints && supportedEndpoints.length > 0 ? { supportedEndpoints } : {}),
...(typeof inputTokenLimit === "number" ? { inputTokenLimit } : {}),
...(typeof outputTokenLimit === "number" ? { outputTokenLimit } : {}),
...(typeof record.description === "string" ? { description: record.description } : {}),
...(record.supportsThinking === true ? { supportsThinking: true } : {}),
...(supportsVision ? { supportsVision: true } : {}),
});
}
return Array.from(deduped.values());
}
export async function getCachedDiscoveredModels(
providerId: string,
connectionId: string
): Promise<SyncedAvailableModel[]> {
return getSyncedAvailableModelsForConnection(providerId, connectionId);
}
export async function persistDiscoveredModels(
providerId: string,
connectionId: string,
models: unknown
): Promise<SyncedAvailableModel[]> {
const normalized = normalizeDiscoveredModels(models);
await replaceSyncedAvailableModelsForConnection(providerId, connectionId, normalized);
return normalized;
}
|