diff --git a/.changeset/openrouter-combined-models-restore.md b/.changeset/openrouter-combined-models-restore.md new file mode 100644 index 000000000..93a238e59 --- /dev/null +++ b/.changeset/openrouter-combined-models-restore.md @@ -0,0 +1,5 @@ +--- +'@tanstack/ai-openrouter': patch +--- + +Restore `OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS`, which the model-metadata sync dropped from the generated `model-meta.ts`. The set is now generated from OpenRouter's catalog (chat models whose `supported_parameters` include `structured_outputs`, `tools` and `tool_choice`) instead of being hand-maintained, so combined tools + `outputSchema` mode now covers every model OpenRouter flags as supporting it. diff --git a/docs/adapters/openrouter.md b/docs/adapters/openrouter.md index 91ffdb9dc..161b79620 100644 --- a/docs/adapters/openrouter.md +++ b/docs/adapters/openrouter.md @@ -119,15 +119,12 @@ upstream models OpenRouter can return the typed object in that same streaming request, so the engine does not make a second finalization call. That happens only when **every** model that can receive the request is in -`OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS`. The set is the upstream models -that already support this combined mode: - -- Anthropic Claude 4.5 and later -- Gemini 3.x text models -- Grok 4.x (not the multi-agent variant) -- OpenAI models from the `gpt-4o-2024-08-06` strict JSON Schema era onward - (the unpinned `openai/gpt-4o-mini` alias is included; the dated pin - `openai/gpt-4o-mini-2024-07-18` is not) +`OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS`. The set is generated from +OpenRouter's catalog on every model sync: every chat model whose +`supported_parameters` include `structured_outputs`, `tools` and `tool_choice` +(Claude 4.5+, Gemini 2.5+, GPT-4o+, Grok 4, DeepSeek V3+, Llama 3.1+, and so +on). Models OpenRouter does not flag, such as `anthropic/claude-opus-4.1`, stay +on the legacy two-call path. If any fallback in `modelOptions.models` is outside that set, OpenRouter keeps the two-call path. Routing suffixes such as `:nitro` do not change the gate. diff --git a/docs/config.json b/docs/config.json index a7325f45d..25dd02b10 100644 --- a/docs/config.json +++ b/docs/config.json @@ -884,7 +884,7 @@ "label": "OpenRouter Adapter", "to": "adapters/openrouter", "addedAt": "2026-04-15", - "updatedAt": "2026-08-19" + "updatedAt": "2026-08-20" }, { "label": "Perplexity Search", diff --git a/packages/ai-openrouter/src/model-meta.ts b/packages/ai-openrouter/src/model-meta.ts index 19da9bc28..42863cfdd 100644 --- a/packages/ai-openrouter/src/model-meta.ts +++ b/packages/ai-openrouter/src/model-meta.ts @@ -1,3 +1,5 @@ +// AUTO-GENERATED by scripts/convert-openrouter-models.ts from scripts/openrouter.models.json +// (`pnpm generate:models`). Do not edit by hand — every block here is rewritten on sync. import type { OpenRouterBaseOptions, OpenRouterCommonOptions, @@ -19619,3 +19621,305 @@ export const OPENROUTER_IMAGE_MODELS = [ OPENAI_GPT_5_IMAGE_MINI.id, OPENAI_GPT_5_4_IMAGE_2.id, ] as const + +/** + * Generated: chat models whose catalog `supported_parameters` include + * `structured_outputs`, `tools` and `tool_choice` (combined tools + outputSchema mode). + */ +export const OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS: ReadonlySet = + new Set([ + _ANTHROPIC_CLAUDE_FABLE_LATEST.id, + _ANTHROPIC_CLAUDE_HAIKU_LATEST.id, + _ANTHROPIC_CLAUDE_OPUS_LATEST.id, + _ANTHROPIC_CLAUDE_SONNET_LATEST.id, + _DEEPSEEK_DEEPSEEK_V4_FLASH_LATEST.id, + _GOOGLE_GEMINI_FLASH_LATEST.id, + _GOOGLE_GEMINI_PRO_LATEST.id, + _MOONSHOTAI_KIMI_LATEST.id, + _OPENAI_GPT_LATEST.id, + _OPENAI_GPT_MINI_LATEST.id, + _X_AI_GROK_LATEST.id, + ANTHROPIC_CLAUDE_FABLE_5.id, + ANTHROPIC_CLAUDE_FABLE_5_BATCH.id, + ANTHROPIC_CLAUDE_HAIKU_4_5.id, + ANTHROPIC_CLAUDE_HAIKU_4_5_BATCH.id, + ANTHROPIC_CLAUDE_OPUS_4_1_BATCH.id, + ANTHROPIC_CLAUDE_OPUS_4_5.id, + ANTHROPIC_CLAUDE_OPUS_4_5_BATCH.id, + ANTHROPIC_CLAUDE_OPUS_4_6.id, + ANTHROPIC_CLAUDE_OPUS_4_6_BATCH.id, + ANTHROPIC_CLAUDE_OPUS_4_7.id, + ANTHROPIC_CLAUDE_OPUS_4_7_FAST.id, + ANTHROPIC_CLAUDE_OPUS_4_7_BATCH.id, + ANTHROPIC_CLAUDE_OPUS_4_8.id, + ANTHROPIC_CLAUDE_OPUS_4_8_FAST.id, + ANTHROPIC_CLAUDE_OPUS_4_8_BATCH.id, + ANTHROPIC_CLAUDE_OPUS_5.id, + ANTHROPIC_CLAUDE_OPUS_5_FAST.id, + ANTHROPIC_CLAUDE_OPUS_5_BATCH.id, + ANTHROPIC_CLAUDE_SONNET_4_5.id, + ANTHROPIC_CLAUDE_SONNET_4_5_BATCH.id, + ANTHROPIC_CLAUDE_SONNET_4_6.id, + ANTHROPIC_CLAUDE_SONNET_4_6_BATCH.id, + ANTHROPIC_CLAUDE_SONNET_5.id, + ANTHROPIC_CLAUDE_SONNET_5_BATCH.id, + ARCEE_AI_TRINITY_LARGE_THINKING.id, + BYTEDANCE_SEED_SEED_1_6.id, + BYTEDANCE_SEED_SEED_1_6_FLASH.id, + BYTEDANCE_SEED_SEED_2_1_TURBO.id, + BYTEDANCE_SEED_SEED_2_0_CODE.id, + BYTEDANCE_SEED_SEED_2_0_LITE.id, + BYTEDANCE_SEED_SEED_2_0_MINI.id, + COHERE_COMMAND_R_08_2024.id, + COHERE_COMMAND_R_PLUS_08_2024.id, + DEEPSEEK_DEEPSEEK_CHAT.id, + DEEPSEEK_DEEPSEEK_CHAT_V3_0324.id, + DEEPSEEK_DEEPSEEK_CHAT_V3_1.id, + DEEPSEEK_DEEPSEEK_R1.id, + DEEPSEEK_DEEPSEEK_R1_0528.id, + DEEPSEEK_DEEPSEEK_V3_1_TERMINUS.id, + DEEPSEEK_DEEPSEEK_V3_2.id, + DEEPSEEK_DEEPSEEK_V3_2_EXP.id, + DEEPSEEK_DEEPSEEK_V4_FLASH.id, + DEEPSEEK_DEEPSEEK_V4_FLASH_0731.id, + DEEPSEEK_DEEPSEEK_V4_PRO.id, + DEEPSEEK_DEEPSEEK_V4_PRO_0813.id, + DOTS_STUDIO_DOTS_3_NOTE_PREVIEW_FREE.id, + GOOGLE_GEMINI_2_5_FLASH.id, + GOOGLE_GEMINI_2_5_FLASH_LITE.id, + GOOGLE_GEMINI_2_5_FLASH_LITE_BATCH.id, + GOOGLE_GEMINI_2_5_FLASH_BATCH.id, + GOOGLE_GEMINI_2_5_PRO.id, + GOOGLE_GEMINI_2_5_PRO_PREVIEW.id, + GOOGLE_GEMINI_2_5_PRO_PREVIEW_05_06.id, + GOOGLE_GEMINI_2_5_PRO_BATCH.id, + GOOGLE_GEMINI_3_FLASH_PREVIEW.id, + GOOGLE_GEMINI_3_FLASH_PREVIEW_BATCH.id, + GOOGLE_GEMINI_3_PRO_IMAGE.id, + GOOGLE_GEMINI_3_1_FLASH_LITE.id, + GOOGLE_GEMINI_3_1_FLASH_LITE_PREVIEW.id, + GOOGLE_GEMINI_3_1_FLASH_LITE_BATCH.id, + GOOGLE_GEMINI_3_1_PRO_PREVIEW.id, + GOOGLE_GEMINI_3_1_PRO_PREVIEW_CUSTOMTOOLS.id, + GOOGLE_GEMINI_3_1_PRO_PREVIEW_BATCH.id, + GOOGLE_GEMINI_3_5_FLASH.id, + GOOGLE_GEMINI_3_5_FLASH_LITE.id, + GOOGLE_GEMINI_3_5_FLASH_LITE_BATCH.id, + GOOGLE_GEMINI_3_5_FLASH_BATCH.id, + GOOGLE_GEMINI_3_6_FLASH.id, + GOOGLE_GEMINI_3_6_FLASH_BATCH.id, + GOOGLE_GEMINI_3_7_FLASH.id, + GOOGLE_GEMINI_3_7_FLASH_BATCH.id, + GOOGLE_GEMMA_3_12B_IT.id, + GOOGLE_GEMMA_3_27B_IT.id, + GOOGLE_GEMMA_4_26B_A4B_IT.id, + GOOGLE_GEMMA_4_26B_A4B_IT_FREE.id, + GOOGLE_GEMMA_4_31B_IT.id, + IBM_GRANITE_GRANITE_4_1_8B.id, + INCEPTION_MERCURY_2.id, + INCLUSIONAI_LING_2_6_1T.id, + INCLUSIONAI_LING_2_6_FLASH.id, + KWAIPILOT_KAT_CODER_AIR_V2_5.id, + KWAIPILOT_KAT_CODER_PRO_V2.id, + KWAIPILOT_KAT_CODER_PRO_V2_5.id, + LIQUID_LFM_2_5_2_6B_FREE.id, + META_LLAMA_LLAMA_3_1_70B_INSTRUCT.id, + META_LLAMA_LLAMA_3_1_8B_INSTRUCT.id, + META_LLAMA_LLAMA_3_3_70B_INSTRUCT.id, + META_LLAMA_LLAMA_4_MAVERICK.id, + META_LLAMA_LLAMA_4_SCOUT.id, + META_MUSE_GLIMMER_30B.id, + META_MUSE_SPARK_1_1.id, + META_MUSE_SPARK_1_2.id, + MINIMAX_MINIMAX_M2.id, + MINIMAX_MINIMAX_M2_5.id, + MINIMAX_MINIMAX_M2_7.id, + MINIMAX_MINIMAX_M3.id, + MINIMAX_MINIMAX_M3_BATCH.id, + MISTRALAI_CODESTRAL_2508.id, + MISTRALAI_MINISTRAL_14B_2512.id, + MISTRALAI_MINISTRAL_3B_2512.id, + MISTRALAI_MINISTRAL_8B_2512.id, + MISTRALAI_MISTRAL_LARGE.id, + MISTRALAI_MISTRAL_LARGE_2407.id, + MISTRALAI_MISTRAL_LARGE_2512.id, + MISTRALAI_MISTRAL_MEDIUM_3.id, + MISTRALAI_MISTRAL_MEDIUM_3_5.id, + MISTRALAI_MISTRAL_MEDIUM_3_1.id, + MISTRALAI_MISTRAL_NEMO.id, + MISTRALAI_MISTRAL_SABA.id, + MISTRALAI_MISTRAL_SMALL_2603.id, + MISTRALAI_MISTRAL_SMALL_3_2_24B_INSTRUCT.id, + MISTRALAI_MIXTRAL_8X22B_INSTRUCT.id, + MISTRALAI_VOXTRAL_SMALL_24B_2507.id, + MOONSHOTAI_KIMI_K2_0905.id, + MOONSHOTAI_KIMI_K2_THINKING.id, + MOONSHOTAI_KIMI_K2_5.id, + MOONSHOTAI_KIMI_K2_6.id, + MOONSHOTAI_KIMI_K2_7_CODE.id, + MOONSHOTAI_KIMI_K2_7_CODE_BATCH.id, + MOONSHOTAI_KIMI_K3.id, + NEX_AGI_NEX_N2_MINI.id, + NVIDIA_NEMOTRON_3_NANO_30B_A3B.id, + NVIDIA_NEMOTRON_3_SUPER_120B_A12B.id, + NVIDIA_NEMOTRON_3_SUPER_120B_A12B_FREE.id, + NVIDIA_NEMOTRON_3_ULTRA_550B_A55B.id, + NVIDIA_NEMOTRON_3_ULTRA_550B_A55B_BATCH.id, + NVIDIA_NEMOTRON_3_5_LIGHTNING.id, + NVIDIA_NEMOTRON_NANO_9B_V2_FREE.id, + OPENAI_GPT_3_5_TURBO.id, + OPENAI_GPT_3_5_TURBO_0613.id, + OPENAI_GPT_3_5_TURBO_16K.id, + OPENAI_GPT_3_5_TURBO_BATCH.id, + OPENAI_GPT_4.id, + OPENAI_GPT_4_TURBO.id, + OPENAI_GPT_4_TURBO_PREVIEW.id, + OPENAI_GPT_4_TURBO_BATCH.id, + OPENAI_GPT_4_1.id, + OPENAI_GPT_4_1_MINI.id, + OPENAI_GPT_4_1_MINI_BATCH.id, + OPENAI_GPT_4_1_NANO.id, + OPENAI_GPT_4_1_NANO_BATCH.id, + OPENAI_GPT_4_1_BATCH.id, + OPENAI_GPT_4O.id, + OPENAI_GPT_4O_2024_05_13.id, + OPENAI_GPT_4O_2024_08_06.id, + OPENAI_GPT_4O_2024_11_20.id, + OPENAI_GPT_4O_MINI.id, + OPENAI_GPT_4O_MINI_2024_07_18.id, + OPENAI_GPT_4O_MINI_BATCH.id, + OPENAI_GPT_4O_BATCH.id, + OPENAI_GPT_5.id, + OPENAI_GPT_5_CODEX_BATCH.id, + OPENAI_GPT_5_MINI.id, + OPENAI_GPT_5_MINI_BATCH.id, + OPENAI_GPT_5_NANO.id, + OPENAI_GPT_5_NANO_BATCH.id, + OPENAI_GPT_5_PRO.id, + OPENAI_GPT_5_PRO_BATCH.id, + OPENAI_GPT_5_BATCH.id, + OPENAI_GPT_5_1.id, + OPENAI_GPT_5_1_CODEX.id, + OPENAI_GPT_5_1_CODEX_MAX.id, + OPENAI_GPT_5_1_CODEX_MINI.id, + OPENAI_GPT_5_1_BATCH.id, + OPENAI_GPT_5_2.id, + OPENAI_GPT_5_2_CHAT.id, + OPENAI_GPT_5_2_CODEX.id, + OPENAI_GPT_5_2_PRO.id, + OPENAI_GPT_5_2_PRO_BATCH.id, + OPENAI_GPT_5_2_BATCH.id, + OPENAI_GPT_5_3_CODEX.id, + OPENAI_GPT_5_4.id, + OPENAI_GPT_5_4_MINI.id, + OPENAI_GPT_5_4_MINI_BATCH.id, + OPENAI_GPT_5_4_NANO.id, + OPENAI_GPT_5_4_NANO_BATCH.id, + OPENAI_GPT_5_4_PRO.id, + OPENAI_GPT_5_4_PRO_BATCH.id, + OPENAI_GPT_5_4_BATCH.id, + OPENAI_GPT_5_5.id, + OPENAI_GPT_5_5_PRO.id, + OPENAI_GPT_5_5_PRO_BATCH.id, + OPENAI_GPT_5_5_BATCH.id, + OPENAI_GPT_5_6_LUNA.id, + OPENAI_GPT_5_6_LUNA_PRO.id, + OPENAI_GPT_5_6_LUNA_PRO_BATCH.id, + OPENAI_GPT_5_6_LUNA_BATCH.id, + OPENAI_GPT_5_6_SOL.id, + OPENAI_GPT_5_6_SOL_PRO.id, + OPENAI_GPT_5_6_SOL_PRO_BATCH.id, + OPENAI_GPT_5_6_SOL_BATCH.id, + OPENAI_GPT_5_6_TERRA.id, + OPENAI_GPT_5_6_TERRA_PRO.id, + OPENAI_GPT_5_6_TERRA_PRO_BATCH.id, + OPENAI_GPT_5_6_TERRA_BATCH.id, + OPENAI_GPT_AUDIO.id, + OPENAI_GPT_AUDIO_MINI.id, + OPENAI_GPT_CHAT_LATEST.id, + OPENAI_GPT_OSS_120B.id, + OPENAI_GPT_OSS_20B.id, + OPENAI_GPT_OSS_20B_FREE.id, + OPENAI_GPT_OSS_SAFEGUARD_20B.id, + OPENAI_O1.id, + OPENAI_O1_BATCH.id, + OPENAI_O3.id, + OPENAI_O3_MINI.id, + OPENAI_O3_MINI_HIGH.id, + OPENAI_O3_MINI_HIGH_BATCH.id, + OPENAI_O3_MINI_BATCH.id, + OPENAI_O3_PRO.id, + OPENAI_O3_PRO_BATCH.id, + OPENAI_O3_BATCH.id, + OPENAI_O4_MINI.id, + OPENAI_O4_MINI_HIGH.id, + OPENAI_O4_MINI_HIGH_BATCH.id, + OPENAI_O4_MINI_BATCH.id, + QWEN_QWEN_2_5_72B_INSTRUCT.id, + QWEN_QWEN_2_5_7B_INSTRUCT.id, + QWEN_QWEN_PLUS.id, + QWEN_QWEN_PLUS_2025_07_28.id, + QWEN_QWEN_PLUS_2025_07_28_THINKING.id, + QWEN_QWEN3_14B.id, + QWEN_QWEN3_235B_A22B_2507.id, + QWEN_QWEN3_30B_A3B_INSTRUCT_2507.id, + QWEN_QWEN3_32B.id, + QWEN_QWEN3_CODER.id, + QWEN_QWEN3_CODER_30B_A3B_INSTRUCT.id, + QWEN_QWEN3_CODER_NEXT.id, + QWEN_QWEN3_CODER_PLUS.id, + QWEN_QWEN3_MAX.id, + QWEN_QWEN3_MAX_THINKING.id, + QWEN_QWEN3_NEXT_80B_A3B_INSTRUCT.id, + QWEN_QWEN3_NEXT_80B_A3B_THINKING.id, + QWEN_QWEN3_VL_235B_A22B_INSTRUCT.id, + QWEN_QWEN3_VL_235B_A22B_THINKING.id, + QWEN_QWEN3_VL_30B_A3B_INSTRUCT.id, + QWEN_QWEN3_VL_30B_A3B_THINKING.id, + QWEN_QWEN3_VL_32B_INSTRUCT.id, + QWEN_QWEN3_VL_8B_INSTRUCT.id, + QWEN_QWEN3_VL_8B_THINKING.id, + QWEN_QWEN3_5_122B_A10B.id, + QWEN_QWEN3_5_27B.id, + QWEN_QWEN3_5_35B_A3B.id, + QWEN_QWEN3_5_397B_A17B.id, + QWEN_QWEN3_5_9B.id, + QWEN_QWEN3_5_FLASH_02_23.id, + QWEN_QWEN3_5_PLUS_02_15.id, + QWEN_QWEN3_5_PLUS_20260420.id, + QWEN_QWEN3_6_27B.id, + QWEN_QWEN3_6_35B_A3B.id, + QWEN_QWEN3_6_FLASH.id, + QWEN_QWEN3_6_MAX_PREVIEW.id, + QWEN_QWEN3_6_PLUS.id, + QWEN_QWEN3_7_MAX.id, + QWEN_QWEN3_7_PLUS.id, + QWEN_QWEN3_8_2_4T_A95B.id, + QWEN_QWEN3_8_27B.id, + QWEN_QWEN3_8_MAX.id, + REKAAI_REKA_EDGE.id, + SAKANA_FUGU_ULTRA.id, + SAKANA_SAKANA_NAMAZU.id, + SAO10K_L3_1_EURYALE_70B.id, + STEPFUN_STEP_3_7_FLASH.id, + TENCENT_HY3.id, + THEDRUMMER_UNSLOPNEMO_12B.id, + THINKINGMACHINES_INKLING_SMALL.id, + UPSTAGE_SOLAR_PRO_3.id, + UPSTAGE_SOLAR_PRO4.id, + X_AI_GROK_4_20.id, + X_AI_GROK_4_3.id, + X_AI_GROK_4_5.id, + X_AI_GROK_4_6.id, + X_AI_GROK_BUILD_0_1.id, + XIAOMI_MIMO_V2_5.id, + XIAOMI_MIMO_V2_5_PRO.id, + Z_AI_GLM_4_6.id, + Z_AI_GLM_4_7.id, + Z_AI_GLM_4_7_FLASH.id, + Z_AI_GLM_5.id, + Z_AI_GLM_5_1.id, + Z_AI_GLM_5_2.id, + Z_AI_GLM_5_2_BATCH.id, + Z_AI_GLM_5_2_FREE.id, + ]) diff --git a/packages/ai-openrouter/tests/openrouter-combined-structured-output.test.ts b/packages/ai-openrouter/tests/openrouter-combined-structured-output.test.ts index 12a72c0f4..19c1302be 100644 --- a/packages/ai-openrouter/tests/openrouter-combined-structured-output.test.ts +++ b/packages/ai-openrouter/tests/openrouter-combined-structured-output.test.ts @@ -169,7 +169,7 @@ describe('OpenRouter combined tools + outputSchema', () => { ).toBe(true) }) - it('returns false for upstream models the upstream gate excludes', () => { + it('returns false for catalog models without structured_outputs support', () => { expect( createOpenRouterText( 'anthropic/claude-opus-4.1', @@ -178,13 +178,13 @@ describe('OpenRouter combined tools + outputSchema', () => { ).toBe(false) expect( createOpenRouterText( - 'openai/gpt-4o-2024-05-13', + 'anthropic/claude-sonnet-4', 'k', ).supportsCombinedToolsAndSchema(), ).toBe(false) expect( createOpenRouterText( - 'openai/gpt-4o-mini-2024-07-18', + 'anthropic/claude-3-haiku', 'k', ).supportsCombinedToolsAndSchema(), ).toBe(false) @@ -199,7 +199,7 @@ describe('OpenRouter combined tools + outputSchema', () => { ).toBe(true) expect( createOpenRouterResponsesText( - 'openai/gpt-4o-2024-05-13', + 'anthropic/claude-sonnet-4', 'k', ).supportsCombinedToolsAndSchema(), ).toBe(false) @@ -215,7 +215,7 @@ describe('OpenRouter combined tools + outputSchema', () => { ).toBe(true) expect( adapter.supportsCombinedToolsAndSchema({ - models: ['openai/gpt-4o-2024-05-13'], + models: ['anthropic/claude-sonnet-4'], }), ).toBe(false) }) @@ -231,7 +231,7 @@ describe('OpenRouter combined tools + outputSchema', () => { ).toBe(true) expect( openRouterSupportsCombinedToolsAndSchema('openai/gpt-4o', { - models: ['openai/gpt-4o-2024-05-13:nitro'], + models: ['anthropic/claude-sonnet-4:nitro'], }), ).toBe(false) }) @@ -303,14 +303,14 @@ describe('OpenRouter combined tools + outputSchema', () => { properties: { answer: { type: 'string' } }, required: ['answer'], }, - modelOptions: { models: ['openai/gpt-4o-2024-05-13'] }, + modelOptions: { models: ['anthropic/claude-sonnet-4'] }, })) { // drain } const params = mockChatSend.mock.calls[0]![0].chatRequest expect(params.responseFormat).toBeUndefined() - expect(params.models).toEqual(['openai/gpt-4o-2024-05-13']) + expect(params.models).toEqual(['anthropic/claude-sonnet-4']) expect(params.tools).toBeDefined() }) }) diff --git a/scripts/convert-openrouter-models.ts b/scripts/convert-openrouter-models.ts index 991449d4a..3312738b4 100644 --- a/scripts/convert-openrouter-models.ts +++ b/scripts/convert-openrouter-models.ts @@ -48,8 +48,26 @@ const perModelInputModalities: Record = {} const imageModels = new Set([]) const chatModels = new Set([]) +const combinedToolsAndSchemaModels = new Set([]) const videoModels = new Set([]) +/** + * Catalog ids whose `supported_parameters` include `structured_outputs`, `tools` + * and `tool_choice` — OpenRouter can run strict json_schema output together with + * tools in one request ("combined mode", #836). Derived from the catalog on every + * sync; never hand-edit (#1163). + */ +function generateCombinedToolsAndSchemaModelsSet(): string { + return `/** + * Generated: chat models whose catalog \`supported_parameters\` include + * \`structured_outputs\`, \`tools\` and \`tool_choice\` (combined tools + outputSchema mode). + */ +export const OPENROUTER_COMBINED_TOOLS_AND_SCHEMA_MODELS: ReadonlySet = + new Set([\n${Array.from(combinedToolsAndSchemaModels) + .map((id) => ` ${id},`) + .join('\n')}\n ])` +} + function generateChatModelsArray(): string { const modelIds = Array.from(chatModels) if (modelIds.length === 0) { @@ -199,6 +217,14 @@ function generateModelMetaString(model: OpenRouterModel): string { ) if (outputModalities.includes('text') && !isNonChat) { chatModels.add(`${constName}.id`) + const params = new Set(model.supported_parameters ?? []) + if ( + params.has('structured_outputs') && + params.has('tools') && + params.has('tool_choice') + ) { + combinedToolsAndSchemaModels.add(`${constName}.id`) + } } if (outputModalities.includes('image')) { imageModels.add(`${constName}.id`) @@ -331,7 +357,8 @@ function convertModels(sourceModels: Array): string { console.log('// Generated ModelMeta entries:') console.log('') -const file = ` +const file = `// AUTO-GENERATED by scripts/convert-openrouter-models.ts from scripts/openrouter.models.json +// (\`pnpm generate:models\`). Do not edit by hand — every block here is rewritten on sync. import type { OpenRouterBaseOptions, OpenRouterCommonOptions } from './text/text-provider-options' ${convertModels(models)} @@ -346,6 +373,8 @@ ${generateChatToolCapabilitiesType()} ${generateVideoModelsArray()} ${generateVideoModelMeta()} ${generateImageModelsArray()} + +${generateCombinedToolsAndSchemaModelsSet()} ` console.log(file) writeFile('packages/ai-openrouter/src/model-meta.ts', file).then(() => {