@riskihajar/opencode-omniroute-auth
v1.3.1
Published
OpenCode authentication plugin for OmniRoute API with /connect command and dynamic model fetching
Downloads
1,469
Maintainers
Readme
OpenCode OmniRoute Auth Plugin
OpenCode plugin for using OmniRoute as a first-class provider with:
/connect omnirouteauth flow- dynamic
/v1/modelsdiscovery - OmniRoute-specific metadata enrichment
- safer combo model capability handling
- practical Responses API compatibility work for real OpenCode usage
This package is intentionally released separately because it goes beyond a minimal fork and tracks OmniRoute/OpenCode interoperability fixes faster.
Why this package exists
If you only want a generic fork, the upstream/mainline style plugin may be enough.
This package exists for teams actually running OmniRoute in OpenCode and needing things that work in practice, not just on paper.
What is different here
- Real
apiMode: "responses"wiringchatuses@ai-sdk/openairesponsesuses@ai-sdk/openai
- OmniRoute responses compatibility fixes
- normalizes/removes unsupported token limit fields on
/responses - converts
reasoningEffortaliases intoreasoning.effort
- normalizes/removes unsupported token limit fields on
- Variant support for reasoning models
- keeps
low/medium/high - merges generated variants with custom ones like
xhigh
- keeps
- OmniRoute-aware model metadata enrichment
models.devenrichment for context/output limits- routed-provider matching using OmniRoute
root/owned_bymetadata - combo model lowest-common-capability calculation
- Safer OpenCode runtime behavior
- provider bootstrap normalization
- local/runtime testing focused on actual OpenCode behavior
Why release it separately
Because OmniRoute behavior and OpenCode provider behavior do not always line up cleanly.
This package is for shipping OmniRoute-specific fixes without waiting for a generic upstream plugin direction, especially around:
- Responses API behavior
- reasoning variants
- custom/proxy provider quirks
- combo model metadata correctness
Highlights
- ✅
/connect omniroutesupport - ✅ API key authentication
- ✅ dynamic model fetching from
/v1/models - ✅ model caching with TTL
- ✅ fallback models when API/model listing fails
- ✅ combo model capability enrichment from
/api/combos - ✅
chatandresponsesruntime modes - ✅ reasoning variant support for OmniRoute reasoning models
- ✅ request normalization for OmniRoute Responses API quirks
- ✅ routed model enrichment for Anthropic/Gemini families behind providers like
antigravity - ✅ image capability hydration for GPT-5/Codex-style routed models
Installation
npm install -g @riskihajar/opencode-omniroute-authThen add it to your OpenCode config:
{
"plugin": [
"@riskihajar/opencode-omniroute-auth"
]
}For local development you can also point OpenCode directly to the repository path:
{
"plugin": [
"/absolute/path/to/opencode-omniroute-auth"
]
}Quick start
1. Connect
Run:
/connect omnirouteThen paste your OmniRoute API key.
2. Select model
Use:
/models3. Done
The plugin will:
- register the
omnirouteprovider - fetch available models from OmniRoute
- enrich model metadata when possible
- inject auth headers for OmniRoute requests
Vision / image input
Pain point this plugin fixes
OmniRoute model listings from /v1/models often do not include enough capability metadata for OpenCode to know whether a model supports image input.
In practice that can cause a bad failure mode for image prompts:
- OpenCode renders the dropped image as
[Image 1] - the provider registry thinks the selected model is text-only
- the image attachment is rejected before the request is sent
- OpenCode injects an error message into the user payload instead of sending
input_image
That degraded payload looks like this:
{
"role": "user",
"content": [
{
"type": "input_text",
"text": "[Image 1] ini gambar apa?"
},
{
"type": "input_text",
"text": "ERROR: Cannot read \"MAP-Midtrans-04-04-2026_10_14_PM.png\" (this model does not support image input). Inform the user."
}
]
}This plugin now fixes that for supported GPT-5/Codex-style OmniRoute models by keeping image capability hydration stable across provider bootstrap and runtime refresh.
How image capability is detected
The plugin decides image support in this order:
- explicit
provider.omniroute.options.modelMetadata - OmniRoute/runtime metadata when available
models.devenrichment when available- conservative GPT-5/Codex-family fallback heuristics for known routed model families
If a model is considered vision-capable, the provider model now exposes both:
capabilities.attachment = truemodalities.input = ['text', 'image']
That combination is important because OpenCode uses provider capability metadata before deciding whether to serialize a dropped/pasted image as input_image.
How to confirm it works
When image handling works, your request payload should contain a real image part, not just an injected error string:
{
"role": "user",
"content": [
{
"type": "input_text",
"text": "[Image 1] gambar apa yok?"
},
{
"type": "input_image",
"image_url": "data:image/png;base64,..."
}
]
}If a model truly does not support image input
The plugin prefers explicit metadata over heuristics.
- If OmniRoute or your manual
modelMetadatasays a model does not support image input, that model should remain text-only. - If metadata is missing, the plugin only enables image support for known GPT-5/Codex-style families where real-world OpenCode usage would otherwise degrade badly.
- Everything else stays on the safe default unless explicit metadata or enrichment says otherwise.
If you know a routed model should stay text-only, pin it explicitly:
{
"provider": {
"omniroute": {
"options": {
"modelMetadata": {
"some-provider/some-text-only-model": {
"supportsVision": false
}
}
}
}
}
}Configuration
Minimal example:
{
"plugin": [
"@riskihajar/opencode-omniroute-auth"
],
"provider": {
"omniroute": {
"options": {
"baseURL": "http://localhost:20128/v1",
"apiMode": "chat"
}
}
}
}Options
| Option | Type | Description |
|---|---|---|
| provider.omniroute.options.baseURL | string | OmniRoute base URL. Default: http://localhost:20128/v1 |
| provider.omniroute.options.apiMode | 'chat' \| 'responses' | Runtime API mode. Default: chat |
| provider.omniroute.options.refreshOnList | boolean | Refresh model list on provider load. Default: true |
| provider.omniroute.options.modelCacheTtl | number | Cache TTL in ms |
| provider.omniroute.options.modelsDev | object | Configure models.dev enrichment |
| provider.omniroute.options.modelMetadata | object \| array | Override/add model metadata, including per-model apiMode and resetEmbeddedReasoningVariant |
API modes
chat
Uses:
@ai-sdk/openai
Best when your OmniRoute/OpenCode flow is primarily Chat Completions compatible.
responses
Uses:
@ai-sdk/openai
This is important.
responses mode here is not just config decoration. It changes the runtime provider implementation so OpenCode can use native Responses API behavior.
Example:
{
"provider": {
"omniroute": {
"options": {
"apiMode": "responses"
}
}
}
}Per-model apiMode override
Not every routed model behaves the same way behind OmniRoute.
Some models work best with Responses API, while others still stream in Chat Completions shape even when the provider is globally configured for responses.
Because of that, this plugin supports per-model apiMode overrides.
Example: keep global responses, but pin a specific model to chat:
{
"provider": {
"omniroute": {
"options": {
"apiMode": "responses",
"modelMetadata": {
"minimax/minimax-m1": {
"apiMode": "chat"
}
}
}
}
}
}Example: force a specific model to stay on responses:
{
"provider": {
"omniroute": {
"options": {
"apiMode": "responses",
"modelMetadata": {
"some-provider/some-model": {
"apiMode": "responses"
}
}
}
}
}
}You can also set apiMode directly inside provider.omniroute.models entries when you define custom seeded models.
If a routed model id already ends with an embedded reasoning suffix like -high or -low, you can clear that forced winner override and restore the normal variant picker with resetEmbeddedReasoningVariant: true.
Example:
{
"provider": {
"omniroute": {
"options": {
"modelMetadata": {
"antigravity/gemini-3.1-pro-high": {
"resetEmbeddedReasoningVariant": true,
"reasoning": true
}
}
}
}
}
}By default, the plugin still applies a conservative fallback for known models that appear to break under Responses streaming, but an explicit per-model override wins.
Current built-in fallback behavior in global responses mode:
- Cursor default aliases
cu/defaultandcursor/defaultfall back tochat - Anthropic-family routed models such as Claude / Opus / Sonnet / Haiku fall back to
chat - Gemini-family routed models fall back to
chat - MLX/Qwen-style routed models such as
mlx/mlx-community/Qwen3.5-4B-MLX-8bitfall back tochatwhen OmniRoute streams Chat Completions chunks on the Responses path - suffixes like
-thinking,-reasoning,-high,-medium,-low,-minimal,-max,-xhigh, and-noneare normalized before that decision
This matters because some routed models may advertise support for both Chat Completions and Responses, but still emit chat.completion.chunk events when called through /v1/responses. In that case, the plugin prefers the safer chat runtime unless you explicitly override the model back to responses.
For Cursor specifically:
cu/defaultandcursor/defaultare forced tochatbecause OmniRoute currently returns Chat Completions streaming there even when OpenCode is globally configured forresponses- Cursor-routed Claude models continue to follow the existing Claude-family fallback behavior instead of getting a broader Cursor-wide special case
Reasoning variants
For reasoning-capable models, this plugin can expose variants like:
lowmediumhigh
and merge them with explicit model variants like:
xhigh
This matters for OmniRoute/Codex-style models where upstream metadata is often incomplete or uneven.
OmniRoute-specific behavior
1. Dynamic model fetching
Models are fetched from:
/v1/models
2. Combo model enrichment
Combo models are resolved using OmniRoute combo metadata from:
/api/combos
Capabilities are calculated conservatively using the lowest common denominator across resolvable backing models.
3. Responses API normalization
When using apiMode: "responses", the plugin normalizes request payloads for OmniRoute quirks, including:
- removing unsupported token limit fields
- converting reasoning aliases into the shape expected by Responses requests
- stripping OpenCode/OpenAI-style aliases that OmniRoute currently rejects on
/v1/responses, includingtemperature,reasoningSummary,reasoning_summary,reasoningEffort,reasoning_effort, andtextVerbosity
This plugin intentionally stays on @ai-sdk/openai for both Chat and Responses modes and treats OmniRoute compatibility as a payload-shaping problem rather than relying on a separate OpenAI-compatible runtime.
For the current OmniRoute behavior tested locally, the plugin preserves Responses fields that are accepted for Codex/GPT-5-style models, including:
storeprompt_cache_keyparallel_tool_callstruncationservice_tiertop_ppresence_penaltyfrequency_penaltymetadatainclude
models.dev enrichment
OmniRoute model listings do not always provide enough metadata for a good OpenCode UX.
This plugin can enrich models with data derived from models.dev, especially:
- context window
- output token limit
- tool support
- reasoning support
- image support when
modalities.inputincludesimage
It also tries harder to resolve routed OmniRoute models by:
- checking the listed model
id - checking OmniRoute
root - normalizing runtime suffixes like
-thinkingor-high - mapping routed provider aliases such as
antigravityto the most likely upstream family when matchingmodels.dev
Example:
{
"provider": {
"omniroute": {
"options": {
"modelsDev": {
"enabled": true,
"url": "https://models.dev/api.json",
"timeoutMs": 1000,
"cacheTtl": 86400000,
"providerAliases": {
"cx": "openai"
}
}
}
}
}
}Custom model metadata
You can override or add model metadata manually.
JSON example:
{
"provider": {
"omniroute": {
"options": {
"modelMetadata": {
"virtual/my-custom-model": {
"contextWindow": 50000,
"maxTokens": 2048,
"apiMode": "chat"
}
}
}
}
}
}opencode.js example with matchers:
{
provider: {
omniroute: {
options: {
modelMetadata: [
{ match: /gpt-5\.4/i, reasoning: true },
{ match: /gpt-5\.3-codex$/i, contextWindow: 200000, maxTokens: 8192 },
],
},
},
},
}Runtime helpers
import {
fetchModels,
clearModelCache,
refreshModels,
} from '@riskihajar/opencode-omniroute-auth/runtime';Debugging and self-test
Check discovered models
To inspect the OmniRoute provider model registry with debug logs enabled:
source ~/.zshrc && OMNIROUTE_PLUGIN_DEBUG=1 opencode models omniroute --print-logs --log-level DEBUGUseful things to verify in the output:
Hydrated model codex/gpt-5.4: attachment=true input.image=true toolcall=true- no later hydration pass flipping the same model back to
attachment=false omniroute/cu/defaultandomniroute/cursor/defaultshould resolve without Responses parser errors
Spawn OpenCode for a quick self-test
To reproduce image handling from the CLI:
source ~/.zshrc && OMNIROUTE_PLUGIN_DEBUG=1 opencode run --model omniroute/codex/gpt-5.4 "[Image 1] ini gambar apa?" --print-logs --log-level DEBUGWhat to look for:
- if OpenCode still blocks image input before request serialization, your payload will degrade into injected
input_texterror content - if image serialization works, the user payload should contain
type: "input_image"
Common verification flow
- restart OpenCode completely after plugin changes
- run
opencode models omniroute --print-logs --log-level DEBUG - confirm the target model is hydrated with
attachment=true - drop or paste an image into a fresh session
- inspect the payload and confirm
input_imageis present
Development
npm install
npm testRelease philosophy
This package is maintained as a pragmatic OmniRoute-focused distribution.
That means the priority order is:
- make it work in real OpenCode setups
- preserve OmniRoute-specific UX/features
- keep the public package easy to consume
If upstream/fork-main behavior is too generic to solve OmniRoute edge cases quickly, this package will continue shipping targeted fixes independently.
License
MIT
