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 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 `createAssistantMessageEventStream()` factory function for use in extensions.
- 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;
}
// OpenRouter provider routing preferences
if (model.baseUrl.includes("openrouter.ai") && model.compat?.openRouterRouting) {
(params as any).provider = model.compat.openRouterRouting;
}
return params;
}
@ -777,6 +782,7 @@ function detectCompat(model: Model<"openai-completions">): Required<OpenAIComple
requiresThinkingAsText: isMistral,
requiresMistralToolIds: isMistral,
thinkingFormat: isZai ? "zai" : "openai",
openRouterRouting: {},
};
}
@ -800,5 +806,6 @@ function getCompat(model: Model<"openai-completions">): Required<OpenAICompletio
requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText,
requiresMistralToolIds: model.compat.requiresMistralToolIds ?? detected.requiresMistralToolIds,
thinkingFormat: model.compat.thinkingFormat ?? detected.thinkingFormat,
openRouterRouting: model.compat.openRouterRouting ?? {},
};
}

View file

@ -214,6 +214,8 @@ export interface OpenAICompletionsCompat {
requiresMistralToolIds?: boolean;
/** Format for reasoning/thinking parameter. "openai" uses reasoning_effort, "zai" uses thinking: { type: "enabled" }. Default: "openai". */
thinkingFormat?: "openai" | "zai";
/** OpenRouter-specific routing preferences. Only used when baseUrl points to OpenRouter. */
openRouterRouting?: OpenRouterRouting;
}
/** Compatibility settings for OpenAI Responses APIs. */
@ -221,6 +223,18 @@ export interface OpenAIResponsesCompat {
// 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
export interface Model<TApi extends Api> {
id: string;

View file

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