fix(deepseek,model-catalog): vision for V4.1-Flash ids, scope synced catalog to gateways
- Declare deepseek-v4.1-flash and deepseek-flash as vision-capable in MODEL_CAPABILITIES - Share installed catalogSource across route chunks via globalThis.__9rCatalogSource - Scope catalog modality keys by provider:model to prevent cross-gateway collisions - Upgrade catalog format to v2 with automatic rebuild of older schemas
This commit is contained in:
@@ -116,6 +116,16 @@ export const MODEL_CAPABILITIES = {
|
||||
// DeepSeek's first V4 model with image input; text limits match V4-Flash.
|
||||
"deepseek-v4-flash-vision-exp": { vision: true, reasoning: true, thinkingFormat: "deepseek", contextWindow: 1000000, maxOutput: 384000 },
|
||||
|
||||
// DeepSeek V4.1-Flash is natively multimodal — models.dev lists
|
||||
// opencode-go/deepseek-v4.1-flash with modalities.input ["text","image"] — and upstream
|
||||
// the retired v4-flash / vision-exp ids route to it, so the live V4.1 ids carry the
|
||||
// same image capability as the exp id above. "deepseek-flash" is the GA id on the
|
||||
// DeepSeek API; it previously fell through to the generic *deepseek* pattern, whose
|
||||
// 128K/64K limits are kept here. The repeated fields are deliberate: an exact entry
|
||||
// short-circuits the pattern table, so a vision-only delta would drop them.
|
||||
"deepseek-v4.1-flash": { vision: true, reasoning: true, thinkingFormat: "deepseek", contextWindow: 1000000, maxOutput: 384000 },
|
||||
"deepseek-flash": { vision: true, reasoning: true, thinkingFormat: "deepseek", contextWindow: 128000, maxOutput: 64000 },
|
||||
|
||||
// Qwen plain coder/text (no vision) — registry "vision-model" / "coder-model" aliases
|
||||
"vision-model": { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 },
|
||||
"coder-model": { reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 },
|
||||
@@ -431,14 +441,27 @@ const MODALITY_KEYS = ["vision", "pdf", "audioInput", "videoInput"];
|
||||
|
||||
// Catalog lookups, installed by the server at startup. Left as no-ops in the
|
||||
// browser bundle, where there is no file to read.
|
||||
//
|
||||
// The server bundles this module into every route chunk that needs it, and each
|
||||
// copy carries its own module state, so an install landing in the copy the
|
||||
// startup hook imported stays invisible to the copy resolving requests. The slot
|
||||
// lives on globalThis instead; the local binding is the fast path.
|
||||
let catalogSource = null;
|
||||
|
||||
/**
|
||||
* Install the synced catalog reader (server only).
|
||||
* @param {{ getModalities: Function, getLimits: Function } | null} source
|
||||
* @param {{ getModalities: (provider: string, model: string) => object|null,
|
||||
* getLimits: (provider: string, model: string) => object|null } | null} source
|
||||
*/
|
||||
export function setCatalogSource(source) {
|
||||
catalogSource = source;
|
||||
if (typeof globalThis !== "undefined") globalThis.__9rCatalogSource = source;
|
||||
}
|
||||
|
||||
function getCatalogSource() {
|
||||
if (catalogSource) return catalogSource;
|
||||
if (typeof globalThis === "undefined") return null;
|
||||
return (catalogSource = globalThis.__9rCatalogSource || null);
|
||||
}
|
||||
|
||||
// Apply the synced catalog + name heuristic on top of a table-resolved result.
|
||||
@@ -447,15 +470,16 @@ export function setCatalogSource(source) {
|
||||
function refine(base, provider, model) {
|
||||
const result = { ...DEFAULT_CAPABILITIES, ...base };
|
||||
|
||||
if (catalogSource) {
|
||||
const modalities = catalogSource.getModalities(model);
|
||||
const source = getCatalogSource();
|
||||
if (source) {
|
||||
const modalities = source.getModalities(provider, model);
|
||||
if (modalities) {
|
||||
for (const key of MODALITY_KEYS) {
|
||||
if (modalities[key] === true) result[key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const limits = catalogSource.getLimits(provider, model);
|
||||
const limits = source.getLimits(provider, model);
|
||||
if (limits) {
|
||||
if (limits.contextWindow > 0) result.contextWindow = limits.contextWindow;
|
||||
if (limits.maxOutput > 0) result.maxOutput = limits.maxOutput;
|
||||
|
||||
@@ -13,6 +13,11 @@ export const CATALOG_FILE = path.join(DATA_DIR, "model-catalog.json");
|
||||
// Trimmed upstream catalog, read by the add-models skill (not by the router).
|
||||
export const CATALOG_RAW_FILE = path.join(DATA_DIR, "model-catalog-raw.json");
|
||||
|
||||
// Schema of the file this module reads. The writer stamps it; a file carrying an
|
||||
// older value predates provider-scoped modality keys, and its flat keys are not
|
||||
// looked up here, so the sync rebuilds it instead of asking upstream for a 304.
|
||||
export const CATALOG_VERSION = 2;
|
||||
|
||||
const EMPTY = { models: {}, providers: {} };
|
||||
let cache = EMPTY;
|
||||
let cachedMtime = -1;
|
||||
@@ -45,14 +50,19 @@ function load() {
|
||||
return cache;
|
||||
}
|
||||
|
||||
// Modality is a property of the model itself — any gateway serving it inherits
|
||||
// the same image/video/pdf support, so this is keyed by model id alone.
|
||||
export function getCatalogModalities(model) {
|
||||
return load().models[baseId(model)] || null;
|
||||
// Modalities are recorded per gateway upstream, and gateways disagree about the
|
||||
// same weights — some do not proxy images at all — so the key is provider +
|
||||
// model, in the local provider id space, exactly like the limits below. Keying
|
||||
// by model id alone made short ids collide across vendors: "auto", "free" and
|
||||
// "efficient" are router modes in one catalog and model names in another, and a
|
||||
// request to the router mode inherited a stranger's vision.
|
||||
export function getCatalogModalities(provider, model) {
|
||||
if (!provider) return null;
|
||||
return load().models[`${provider}:${baseId(model)}`] || null;
|
||||
}
|
||||
|
||||
// Context and output limits are a property of the gateway, not the model: each
|
||||
// one truncates differently, so these stay keyed by provider + model.
|
||||
// Context and output limits are a property of the gateway too: each one
|
||||
// truncates differently, so these stay keyed by provider + model.
|
||||
export function getCatalogLimits(provider, model) {
|
||||
const byProvider = provider && load().providers[provider];
|
||||
if (!byProvider) return null;
|
||||
|
||||
Reference in New Issue
Block a user