packages/ai)如何处理~/GitHub/pi-mono)如何处理OpenRouter /v1/models 端点对每个模型返回一个 architecture 子对象,里面有三个并列字段,分别从不同粒度声明能力:
| 字段 | 类型 | 作用 |
|---|---|---|
architecture.input_modalities |
string[] |
输入能接受的模态,枚举 text | image | file | audio | video。最可靠。 |
architecture.output_modalities |
string[] |
输出能产出的模态,枚举 text | image | embeddings | audio | video | rerank | speech | transcription。 |
architecture.modality |
string | null |
人类可读的 "input→output" 描述,比如 "text+image+video->text",是给 UI 展示用的字符串,不是结构化字段。 |
supported_parameters |
string[] |
声明 tools、tool_choice、reasoning、structured_outputs 等能力。判断能否用工具调用靠这个。 |
pricing |
对象 | 包含 audio、image、input_audio_cache、image_token 等按模态单独计价的子项。 |
关键事实:OpenRouter 的 schema 里 有 output_modalities: "transcription" 这个枚举值(用来描述纯转写模型),但实际目录里 342 个模型中没有任何一个使用它。ASR 在 OpenRouter 是按"支持 audio 输入的 chat 模型"来暴露的,输出是 "text"。这是下游最容易踩坑的地方。
直接拉取的真实数据:
// 千问 3.6(多模态,文本 + 图像 + 视频输入)
{
"id": "qwen/qwen3.6-plus",
"architecture": {
"modality": "text+image+video->text",
"input_modalities": ["text", "image", "video"],
"output_modalities": ["text"],
"tokenizer": "Qwen3"
},
"supported_parameters": ["tools", "tool_choice", "reasoning", ...]
}
// MiniMax M2.7(纯文本)
{
"id": "minimax/minimax-m2.7",
"architecture": {
"modality": "text->text",
"input_modalities": ["text"],
"output_modalities": ["text"],
"tokenizer": "Other"
},
"supported_parameters": ["tools", "tool_choice", "reasoning", ...]
}
// 千问 3.6 Flash(同样是多模态)
{
"id": "qwen/qwen3.6-flash",
"architecture": {
"modality": "text+image+video->text",
"input_modalities": ["text", "image", "video"],
"output_modalities": ["text"],
"tokenizer": "Qwen3"
}
}
对 342 个模型做交叉统计:
| input_modalities | output_modalities | 模型数 |
|---|---|---|
text | text | 169 |
text, image, file | text | 60 |
text, image | text | 49 |
text, image, video | text | 27 |
audio, file, image, text, video | text | 13 |
file, text | text | 8 |
| ... | ... | ... |
text, image | image, text | 3(图像生成) |
text, image | audio, text | 2(TTS) |
audio, text | audio, text | 2(语音对话) |
纯音频输入(不含 image/video)的只有:
openai/gpt-audio · openai/gpt-audio-mini(text, audio ↔ text, audio,语音对话)mistralai/voxtral-small-24b-2507(text, audio, file → text,典型 ASR + chat 混合)报告提到的"千问 3 ASR"在本次抓取的 342 个模型中没有出现。如果你那边的 /v1/models 能拿到,要么是不同时间点的快照(OpenRouter 持续更新),要么走的是另一个 baseUrl。判断方法不变:以 input_modalities 是否包含 "audio" 为准。
input_modalities 包含 "image"。["text"](少数图像生成/编辑模型的 output_modalities 含 "image",那不是视觉理解)。"image" 来,不需要看 supported_parameters。input_modalities 包含 "audio"。["text"](不是 "transcription")。output_modalities 含 "audio" → 能说话;只有 ["text"] → 纯 ASR 或语音聊天但只回文字。| 能力 | 判定字段 | 判定值 |
|---|---|---|
| 视觉理解(看图) | input_modalities | 含 "image",且 output_modalities = ["text"] |
| ASR(语音→文字) | input_modalities | 含 "audio",且 output_modalities = ["text"] |
| 语音对话(双向) | input_modalities + output_modalities | 都含 "audio" |
| 图像生成 | output_modalities | 含 "image" |
| TTS(文字→语音) | output_modalities | 含 "audio" |
| 嵌入/向量 | output_modalities | 含 "embeddings" |
| 重排序 | output_modalities | 含 "rerank" |
| 工具调用 | supported_parameters | 含 "tools" |
| 推理/思考 | supported_parameters | 含 "reasoning" |
packages/ai)如何处理OpenRouter 在 OMP 中由 packages/ai/src/provider-models/openai-compat.ts 的 openrouterModelManagerOptions() 注册,挂在 descriptors.ts 的 OpenRouter catalog descriptor 下:
// packages/ai/src/provider-models/descriptors.ts:216-221
catalogDescriptor(
"openrouter",
"openai/gpt-5.4",
config => openrouterModelManagerOptions(config),
catalog("OpenRouter", ["OPENROUTER_API_KEY"], { allowUnauthenticated: true }),
),
实际抓取走通用 OpenAI 兼容 fetchOpenAICompatibleModels()(packages/ai/src/utils/discovery/openai-compatible.ts),即 GET {baseUrl}/models。
packages/ai/src/provider-models/openai-compat.ts:1349-1401:
export function openrouterModelManagerOptions(
config?: OpenRouterModelManagerConfig,
): ModelManagerOptions<"openai-completions"> {
const apiKey = config?.apiKey;
const baseUrl = config?.baseUrl ?? "https://openrouter.ai/api/v1";
return {
providerId: "openrouter",
fetchDynamicModels: () =>
fetchOpenAICompatibleModels({
api: "openai-completions",
provider: "openrouter",
baseUrl,
apiKey,
filterModel: (entry) => {
const params = entry.supported_parameters;
return Array.isArray(params) && params.includes("tools");
},
mapModel: (entry, defaults) => {
const pricing = entry.pricing;
const params = entry.supported_parameters ?? [];
const modality = String(entry.architecture?.modality ?? "");
// ...
return {
...defaults,
reasoning: params.includes("reasoning"),
input: modality.includes("image") ? ["text", "image"] : ["text"],
// pricing、contextWindow、maxTokens 省略
};
},
}),
};
}
两处决定下游能不能区分模型:
filterModel 用 supported_parameters.includes("tools") 过滤。这把所有 ASR-only 模型(如 Voxtral、Whisper)直接扔掉了,因为它们通常不声明 tools。下游在 OMP 提供的 OpenRouter 模型列表里看不到任何 ASR 模型。
input 字段用 modality.includes("image") 判别,是字符串子串匹配,不是结构化字段判断。architecture.modality 是 OpenRouter 给人看的字符串,碰巧一直含 "image",所以能 work,但本质上脆弱。
Model 类型不支持 audiopackages/ai/src/types.ts:881-893:
export interface Model<TApi extends Api = any> {
id: string;
name: string;
api: TApi;
provider: Provider;
baseUrl: string;
reasoning: boolean;
input: ("text" | "image")[]; // ← 只有 text 和 image 两个值
cost: { ... };
contextWindow: number;
maxTokens: number;
// ...
}
OMP 的 Model.input 类型联合只有 "text" | "image",没有 "audio"、"file"、"video"。也就是说:
Model 只能区分"纯文本"和"文本+图像"两种。packages/ai/src/providers/vision-guard.ts + openai-completions.ts:1550-1577:
// 实际运行时
const supportsImages = model.input.includes("image");
const content: ChatCompletionContentPart[] = [];
let omittedImages = false;
for (const item of msg.content) {
if (item.type === "text") { content.push({ type: "text", text: item.text }); continue; }
if (item.type === "image") {
if (supportsImages) {
content.push({ type: "image_url", image_url: { url: dataUrl, ... } });
} else {
omittedImages = true; // ← 走占位文本分支
content.push({ type: "text", text: NON_VISION_IMAGE_PLACEHOLDER });
}
}
}
也就是说:OMP 不是按 OpenRouter 的能力去路由/拒绝,而是按"我以为这个模型能不能看图"去发请求:能看就发 image_url,不能看就把图片块替换成 "[image omitted: model does not support vision]" 文本,让模型收到"曾经有图但被我扣了"的提示。
在 OpenRouter 的场景下,OMP 的判定可能与上游不一致:
modality 含 image)→ 发 image_url。上游能收。✅modality = "text->text")→ 发占位文本。即使上游真能看图(OpenRouter 路由到多模态后端),也会被 OMP 提前扣掉。ImageContent 类型本身不包含音频,所以 audio 块在 OMP 这一层就进不来)。~/GitHub/pi-mono)如何处理Pi-Mono 走的是离线生成 + 静态打包路线。生成脚本在 ~/GitHub/pi-mono/packages/ai/scripts/generate-models.ts:281-337:
async function fetchOpenRouterModels(): Promise<Model<any>[]> {
const response = await fetch("https://openrouter.ai/api/v1/models");
const data = await response.json();
const models: Model<any>[] = [];
for (const model of data.data) {
// 只保留支持工具的模型
if (!model.supported_parameters?.includes("tools")) continue;
// 解析输入模态
const input: ("text" | "image")[] = ["text"];
if (model.architecture?.modality?.includes("image")) {
input.push("image");
}
// pricing、contextWindow、maxTokens 解析省略
models.push({
id: model.id,
name: model.name,
api: "openai-completions",
baseUrl: "https://openrouter.ai/api/v1",
provider: "openrouter",
reasoning: model.supported_parameters?.includes("reasoning") || false,
input,
cost: { input: inputCost, output: outputCost, ... },
contextWindow: model.context_length || 4096,
maxTokens: model.top_provider?.max_completion_tokens || 4096,
});
}
return models;
}
输出到 models.generated.ts(api: "openai-completions",静态打进二进制),运行期不再调 /v1/models。
Pi-Mono 的运行时 Model 类型(packages/ai/src/types.ts:546-576)也是:
export interface Model<TApi extends Api> {
// ...
reasoning: boolean;
input: ("text" | "image")[]; // ← 同样只有 text 和 image
// ...
}
下游路由逻辑:~/GitHub/pi-mono/packages/ai/src/providers/transform-messages.ts:35-55 把图片按 model.input.includes("image") 走"降级为占位文本"或"原样转发"。
input_modalities 含 "image",output_modalities = ["text"]。input_modalities 含 "audio",output_modalities = ["text"]。没有 "transcription" 枚举值被实际使用。input_modalities = ["text"],output_modalities = ["text"]。architecture.modality 是给人看的字符串,不要拿来做能力判断,应该用结构化的 input_modalities / output_modalities 数组。model.architecture.modality.includes("image"),依赖 modality 这个非结构化字符串恰好含 "image"。supported_parameters.includes("tools") 过滤,把所有 ASR-only 模型从目录里拿掉。Model.input 类型都只有 "text" | "image",audio 能力在 OMP/Pi-Mono 模型层完全不可见。| 风险 | 影响 |
|---|---|
字符串子串匹配 modality.includes("image") |
如果 OpenRouter 未来给 modality 字段引入新写法(例如带下划线或缩写),OMP 和 Pi-Mono 的视觉判定会失效。应当改用 input_modalities.includes("image")。 |
用 tools 过滤导致 ASR 模型被丢弃 |
下游完全无法通过 OMP 拿到任何 ASR 模型的元信息。要 ASR 得自己调 /v1/models 并按 input_modalities.includes("audio") 抓。 |
Model.input 没有 audio 槽位 |
即使拿到了 ASR 模型,OMP 也存不下这个能力;只能塞进 input: ["text"] 假装是纯文本模型,把 audio 输入当作不可处理的块。 |
| 图片被替换成占位文本 | 不报错也不走重试:上游真能看图也被 OMP 提前扣掉,下游拿不到图像内容。 |
model.input,直接对 OpenRouter /v1/models 跑一遍,用 input_modalities 数组做权威判定。text、text+image、text+audio(ASR)、text+image+audio+...(全模态)。input_modalities.includes("audio") && output_modalities.includes("text")。input_modalities.includes("image") && output_modalities.includes("text") && !output_modalities.includes("image")。后者用来排除"图像生成"模型。supported_parameters.includes("tools")(这条 OpenRouter 语义清晰,照用即可)。output_modalities 查询参数):https://openrouter.ai/docs/guides/overview/modelspackages/ai/src/provider-models/openai-compat.ts:1349-1401 · OpenRouter resolverpackages/ai/src/provider-models/descriptors.ts:216-221 · OpenRouter 注册packages/ai/src/utils/discovery/openai-compatible.ts:107-183 · 通用 fetchpackages/ai/src/types.ts:881-893 · Model.input 类型packages/ai/src/providers/vision-guard.ts · 视觉降级packages/ai/src/providers/openai-completions.ts:1549-1577 · 图片内容构建~/GitHub/pi-mono/packages/ai/scripts/generate-models.ts:281-337 · 静态生成~/GitHub/pi-mono/packages/ai/src/types.ts:546-576 · Model.input 类型~/GitHub/pi-mono/packages/ai/src/providers/transform-messages.ts:35-55 · 视觉降级