Repository navigation
Description
Summary
The Pi model export appears to normalize the OpenRouter router slug openrouter/free into openrouter-free. The exported model is then listed by Pi but cannot be invoked through the OpenCodex proxy: the upstream rejects it with HTTP 400. The canonical slug works when sent unchanged, and a combo targeting it is a viable workaround.
Environment
- OpenCodex:
2.94.0 - Client: Pi (
ocx export --client pi) - OpenRouter is configured as a provider in OpenCodex; Pi talks to the local OpenCodex proxy.
Steps to reproduce
-
Configure OpenRouter in OpenCodex and enable the OpenRouter Free Models Router (
openrouter/free). -
Confirm the live proxy model catalog exposes the router as
openrouter/openrouter/freefromGET /v1/models(provider prefix plus the full upstream model slug). -
Export the catalog for Pi using
ocx export --client pi --json. -
Observe that the exported Pi model entry is:
{ "id": "openrouter/openrouter-free", "name": "openrouter/free (openrouter)" } -
Invoke the exported entry through Pi, or send a Chat Completions request to the proxy with
model: "openrouter/openrouter-free".
Actual behavior
The request fails with HTTP 400:
{
"error": {
"message": "Provider error 400: openrouter/openrouter-free is not a valid model ID",
"type": "invalid_request_error",
"code": "invalid_request_error"
}
}The ID advertised to Pi does not identify a valid model selector at the proxy/upstream boundary.
Expected behavior
The Pi export should contain an ID which the OpenCodex proxy accepts and routes to the configured OpenRouter router. In this case the live selector openrouter/openrouter/free works when sent through the proxy; OpenRouter responds with a concrete free model (for example, liquid/lfm-2.5-2.6b:free). The returned model can vary per request, as expected for openrouter/free.
Impact
Users selecting openrouter/free in OpenCodex and exporting their catalog to Pi see the model in Pi's picker, but calls fail. This makes the advertised router unusable via the normal exported-Pi workflow. Other provider/model slugs containing nested slashes may be affected if export or model resolution treats slash-separated IDs as only one provider/model separator.
Workaround confirmed
A combo with a stable combo ID and the canonical target works:
combo/subagents-free -> openrouter/openrouter/free
The combo exports as combo/subagents-free, Pi can call it successfully, and OpenCodex logs show it resolving to openrouter/free. This is functional, but users should not need to create a one-target combo merely to work around an incorrectly exported model ID.
Suggested fix
Preserve the canonical upstream model slug when generating the Pi catalog and when resolving requests. The provider/model boundary should be parsed unambiguously (provider is openrouter; the complete upstream model ID is openrouter/free), rather than flattening or replacing the slash in the model slug. Keep the exported ID, display name, and request resolver mapping consistent.
A regression test could:
- Add a provider model whose upstream ID is
openrouter/free. - Export the Pi catalog and assert that its model ID resolves to the same upstream selector (or to an explicit stable alias mapped to it).
- Send a request using the exported ID through the proxy and assert it reaches OpenRouter successfully and returns the concrete model from the router response.
- Include nested-slash model IDs in the test matrix so the provider prefix is separated from the full provider model ID without losing path segments.
I verified that openrouter/openrouter-free fails, while the live selector openrouter/openrouter/free succeeds and returns a concrete :free model. I also verified the combo workaround above. No API keys or credentials are included here.
Activity
[-][Bug]: Pi export rewrites OpenRouter router slug into an invalid model ID[/-][+][Bug]: Pi export rewrites OpenRouter router slug `openrouter/free` into an invalid model ID[/+]github-actions commentedon Nov 4, 2026
Automated translation bookkeeping — detected language: English.
coderabbitai commentedon Nov 4, 2026
🔗 Related PRs
#7971 - fix(router): decode routed slugs via modelDisplayNames ids [open]
#7979 - fix(router): decode routed slugs via modelDisplayNames ids [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
github-actions commentedon Nov 4, 2026
Issue closed: insufficient detail
Thanks for taking the time to submit this issue. It was closed automatically because the structured report is missing information needed to evaluate or reproduce it.
Please edit the issue to add:
See the Contributing guide for details.
Once the report passes the automated checks, it will be reopened automatically unless a maintainer has changed its state.
huenchunb commentedon Nov 4, 2026
I have updated the report with exact reproduction steps and observed behavior. The fix should be scoped to Pi client export: preserve the canonical nested OpenRouter model ID so the generated catalog selector maps unambiguously to provider=openrouter and upstream model=openrouter/free. A one-target combo remains a verified workaround. I will investigate the export boundary and regression coverage in a fork.
huenchunb commentedon Nov 4, 2026
I have updated the report with exact reproduction steps and observed behavior. The fix should be scoped to Pi client export: preserve the canonical nested OpenRouter model ID so the generated catalog selector maps unambiguously to provider=openrouter and upstream model=openrouter/free. A one-target combo remains a verified workaround. I will investigate the export boundary and regression coverage in a fork.
huenchunb commentedon Nov 4, 2026
A fully structured bug report was opened as #8172 and passes the repository issue-quality validator. The proposed fix is in draft PR #8171 targeting
dev; the old issue remains closed because GitHub rejected a state reopen (422). Please track the active report at #8172.