Merge remote-tracking branch 'upstream/master'
# Conflicts: # open-sse/handlers/chatCore.js # open-sse/providers/registry/index.js # open-sse/services/usage.js
This commit is contained in:
commit
eaf204b83c
114 files changed
+5587
-2184
No files matched your search
@@ -6,6 +6,16 @@
|
||||
// 3. PATTERN_CAPABILITIES — glob match, ordered specific -> generic
|
||||
// 4. DEFAULT_CAPABILITIES — safe floor (always returned)
|
||||
//
|
||||
// Two extra layers then refine the result, and neither can override the hand
|
||||
// written tables above (steps 1-2 short-circuit before they are consulted):
|
||||
// • the synced catalog — modalities keyed by model, limits keyed by provider
|
||||
// + model, refreshed from models.dev in the background. It reads a file, so
|
||||
// the server installs it via setCatalogSource(); this module stays free of
|
||||
// node:fs because the dashboard bundles it into the browser too.
|
||||
// • visionPatterns.js — name-based vision detection, last resort so a model
|
||||
// nobody has catalogued yet still accepts images.
|
||||
// Both only ever turn a capability ON.
|
||||
//
|
||||
// ── HOW TO ADD / UPDATE A MODEL ──────────────────────────────────────
|
||||
// Authoritative data source: https://models.dev/api.json (145 providers, 4000+
|
||||
// models, MIT). Each model exposes the exact fields we map below:
|
||||
@@ -23,6 +33,7 @@
|
||||
// 2.0+, Grok, Perplexity). Verify with: curl -s https://models.dev/api.json
|
||||
|
||||
import { matchPattern } from "./pricing.js";
|
||||
import { looksLikeVisionModel } from "./visionPatterns.js";
|
||||
|
||||
/**
|
||||
* Safe floor — every resolved result is merged over this so consumers
|
||||
@@ -46,6 +57,7 @@ export const DEFAULT_CAPABILITIES = {
|
||||
thinkingFormat: null,
|
||||
thinkingCanDisable: true, // false → model cannot turn thinking off (clamp to min instead of disable)
|
||||
thinkingRange: null, // { min, max } for budget formats; null = no clamp
|
||||
thinkingEffortSupported: false, // zai format only: model accepts a reasoning_effort level (GLM-5.2+; older GLM ignores it)
|
||||
// limits (tokens)
|
||||
contextWindow: 200000,
|
||||
maxOutput: 64000,
|
||||
@@ -94,8 +106,14 @@ export const MODEL_CAPABILITIES = {
|
||||
// Gemini image-gen / OpenAI image / xai image variants
|
||||
"gpt-image-1": { imageOutput: true, tools: false },
|
||||
|
||||
// GLM vision variant (text GLM has no vision)
|
||||
"glm-4.6v": { vision: true, reasoning: true, thinkingFormat: "zai", contextWindow: 128000 },
|
||||
// GLM vision variants (text GLM has no vision) — 5.3-Flash and 5V-Turbo are
|
||||
// natively multimodal per z.ai, and 5.3-Flash carries the full 1M window.
|
||||
"glm-5.3-flash": { vision: true, videoInput: true, pdf: true, reasoning: true, thinkingFormat: "zai", contextWindow: 1000000, maxOutput: 131072 },
|
||||
"glm-4.6v": { vision: true, videoInput: true, reasoning: true, thinkingFormat: "zai", contextWindow: 128000, maxOutput: 32768 },
|
||||
"glm-4.5v": { vision: true, videoInput: true, reasoning: true, thinkingFormat: "zai", contextWindow: 64000, maxOutput: 16384 },
|
||||
|
||||
// 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 },
|
||||
|
||||
// Qwen plain coder/text (no vision) — registry "vision-model" / "coder-model" aliases
|
||||
"vision-model": { vision: true, reasoning: true, thinkingFormat: "qwen", contextWindow: 1000000 },
|
||||
@@ -108,6 +126,8 @@ export const MODEL_CAPABILITIES = {
|
||||
"kimi-for-coding-highspeed": { vision: true, videoInput: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 65536 },
|
||||
"kimi-k2.7-code": { vision: true, videoInput: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 65536 },
|
||||
"kimi-k2.7-code-highspeed": { vision: true, videoInput: true, reasoning: true, thinkingFormat: "kimi", thinkingCanDisable: false, contextWindow: 262144, maxOutput: 65536 },
|
||||
// OpenCode Free Muse Spark — OpenAI Responses reasoning supports up to xhigh.
|
||||
"muse-spark-1.2-contributor-free": { reasoning: true, thinkingFormat: "openai", contextWindow: 1048576, maxOutput: 131072 },
|
||||
};
|
||||
|
||||
const KIRO_GPT_5_6_CAPABILITIES = { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 272000, maxOutput: 128000 };
|
||||
@@ -234,6 +254,8 @@ export const PATTERN_CAPABILITIES = [
|
||||
// ── Grok (vision + Live Search) ──────────────────────────────────
|
||||
{ pattern: "*grok*image*", caps: { imageOutput: true } },
|
||||
{ pattern: "*grok-code*", caps: { reasoning: true, thinkingFormat: "openai", contextWindow: 256000 } },
|
||||
// Grok 4.6: 500k context, no text output limit (docs.x.ai/developers/grok-4-6)
|
||||
{ pattern: "*grok-4.6*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 500000, maxOutput: 500000 } },
|
||||
// Grok 4.5 (Grok CLI / Grok Build): 500k context per cli-chat-proxy /v1/models
|
||||
{ pattern: "*grok-4.5*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 500000, maxOutput: 64000 } },
|
||||
{ pattern: "*grok-4*", caps: { vision: true, reasoning: true, search: true, thinkingFormat: "openai", contextWindow: 256000 } },
|
||||
@@ -261,6 +283,10 @@ export const PATTERN_CAPABILITIES = [
|
||||
{ pattern: "*kimi*", caps: { reasoning: true, thinkingFormat: "kimi", contextWindow: 262144 } },
|
||||
|
||||
// ── GLM / Z.ai (thinking.enabled; disable via enable_thinking:false) ─
|
||||
// reasoning_effort is only read by z.ai from GLM-5.2 onward (docs.z.ai/guides/capabilities/thinking) —
|
||||
// older GLM (4.x, 5.0, 5.1, 5-turbo, 5v-turbo) ignore it, so gate it per exact version, not the "*glm-5*" catch-all.
|
||||
{ pattern: "*glm-5.3*", caps: { reasoning: true, thinkingFormat: "zai", thinkingEffortSupported: true, contextWindow: 200000, maxOutput: 128000 } },
|
||||
{ pattern: "*glm-5.2*", caps: { reasoning: true, thinkingFormat: "zai", thinkingEffortSupported: true, contextWindow: 200000, maxOutput: 128000 } },
|
||||
{ pattern: "*glm-5*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000, maxOutput: 128000 } },
|
||||
{ pattern: "*glm-4.7*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000, maxOutput: 128000 } },
|
||||
{ pattern: "*glm-4*", caps: { reasoning: true, thinkingFormat: "zai", contextWindow: 200000 } },
|
||||
@@ -325,6 +351,46 @@ export const PATTERN_CAPABILITIES = [
|
||||
* @param {string} model
|
||||
* @returns {object} full capabilities object
|
||||
*/
|
||||
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.
|
||||
let catalogSource = null;
|
||||
|
||||
/**
|
||||
* Install the synced catalog reader (server only).
|
||||
* @param {{ getModalities: Function, getLimits: Function } | null} source
|
||||
*/
|
||||
export function setCatalogSource(source) {
|
||||
catalogSource = source;
|
||||
}
|
||||
|
||||
// Apply the synced catalog + name heuristic on top of a table-resolved result.
|
||||
// Strictly additive: a capability already true stays true, and a false one only
|
||||
// flips when an outside source positively declares support.
|
||||
function refine(base, provider, model) {
|
||||
const result = { ...DEFAULT_CAPABILITIES, ...base };
|
||||
|
||||
if (catalogSource) {
|
||||
const modalities = catalogSource.getModalities(model);
|
||||
if (modalities) {
|
||||
for (const key of MODALITY_KEYS) {
|
||||
if (modalities[key] === true) result[key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const limits = catalogSource.getLimits(provider, model);
|
||||
if (limits) {
|
||||
if (limits.contextWindow > 0) result.contextWindow = limits.contextWindow;
|
||||
if (limits.maxOutput > 0) result.maxOutput = limits.maxOutput;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.vision && looksLikeVisionModel(model)) result.vision = true;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getCapabilitiesForModel(provider, model) {
|
||||
if (!model) return { ...DEFAULT_CAPABILITIES };
|
||||
|
||||
@@ -342,13 +408,13 @@ export function getCapabilitiesForModel(provider, model) {
|
||||
if (MODEL_CAPABILITIES[baseModel]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[baseModel] };
|
||||
if (MODEL_CAPABILITIES[model]) return { ...DEFAULT_CAPABILITIES, ...MODEL_CAPABILITIES[model] };
|
||||
|
||||
// 3. Pattern match (first match wins)
|
||||
// 3. Pattern match (first match wins), refined by catalog + name heuristic
|
||||
for (const { pattern, caps } of PATTERN_CAPABILITIES) {
|
||||
if (matchPattern(pattern, baseModel) || matchPattern(pattern, model)) {
|
||||
return { ...DEFAULT_CAPABILITIES, ...caps };
|
||||
return refine(caps, provider, model);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Floor
|
||||
return { ...DEFAULT_CAPABILITIES };
|
||||
return refine(null, provider, model);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Read side of the model catalog synced from models.dev.
|
||||
//
|
||||
// The file is the source of truth; the only thing held in memory is a parsed
|
||||
// copy dropped as soon as the file's mtime changes. getCapabilitiesForModel is
|
||||
// synchronous and runs per request, so the hot path is one stat (~1us) and the
|
||||
// parse (~0.1ms on a ~18KB file) only reruns after a sync.
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { DATA_DIR } from "@/lib/dataDir.js";
|
||||
|
||||
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");
|
||||
|
||||
const EMPTY = { models: {}, providers: {} };
|
||||
let cache = EMPTY;
|
||||
let cachedMtime = -1;
|
||||
|
||||
// "zai-org/GLM-4.6V:free" -> "glm-4.6v"
|
||||
function baseId(model) {
|
||||
if (!model) return "";
|
||||
const withoutVendor = model.includes("/") ? model.split("/").pop() : model;
|
||||
return withoutVendor.toLowerCase().split(":")[0];
|
||||
}
|
||||
|
||||
function load() {
|
||||
let mtime;
|
||||
try {
|
||||
mtime = fs.statSync(CATALOG_FILE).mtimeMs;
|
||||
} catch {
|
||||
cache = EMPTY;
|
||||
cachedMtime = -1;
|
||||
return cache;
|
||||
}
|
||||
if (mtime === cachedMtime) return cache;
|
||||
|
||||
cachedMtime = mtime;
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(CATALOG_FILE, "utf8"));
|
||||
cache = { models: parsed?.models || {}, providers: parsed?.providers || {} };
|
||||
} catch {
|
||||
cache = EMPTY;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// Context and output limits are a property of the gateway, not the model: 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;
|
||||
return byProvider[model] || byProvider[baseId(model)] || null;
|
||||
}
|
||||
|
||||
// Force a re-read on the next lookup (called right after a sync writes the file).
|
||||
export function invalidateCatalog() {
|
||||
cachedMtime = -1;
|
||||
}
|
||||
|
||||
// Hand the reader to capabilities.js. That module is bundled into the browser
|
||||
// too, so it cannot import this file directly — the server pushes it in.
|
||||
export async function installCatalogSource() {
|
||||
const { setCatalogSource } = await import("./capabilities.js");
|
||||
setCatalogSource({ getModalities: getCatalogModalities, getLimits: getCatalogLimits });
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export default {
|
||||
deprecationNotice: "RISK_NOTICE",
|
||||
},
|
||||
category: "oauth",
|
||||
serviceKinds: ["llm", "image"],
|
||||
serviceKinds: ["llm", "image", "webSearch"],
|
||||
transport: {
|
||||
baseUrls: [ANTIGRAVITY_IDE_BASE_URL],
|
||||
format: "antigravity",
|
||||
@@ -82,6 +82,11 @@ export default {
|
||||
loadCodeAssistUserAgent: ANTIGRAVITY_IDE_USER_AGENT,
|
||||
refreshLeadMs: 300000,
|
||||
},
|
||||
searchViaChat: {
|
||||
defaultModel: "gemini-2.5-flash",
|
||||
endpoint: `${ANTIGRAVITY_IDE_BASE_URL}/v1internal:generateContent`,
|
||||
freeTier: "Free — Google Search grounding through an Antigravity OAuth account.",
|
||||
},
|
||||
features: {
|
||||
usage: true,
|
||||
},
|
||||
|
||||
@@ -45,6 +45,7 @@ export default {
|
||||
{ id: "deepseek-v4-pro-max", name: "DeepSeek V4 Pro Max", upstreamModelId: "deepseek-v4-pro" },
|
||||
{ id: "deepseek-v4-pro-none", name: "DeepSeek V4 Pro No Thinking", upstreamModelId: "deepseek-v4-pro" },
|
||||
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash" },
|
||||
{ id: "deepseek-v4-flash-vision-exp", name: "DeepSeek V4 Flash Vision (Exp)" },
|
||||
{ id: "deepseek-chat", name: "DeepSeek V3.2 Chat" },
|
||||
{ id: "deepseek-reasoner", name: "DeepSeek V3.2 Reasoner" },
|
||||
],
|
||||
|
||||
@@ -22,10 +22,12 @@ export default {
|
||||
},
|
||||
models: [
|
||||
{ id: "glm-5.3", name: "GLM 5.3" },
|
||||
{ id: "glm-5.3-flash", name: "GLM 5.3 Flash (Vision)" },
|
||||
{ id: "glm-5.2", name: "GLM 5.2" },
|
||||
{ id: "glm-5.1", name: "GLM 5.1" },
|
||||
{ id: "glm-5", name: "GLM 5" },
|
||||
{ id: "glm-4.7", name: "GLM-4.7" },
|
||||
{ id: "glm-4.6v", name: "GLM 4.6V (Vision)" },
|
||||
{ id: "glm-4.6", name: "GLM-4.6" },
|
||||
{ id: "glm-4.5-air", name: "GLM-4.5-Air" },
|
||||
],
|
||||
|
||||
@@ -46,12 +46,27 @@ export default {
|
||||
],
|
||||
models: [
|
||||
{ id: "glm-5.3", name: "GLM 5.3" },
|
||||
{ id: "glm-5.3-flash", name: "GLM 5.3 Flash (Vision)" },
|
||||
{ id: "glm-5.2", name: "GLM 5.2" },
|
||||
{ id: "glm-5.1", name: "GLM 5.1" },
|
||||
{ id: "glm-5", name: "GLM 5" },
|
||||
{ id: "glm-4.7", name: "GLM 4.7" },
|
||||
{ id: "glm-4.6v", name: "GLM 4.6V (Vision)" },
|
||||
],
|
||||
serviceKinds: ["llm", "webSearch"],
|
||||
// Coding plan bundles web search on the same API key as chat.
|
||||
searchConfig: {
|
||||
baseUrl: "https://api.z.ai/api/mcp/web_search_prime/mcp",
|
||||
method: "POST",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
costPerQuery: 0,
|
||||
searchTypes: ["web"],
|
||||
defaultMaxResults: 5,
|
||||
maxMaxResults: 50,
|
||||
timeoutMs: 10000,
|
||||
cacheTTLMs: 300000,
|
||||
},
|
||||
features: {
|
||||
usage: true,
|
||||
usageApikey: true,
|
||||
|
||||
@@ -120,6 +120,8 @@ import p117 from "./selfhosted-tts.js";
|
||||
import p118 from "./selfhosted-embedding.js";
|
||||
import p119 from "./fish-audio.js";
|
||||
import p120 from "./alitp-intl.js";
|
||||
import p121 from "./xquik.js";
|
||||
import p122 from "./ollama-search.js";
|
||||
|
||||
export default [
|
||||
p0,
|
||||
@@ -243,4 +245,6 @@ export default [
|
||||
p118,
|
||||
p119,
|
||||
p120,
|
||||
p121,
|
||||
p122,
|
||||
];
|
||||
@@ -0,0 +1,35 @@
|
||||
export default {
|
||||
id: "ollama-search",
|
||||
alias: "ollama-search",
|
||||
display: {
|
||||
name: "Ollama Search",
|
||||
icon: "cloud",
|
||||
color: "#ffffff",
|
||||
textIcon: "OL",
|
||||
website: "https://ollama.com",
|
||||
notice: {
|
||||
text: "Web search via Ollama Cloud subscription. Reuses the API key from the Ollama (chat) provider.",
|
||||
apiKeyUrl: "https://ollama.com/settings/keys",
|
||||
},
|
||||
},
|
||||
category: "apikey",
|
||||
authType: "apikey",
|
||||
authModes: ["apikey"],
|
||||
serviceKinds: ["webSearch"],
|
||||
// Credential fallback: reuses the API key registered under the `ollama`
|
||||
// chat provider — one key, chat + search.
|
||||
credentialFallback: "ollama",
|
||||
searchConfig: {
|
||||
baseUrl: "https://ollama.com/api/web_search",
|
||||
method: "POST",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
costPerQuery: 0,
|
||||
freeMonthlyQuota: 1000,
|
||||
searchTypes: ["web"],
|
||||
defaultMaxResults: 5,
|
||||
maxMaxResults: 10,
|
||||
timeoutMs: 10000,
|
||||
cacheTTLMs: 300000,
|
||||
},
|
||||
};
|
||||
@@ -31,12 +31,14 @@ export default {
|
||||
{ format: "openai-responses", baseUrl: "https://opencode.ai/zen/go/v1/responses", auth: { combined: true, header: "Authorization", scheme: "bearer" } },
|
||||
],
|
||||
models: [
|
||||
{ id: "glm-5.3-flash", name: "GLM 5.3 Flash (Vision)", supportedFormats: ["openai"] },
|
||||
{ id: "glm-5.2", name: "GLM 5.2", supportedFormats: ["openai"] },
|
||||
{ id: "glm-5.1", name: "GLM 5.1", supportedFormats: ["openai"] },
|
||||
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code", supportedFormats: ["openai"] },
|
||||
{ id: "kimi-k2.6", name: "Kimi K2.6", supportedFormats: ["openai"] },
|
||||
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", supportedFormats: ["openai", "claude", "openai-responses"] },
|
||||
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", supportedFormats: ["openai", "claude", "openai-responses"] },
|
||||
{ id: "deepseek-v4-flash-vision-exp", name: "DeepSeek V4 Flash Vision (Exp)", supportedFormats: ["openai", "claude", "openai-responses"] },
|
||||
{ id: "mimo-v2.5", name: "MiMo V2.5", supportedFormats: ["openai"] },
|
||||
{ id: "mimo-v2.5-pro", name: "MiMo V2.5 Pro", supportedFormats: ["openai"] },
|
||||
{ id: "minimax-m3", name: "MiniMax M3", supportedFormats: ["openai", "claude"] },
|
||||
|
||||
@@ -19,7 +19,11 @@ export default {
|
||||
},
|
||||
noAuth: true,
|
||||
},
|
||||
models: [],
|
||||
models: [
|
||||
// Only this model is served by /zen/v1/responses; the rest stay on
|
||||
// /chat/completions, so the format is declared per-model, not per-provider.
|
||||
{ id: "muse-spark-1.2-contributor-free", name: "Muse Spark 1.2 Contributor Free", targetFormat: "openai-responses" },
|
||||
],
|
||||
modelsFetcher: { url: "https://opencode.ai/zen/v1/models", type: "opencode-free" },
|
||||
passthroughModels: true,
|
||||
};
|
||||
@@ -27,6 +27,8 @@ export default {
|
||||
refreshUrl: "https://auth.x.ai/oauth2/token",
|
||||
},
|
||||
models: [
|
||||
{ id: "grok-4.6", name: "Grok 4.6" },
|
||||
{ id: "grok-4.5", name: "Grok 4.5" },
|
||||
{ id: "grok-4", name: "Grok 4" },
|
||||
{ id: "grok-4-fast-reasoning", name: "Grok 4 Fast Reasoning" },
|
||||
{ id: "grok-code-fast-1", name: "Grok Code Fast" },
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
export default {
|
||||
id: "xquik",
|
||||
alias: "xquik",
|
||||
display: {
|
||||
name: "Xquik",
|
||||
icon: "tag",
|
||||
color: "#5C3327",
|
||||
textIcon: "XQ",
|
||||
website: "https://docs.xquik.com/api-reference/x/search-tweets",
|
||||
notice: {
|
||||
apiKeyUrl: "https://xquik.com",
|
||||
text: "Searches public X posts. Billing uses 1 Xquik credit per returned post."
|
||||
}
|
||||
},
|
||||
category: "apikey",
|
||||
authType: "apikey",
|
||||
serviceKinds: [
|
||||
"webSearch"
|
||||
],
|
||||
searchConfig: {
|
||||
baseUrl: "https://xquik.com/api/v1/x/tweets/search",
|
||||
validateUrl: "https://xquik.com/api/v1/credits",
|
||||
method: "GET",
|
||||
authType: "apikey",
|
||||
authHeader: "x-api-key",
|
||||
searchTypes: [
|
||||
"x"
|
||||
],
|
||||
defaultMaxResults: 5,
|
||||
maxMaxResults: 100,
|
||||
timeoutMs: 10000,
|
||||
cacheTTLMs: 60000,
|
||||
creditsPerResult: 1
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
// Name-based vision detection — last resort when neither the catalog file nor
|
||||
// the capability tables know a model. Vendors put the modality in the id
|
||||
// ("qwen3-vl-plus", "glm-4.6v", "deepseek-v4-flash-vision-exp"), so a custom or
|
||||
// freshly released model still gets image input instead of silently dropping it.
|
||||
//
|
||||
// Only ever turns vision ON. Never used to turn a declared capability off.
|
||||
|
||||
const SEP = "[-_/:.]";
|
||||
|
||||
// Image GENERATION, video generation, and non-chat models also carry these
|
||||
// words but take no image input — checked first so they can never match.
|
||||
const NOT_VISION = new RegExp(
|
||||
[
|
||||
`(^|${SEP})(image|img)(${SEP}|$)`,
|
||||
"stable-image", "gen[0-9]_image", "nanobanana", "imagine",
|
||||
"t2v", "i2v", "flux", "dall", "sdxl", "diffusion",
|
||||
"embed", "rerank", "guard", "moderation",
|
||||
"tts", "stt", "whisper", "voice", "speech", "audio",
|
||||
].join("|"),
|
||||
"i"
|
||||
);
|
||||
|
||||
// Explicit modality words, plus the "<digit>v" suffix vendors use for vision
|
||||
// variants (glm-4.6v, glm-5v-turbo). The digit-v branch requires a dotted
|
||||
// version so the never-shipped `gpt-4v` cannot match.
|
||||
const VISION_NAME = new RegExp(
|
||||
[
|
||||
`(^|${SEP})(vision|vl|vlm|multimodal|omni|visual)(${SEP}|$)`,
|
||||
`[0-9]\\.[0-9]+v(${SEP}|$)`,
|
||||
`(^|${SEP})glm-[0-9]+v(${SEP}|$)`,
|
||||
"(^|[-_/:.])(llava|pixtral|internvl|cogvlm|minicpm-v|moondream|idefics|fuyu)",
|
||||
].join("|"),
|
||||
"i"
|
||||
);
|
||||
|
||||
// Does this model id look like a vision model? Name signal only.
|
||||
export function looksLikeVisionModel(modelId) {
|
||||
if (!modelId) return false;
|
||||
const id = String(modelId).toLowerCase();
|
||||
if (NOT_VISION.test(id)) return false;
|
||||
return VISION_NAME.test(id);
|
||||
}
|
||||
Reference in new issue
Block a user