Read linked issue #843 and PR details.
UnderstandReviewed relevant source files.
UnderstandChecked changelogs.
UnderstandAdded changelog entry.
EditResolved conflicts and fixed test file.
EditYou are given one or more GitHub PR URLs: https://github.com/badlogic/pi-mono/pull/859
For each PR URL, do the following in order:
packages/*/CHANGELOG.md files. Report whether an entry exists. If missing, state that a changelog entry is required before merge and that you will add it if the user decides to merge. Follow the changelog format rules in AGENTS.md. Verify:
### Breaking Changes, ### Added, ### Fixed, etc.)Fixed foo ([#123](https://github.com/badlogic/pi-mono/pull/123) by [@user](https://github.com/user))### Breaking Changes, not just ### FixedOutput format per PR: PR: Changelog:
If no issues are found, say so under Bad and Ugly.
>cd /Users/badlogic/workspaces/pi-mono && gh pr view 859 --json title,body,comments,commits,files,author,state,labels,reviews{"author":{"id":"U_kgDODeHOww","is_bot":false,"login":"v01dpr1mr0s3","name":"jake"},"body":"**What this is**\r\n\r\nFollowing #843 , this adds support for OpenRouter provider selection as described in the issue.\r\n\r\n**What it does**\r\n\r\nThese changes allow *custom* models to specify which upstream providers OpenRouter should route requests to via the `openRouterRouting` field in model definitions. Which model/provider combinations to use is completely up to you.\r\n\r\n**Why it is so incomplete**\r\n\r\n1. I wanted to keep it minimal.\r\n2. Nobody else asked for it yet.\r\n3. Probably nobody needs it aside from me and other secret OpenRouter provider sommeliers.\r\n4. It's a tiny hint at how OpenRouter provider routing support could be extended in the future, if need be.\r\n\r\n**Limitations**\r\n\r\n⚠️ This is specifically designed to handle {one OR model} x {one provider} binding. No load balancing, priority selection or other OR features.\r\n\r\nThe idea is that for maintaining quality of inference and cache intact, you should stick to the single provider anyway, and routing between them is not desired.\r\n\r\n**How it works**\r\n\r\nThe only introduced OpenRouter provider payload fields are:\r\n- `only`: list of provider slugs to exclusively use\r\n- `order`: list of provider slugs to try in order\r\n\r\nBoth of these should be used in tandem to ensure provider selection. In my testing, sometimes OpenRouter ignores the `only` parameter alone, but works when `only` *and* `order` are present.\r\n\r\nSo, the idea is that you:\r\n- ask pi to create a custom openrouter config of your favorite model\r\n- it will live in `~/.pi/models.json`\r\n- it will look like this:\r\n\r\n<img width=\"784\" height=\"409\" alt=\"Screenshot 2026-01-19 at 19 41 43\" src=\"https://github.com/user-attachments/assets/f1f6c4d1-25cf-413c-8d6b-424a63bfaa83\" />\r\n\r\n... and the rest of the infrastructure would be handled by pi. It should correctly identify if you have OpenRouter API key for a model, it should work with /scoped-models and so on.\r\n\r\nThe only funky part of this code is the detection of a model by `baseUrl` and not `provider`, **but** it's intentional: it allows to create custom \"virtually provided models\" with openrouter-bedrock, openrouter-novita-fp8 etc labels in the UI and they all get the routing and API key pickup seamlessly.\r\n","comments":[{"id":"IC_kwDOPbFNk87gs49Z","author":{"login":"github-actions"},"authorAssociation":"CONTRIBUTOR","body":"Hi @v01dpr1mr0s3, thanks for your interest in contributing!\n\nWe ask new contributors to open an issue first before submitting a PR. This helps us discuss the approach and avoid wasted effort.\n\n**Next steps:**\n1. Open an issue describing what you want to change and why (keep it concise, write in your human voice, AI slop will be closed)\n2. Once a maintainer approves with `lgtm`, you'll be added to the approved contributors list\n3. Then you can submit your PR\n\nThis PR will be closed automatically. See https://github.com/badlogic/pi-mono/blob/main/CONTRIBUTING.md for more details.","createdAt":"2026-01-19T19:43:19Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/859#issuecomment-3769864025","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87gtLU1","author":{"login":"v01dpr1mr0s3"},"authorAssociation":"NONE","body":"> Once a maintainer approves with lgtm, you'll be added to the approved contributors list\r\n\r\n> badlogic 6 hours ago\r\n> lgtm \r\n\r\n!","createdAt":"2026-01-19T20:03:08Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/pull/859#issuecomment-3769939253","viewerDidAuthor":false}],"commits":[{"authoredDate":"2026-01-19T18:39:04Z","authors":[{"email":"[REDACTED]","id":"U_kgDODeHOww","login":"v01dpr1mr0s3","name":"jake"}],"committedDate":"2026-01-20T16:42:03Z","messageBody":"Allows custom models to specify which upstream providers OpenRouter\nshould route requests to via the `openRouterRouting` field in model\ndefinitions.\n\nSupported fields:\n- `only`: list of provider slugs to exclusively use\n- `order`: list of provider slugs to try in order","messageHeadline":"feat(ai): add OpenRouter provider routing support","oid":"43f4be6104e01d37b8239a85f1ed846e2cb20e8b"}],"files":[{"path":"packages/ai/src/providers/openai-completions.ts","additions":7,"deletions":0},{"path":"packages/ai/src/types.ts","additions":14,"deletions":0},{"path":"packages/coding-agent/src/core/model-registry.ts","additions":7,"deletions":0}],"labels":[],"reviews":[],"state":"OPEN","title":"feat(ai): add OpenRouter provider routing support"}>cd /Users/badlogic/workspaces/pi-mono && gh pr diff 859diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts
index 18abbe421..1a28f2067 100644
--- a/packages/ai/src/providers/openai-completions.ts
+++ b/packages/ai/src/providers/openai-completions.ts
@@ -413,6 +413,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;
}
@@ -728,6 +733,7 @@ function detectCompat(model: Model<"openai-completions">): Required<OpenAIComple
requiresThinkingAsText: isMistral,
requiresMistralToolIds: isMistral,
thinkingFormat: isZai ? "zai" : "openai",
+ openRouterRouting: {},
};
}
@@ -751,5 +757,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 ?? {},
};
}
diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts
index d64f3fd1f..d4019da96 100644
--- a/packages/ai/src/types.ts
+++ b/packages/ai/src/types.ts
@@ -232,6 +232,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. */
@@ -239,6 +241,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;
diff --git a/packages/coding-agent/src/core/model-registry.ts b/packages/coding-agent/src/core/model-registry.ts
index 5ccec0d75..e284267d7 100644
--- a/packages/coding-agent/src/core/model-registry.ts
+++ b/packages/coding-agent/src/core/model-registry.ts
@@ -19,12 +19,19 @@ import type { AuthStorage } from "./auth-storage.js";
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
const OpenAICompletionsCompatSchema = Type.Object({
supportsStore: Type.Optional(Type.Boolean()),
supportsDeveloperRole: Type.Optional(Type.Boolean()),
supportsReasoningEffort: Type.Optional(Type.Boolean()),
maxTokensField: Type.Optional(Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")])),
+ openRouterRouting: Type.Optional(OpenRouterRoutingSchema),
});
const OpenAIResponsesCompatSchema = Type.Object({Now let me read issue #843 that's referenced in the PR:
>cd /Users/badlogic/workspaces/pi-mono && gh issue view 843 --json title,body,comments,labels,state{"body":"### What do you want to change?\n\nOpenRouter by default supports auto-routing your requests across multiple providers available for a model ([relevant docs here](https://openrouter.ai/docs/guides/routing/provider-selection)). Their idea is that they will route your request to the best providers available at any given time. It would be nice to be able to configure in pi through exactly which providers a request to a given model would go.\n\n### Why?\n\nFor some models like GLM, there are multiple providers available (for example, look at [GLM 4.7's providers page](https://openrouter.ai/z-ai/glm-4.7/providers)), but their quality differs vastly. Even for Anthropic models there are options of Anthropic, Google Vertex or Bedrock inference. Three main considerations: \n\na) cache hits and misses\n\nI didn't check it by hand, but I think it's obvious that caching is non-transferable between providers; so if you have a long context with Opus 4.5 via OpenRouter via Anthropic, and then OR decides to switch you to Bedrock for a request or two, then all your cache buildup goes bust and you pay in full.\n\nb) quantization\n\nOR's auto-routing doesn't care about quantization and there's always a chance for your next request to be auto-routed to an FP4 version of a model instead of FP8 that you were working with previously, without you knowing about it at all.\n\nc) service quality\n\nSome providers have more issues with rate limiting, inference speed or responding to a request at all, would be nice to be able to avoid some of them explicitly on a per-model basis.\n\n### How? (optional)\n\n_No response_","comments":[{"id":"IC_kwDOPbFNk87gm7Tm","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"The solution to this is let you specify the providers field as per their docs in `models.json` and for `openai-completions.ts` to send that along if the active model has that field set. Need to think about what that field would be called inside a model definition in models.json, and how it is exposed in packages/ai/src/types.ts in the Model type definition. Also needs to be part of the model-registry.ts validation somehow (don't need to validate, just not explode if it exists, the provider implementation will know how to handle it).\n\nI currently don't have time nor a need to work on this. If you can create a clean and tested PR that you fully understand, I'll happily merge it. Create a draft PR and we can iterate on it.\n\nlgtm","createdAt":"2026-01-19T13:18:57Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[{"content":"THUMBS_UP","users":{"totalCount":1}}],"url":"https://github.com/badlogic/pi-mono/issues/843#issuecomment-3768300774","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87gs6RK","author":{"login":"v01dpr1mr0s3"},"authorAssociation":"NONE","body":"Apologies if I'm not doing it correctly here, but I made a PR for that and, well, there it is.","createdAt":"2026-01-19T19:44:48Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/843#issuecomment-3769869386","viewerDidAuthor":false}],"labels":[],"state":"CLOSED","title":"Allow inference provider selection/filtering for OpenRouter models"}Now let me read the relevant source files to fully understand the implementation:
Now let me check the changelogs:
Now let me check the documentation that might need updates:
Now I have all the relevant context. Let me check if there's any related documentation that should be updated:
>cd /Users/badlogic/workspaces/pi-mono/packages/coding-agent && grep -r "openRouter" docs/ 2>/dev/null || echo "No mentions found"No mentions found>cd /Users/badlogic/workspaces/pi-mono/packages/ai && grep -r "openRouterRouting" test/ 2>/dev/null || echo "No mentions found"No mentions foundLet me verify the current state of the types by reading the end of the openai-completions file to understand the full context of the changes:
Now I have the complete picture. Here's my review:
PR: https://github.com/badlogic/pi-mono/pull/859
Changelog:
packages/ai/CHANGELOG.md before merge. The entry should be:
### Added under ## [Unreleased]Added OpenRouter provider routing support for custom models via \openRouterRouting` compat field (#859 by @v01dpr1mr0s3)`Good:
openRouterRouting is added to OpenAICompletionsCompat which is the right home for OpenAI-compatible API quirksmodel.baseUrl.includes("openrouter.ai")) rather than provider name, which correctly handles custom "virtual providers" with OpenRouter-specific labels (as the PR author notes)model-registry.ts updated to allow the new field without explodingonly and order together aligns with the author's testing findings about OpenRouter ignoring only aloneBad:
openRouterRouting in getCompat() return object: In openai-completions.ts, the detectCompat() function adds openRouterRouting: {} at line 733, and getCompat() adds openRouterRouting: model.compat.openRouterRouting ?? {} at line 757, but the PR diff shows these are being added. However, looking at the full file, the getCompat() function is missing the openRouterRouting merge logic. Wait, looking again at the diff, it does add line 757. This looks correct.provider field is correctly added to the params when openRouterRouting is set.as any: Line 418 uses (params as any).provider = model.compat.openRouterRouting; which bypasses TypeScript type checking. This is acceptable given OpenAI's SDK types don't include OpenRouter-specific fields.Ugly:
Questions or Assumptions:
provider field format matches what's described in their docs. The PR author has tested this manually.openRouterRouting is set on a non-OpenRouter model? The baseUrl check guards against this, but the compat field could still be set incorrectly. Not a real concern given this is for custom models where the user controls the config.Change summary:
packages/ai/src/types.ts: Added OpenRouterRouting interface with only and order fields. Added openRouterRouting optional field to OpenAICompletionsCompat.packages/ai/src/providers/openai-completions.ts: Added logic in buildParams() to inject provider field into request params when model uses OpenRouter and has openRouterRouting set. Added openRouterRouting to detectCompat() defaults and getCompat() merge logic.packages/coding-agent/src/core/model-registry.ts: Added TypeBox schema for OpenRouterRouting and added it to OpenAICompletionsCompatSchema for validation.Tests:
packages/ai/test/ to verify the routing parameters are correctly passed to OpenRouter requests.Documentation:
packages/coding-agent/README.md does not need modification. The existing "Custom Models and Providers" section covers the compat field, and this PR adds a new sub-field. However, documenting the openRouterRouting field would be beneficial for discoverability. A brief mention in the compat field section would help users find this feature.fuck it merge it via gh cli,
>cd /Users/badlogic/workspaces/pi-mono && git fetch origin main && git checkout main && git pull origin mainFrom https://github.com/badlogic/pi-mono
* branch main -> FETCH_HEAD
Already on 'main'
Your branch is up to date with 'origin/main'.
From https://github.com/badlogic/pi-mono
* branch main -> FETCH_HEAD
Already up to date.>cd /Users/badlogic/workspaces/pi-mono && git checkout -b pr-859-openrouter-routing && gh pr checkout 859 --forceSwitched to a new branch 'pr-859-openrouter-routing'
From https://github.com/badlogic/pi-mono
* [new ref] refs/pull/859/head -> v01dpr1mr0s3/main
Switched to branch 'v01dpr1mr0s3/main'>cd /Users/badlogic/workspaces/pi-mono && git rebase mainRebasing (1/1)
Auto-merging packages/ai/src/providers/openai-completions.ts
Auto-merging packages/ai/src/types.ts
Auto-merging packages/coding-agent/src/core/model-registry.ts
CONFLICT (content): Merge conflict in packages/coding-agent/src/core/model-registry.ts
error: could not apply 43f4be61... feat(ai): add OpenRouter provider routing support
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Could not apply 43f4be61... # feat(ai): add OpenRouter provider routing support
Command exited with code 1I see the conflict. The PR was based on an older version that didn't have the newer compat fields. Let me resolve it:
manually went in and resolved, check if correct
Looks correct. The conflict is resolved properly: all the existing fields are kept (requiresToolResultName, requiresAssistantAfterToolResult, etc.) and openRouterRouting is added at the end.
>cd /Users/badlogic/workspaces/pi-mono && git add packages/coding-agent/src/core/model-registry.ts && git rebase --continue[detached HEAD 5bbdf648] feat(ai): add OpenRouter provider routing support
Author: jake <[REDACTED]>
3 files changed, 28 insertions(+)
Successfully rebased and updated refs/heads/v01dpr1mr0s3/main.>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 432 files in 274ms. No fixes applied.
packages/ai/test/openai-completions-tool-result-images.test.ts(22,7): error TS2741: Property 'openRouterRouting' is missing in type '{ supportsStore: true; supportsDeveloperRole: true; supportsReasoningEffort: true; supportsUsageInStreaming: true; maxTokensField: "max_completion_tokens"; requiresToolResultName: false; requiresAssistantAfterToolResult: false; requiresThinkingAsText: false; requiresMistralToolIds: false; thinkingFormat: "openai"; }' but required in type 'Required<OpenAICompletionsCompat>'.
Command exited with code 2There's a type error. The test file needs openRouterRouting added to the compat object:
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 432 files in 265ms. No fixes applied.
> @mariozechner/pi-web-ui@0.49.3 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 37ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.Now let me add the changelog entry:
>cd /Users/badlogic/workspaces/pi-mono && git add packages/ai/CHANGELOG.md packages/ai/test/openai-completions-tool-result-images.test.ts && git commit --amend --no-editRunning formatting, linting, and type checking...
> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 432 files in 267ms. No fixes applied.
> @mariozechner/pi-web-ui@0.49.3 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 37ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.
✅ All pre-commit checks passed!
[v01dpr1mr0s3/main dac7474d] feat(ai): add OpenRouter provider routing support
Author: jake <[REDACTED]>
Date: Mon Jan 19 20:39:04 2026 +0200
5 files changed, 30 insertions(+)>cd /Users/badlogic/workspaces/pi-mono && git checkout main && git merge v01dpr1mr0s3/main --no-ff -m "feat(ai): add OpenRouter provider routing support (#859)"Switched to branch 'main'
Your branch is up to date with 'origin/main'.
Merge made by the 'ort' strategy.
packages/ai/CHANGELOG.md | 1 +
packages/ai/src/providers/openai-completions.ts | 7 +++++++
packages/ai/src/types.ts | 14 ++++++++++++++
.../ai/test/openai-completions-tool-result-images.test.ts | 1 +
packages/coding-agent/src/core/model-registry.ts | 7 +++++++
5 files changed, 30 insertions(+)>cd /Users/badlogic/workspaces/pi-mono && git push origin mainTo https://github.com/badlogic/pi-mono.git
a6d878e8..3635e45f main -> main>cd /Users/badlogic/workspaces/pi-mono && gh pr close 859 --comment "Merged via rebase onto main with changelog entry added. Thanks @v01dpr1mr0s3!"✓ Closed pull request badlogic/pi-mono#859 (feat(ai): add OpenRouter provider routing support)>cd /Users/badlogic/workspaces/pi-mono && git branch -D v01dpr1mr0s3/main pr-859-openrouter-routing 2>/dev/null; trueDeleted branch v01dpr1mr0s3/main (was dac7474d).
Deleted branch pr-859-openrouter-routing (was a6d878e8).Done. PR #859 has been:
openRouterRouting field in the test filepackages/ai/CHANGELOG.md>git statusOn branch main
Your branch is behind 'origin/main' by 7 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
Untracked files:
(use "git add <file>..." to include in what will be committed)
.pi/settings.json
nothing added to commit but untracked files present (use "git add" to track)