feat(ai): add OpenRouter provider routing support

Allows custom models to specify which upstream providers OpenRouter
should route requests to via the `openRouterRouting` field in model
definitions.

Supported fields:
- `only`: list of provider slugs to exclusively use
- `order`: list of provider slugs to try in order
This commit is contained in:
jake 2026-01-19 20:39:04 +02:00 committed by Mario Zechner
parent a6d878e804
commit dac7474da2
5 changed files with 30 additions and 0 deletions

View file

@ -4,6 +4,7 @@
### Added ### Added
- Added OpenRouter provider routing support for custom models via `openRouterRouting` compat field ([#859](https://github.com/badlogic/pi-mono/pull/859) by [@v01dpr1mr0s3](https://github.com/v01dpr1mr0s3))
- Added `azure-openai-responses` provider support for Azure OpenAI Responses API. ([#890](https://github.com/badlogic/pi-mono/pull/890) by [@markusylisiurunen](https://github.com/markusylisiurunen)) - Added `azure-openai-responses` provider support for Azure OpenAI Responses API. ([#890](https://github.com/badlogic/pi-mono/pull/890) by [@markusylisiurunen](https://github.com/markusylisiurunen))
- Added `createAssistantMessageEventStream()` factory function for use in extensions. - Added `createAssistantMessageEventStream()` factory function for use in extensions.
- Added `resetApiProviders()` to clear and re-register built-in API providers. - Added `resetApiProviders()` to clear and re-register built-in API providers.

View file

@ -445,6 +445,11 @@ function buildParams(model: Model<"openai-completions">, context: Context, optio
params.reasoning_effort = options.reasoningEffort; params.reasoning_effort = options.reasoningEffort;
} }
// OpenRouter provider routing preferences
if (model.baseUrl.includes("openrouter.ai") && model.compat?.openRouterRouting) {
(params as any).provider = model.compat.openRouterRouting;
}
return params; return params;
} }
@ -777,6 +782,7 @@ function detectCompat(model: Model<"openai-completions">): Required<OpenAIComple
requiresThinkingAsText: isMistral, requiresThinkingAsText: isMistral,
requiresMistralToolIds: isMistral, requiresMistralToolIds: isMistral,
thinkingFormat: isZai ? "zai" : "openai", thinkingFormat: isZai ? "zai" : "openai",
openRouterRouting: {},
}; };
} }
@ -800,5 +806,6 @@ function getCompat(model: Model<"openai-completions">): Required<OpenAICompletio
requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText, requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText,
requiresMistralToolIds: model.compat.requiresMistralToolIds ?? detected.requiresMistralToolIds, requiresMistralToolIds: model.compat.requiresMistralToolIds ?? detected.requiresMistralToolIds,
thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat, thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
openRouterRouting: model.compat.openRouterRouting ?? {},
}; };
} }

View file

@ -214,6 +214,8 @@ export interface OpenAICompletionsCompat {
requiresMistralToolIds?: boolean; requiresMistralToolIds?: boolean;
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "zai" uses thinking: { type: "enabled" }. Default: "openai". */ /** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "zai" uses thinking: { type: "enabled" }. Default: "openai". */
thinkingFormat?: "openai" | "zai"; thinkingFormat?: "openai" | "zai";
/** OpenRouter-specific routing preferences. Only used when baseUrl points to OpenRouter. */
openRouterRouting?: OpenRouterRouting;
} }
/** Compatibility settings for OpenAI Responses APIs. */ /** Compatibility settings for OpenAI Responses APIs. */
@ -221,6 +223,18 @@ export interface OpenAIResponsesCompat {
// Reserved for future use // Reserved for future use
} }
/**
* OpenRouter provider routing preferences.
* Controls which upstream providers OpenRouter routes requests to.
* @see https://openrouter.ai/docs/provider-routing
*/
export interface OpenRouterRouting {
/** List of provider slugs to exclusively use for this request (e.g., ["amazon-bedrock", "anthropic"]). */
only?: string[];
/** List of provider slugs to try in order (e.g., ["anthropic", "openai"]). */
order?: string[];
}
// Model interface for the unified model system // Model interface for the unified model system
export interface Model<TApi extends Api> { export interface Model<TApi extends Api> {
id: string; id: string;

View file

@ -30,6 +30,7 @@ const compat: Required<OpenAICompletionsCompat> = {
requiresThinkingAsText: false, requiresThinkingAsText: false,
requiresMistralToolIds: false, requiresMistralToolIds: false,
thinkingFormat: "openai", thinkingFormat: "openai",
openRouterRouting: {},
}; };
function buildToolResult(toolCallId: string, timestamp: number): ToolResultMessage { function buildToolResult(toolCallId: string, timestamp: number): ToolResultMessage {

View file

@ -25,6 +25,12 @@ import type { AuthStorage } from "./auth-storage.js";
const Ajv = (AjvModule as any).default || AjvModule; const Ajv = (AjvModule as any).default || AjvModule;
// Schema for OpenRouter routing preferences
const OpenRouterRoutingSchema = Type.Object({
only: Type.Optional(Type.Array(Type.String())),
order: Type.Optional(Type.Array(Type.String())),
});
// Schema for OpenAI compatibility settings // Schema for OpenAI compatibility settings
const OpenAICompletionsCompatSchema = Type.Object({ const OpenAICompletionsCompatSchema = Type.Object({
supportsStore: Type.Optional(Type.Boolean()), supportsStore: Type.Optional(Type.Boolean()),
@ -37,6 +43,7 @@ const OpenAICompletionsCompatSchema = Type.Object({
requiresThinkingAsText: Type.Optional(Type.Boolean()), requiresThinkingAsText: Type.Optional(Type.Boolean()),
requiresMistralToolIds: Type.Optional(Type.Boolean()), requiresMistralToolIds: Type.Optional(Type.Boolean()),
thinkingFormat: Type.Optional(Type.Union([Type.Literal("openai"), Type.Literal("zai")])), thinkingFormat: Type.Optional(Type.Union([Type.Literal("openai"), Type.Literal("zai")])),
openRouterRouting: Type.Optional(OpenRouterRoutingSchema),
}); });
const OpenAIResponsesCompatSchema = Type.Object({ const OpenAIResponsesCompatSchema = Type.Object({