OpenRouter 模型能力声明 & 下游识别指南

报告日期 2026-06-02 · 数据基于 GET https://openrouter.ai/api/v1/models 实时抓取(342 条目) · 涵盖 OMP(oh-my-pi)和 Pi-Mono(~/GitHub/pi-mono)源码

目录

1. OpenRouter 是怎么声明模型能力的

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[] 声明 toolstool_choicereasoningstructured_outputs 等能力。判断能否用工具调用靠这个。
pricing 对象 包含 audioimageinput_audio_cacheimage_token 等按模态单独计价的子项。

关键事实:OpenRouter 的 schema 里 output_modalities: "transcription" 这个枚举值(用来描述纯转写模型),但实际目录里 342 个模型中没有任何一个使用它。ASR 在 OpenRouter 是按"支持 audio 输入的 chat 模型"来暴露的,输出是 "text"。这是下游最容易踩坑的地方。

1.1 三种典型模型的实际样本

直接拉取的真实数据:

// 千问 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"
  }
}

2. 当前 OpenRouter 目录中音频输入模型的真实样貌

对 342 个模型做交叉统计:

input_modalitiesoutput_modalities模型数
texttext169
text, image, filetext60
text, imagetext49
text, image, videotext27
audio, file, image, text, videotext13
file, texttext8
.........
text, imageimage, text3(图像生成)
text, imageaudio, text2(TTS)
audio, textaudio, text2(语音对话)

纯音频输入(不含 image/video)的只有:

报告提到的"千问 3 ASR"在本次抓取的 342 个模型中没有出现。如果你那边的 /v1/models 能拿到,要么是不同时间点的快照(OpenRouter 持续更新),要么走的是另一个 baseUrl。判断方法不变:以 input_modalities 是否包含 "audio" 为准。

3. 视觉/ASR 的判别规则

3.1 视觉(图片理解)

3.2 ASR(语音转文字)

3.3 区分速查表

能力判定字段判定值
视觉理解(看图)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"

4. OMP(packages/ai)如何处理

4.1 抓取路径

OpenRouter 在 OMP 中由 packages/ai/src/provider-models/openai-compat.tsopenrouterModelManagerOptions() 注册,挂在 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

4.2 过滤与映射(关键代码)

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 省略
          };
        },
      }),
  };
}

两处决定下游能不能区分模型:

  1. filterModelsupported_parameters.includes("tools") 过滤。这把所有 ASR-only 模型(如 Voxtral、Whisper)直接扔掉了,因为它们通常不声明 tools。下游在 OMP 提供的 OpenRouter 模型列表里看不到任何 ASR 模型

  2. input 字段用 modality.includes("image") 判别,是字符串子串匹配,不是结构化字段判断。architecture.modality 是 OpenRouter 给人看的字符串,碰巧一直含 "image",所以能 work,但本质上脆弱。

4.3 OMP 的 Model 类型不支持 audio

packages/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"。也就是说:

4.4 视觉守卫:图片在不支持时被替换成占位文本

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 的判定可能与上游不一致:

5. Pi-Mono(~/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.tsapi: "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") 走"降级为占位文本"或"原样转发"。

6. 结论与对下游应用的建议

6.1 OpenRouter 怎么声明(事实)

6.2 OMP / Pi-Mono 怎么识别(事实)

6.3 风险点

风险影响
字符串子串匹配 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 提前扣掉,下游拿不到图像内容。

6.4 下游应当怎么实现(建议)

  1. 不要相信 OMP/Pi-Mono 给出的 model.input直接对 OpenRouter /v1/models 跑一遍,用 input_modalities 数组做权威判定。
  2. 下游按"四类"建索引:texttext+imagetext+audio(ASR)、text+image+audio+...(全模态)。
  3. 判定 ASR:input_modalities.includes("audio") && output_modalities.includes("text")
  4. 判定视觉:input_modalities.includes("image") && output_modalities.includes("text") && !output_modalities.includes("image")。后者用来排除"图像生成"模型。
  5. 判定工具调用:supported_parameters.includes("tools")(这条 OpenRouter 语义清晰,照用即可)。
  6. 把这份分类缓存下来;不要每次发请求时再去打 OpenRouter。

附录:完整参考链接