mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
refactor(@omniroute/opencode-provider): rewrite for schema correctness + publishability
The 1.0.0 release of the package was broken end-to-end:
1. index.js re-exported from "./index.ts" — Node can't import .ts at runtime,
so any consumer who `npm install`ed the package got ERR_UNKNOWN_FILE_EXTENSION.
2. The emitted provider shape did not match the OpenCode schema
(https://opencode.ai/config.json). It used a custom `{id, name, npm, options, auth}`
instead of the schema's `{npm: "@ai-sdk/openai-compatible", name, options, models}`.
3. README told users to pass `baseURL: "http://localhost:20128/v1"` but the code
appended `/v1` again — every request would 404 at `/v1/v1/...`.
4. No build step, no LICENSE file, no repository/author/engines fields, no tests.
This rewrite:
- Moves source under `src/`, adds a tsup build emitting CJS + ESM + .d.ts.
- `createOmniRouteProvider` now returns a schema-valid entry with
`npm: "@ai-sdk/openai-compatible"` + `models: Record<string, { name }>`.
- Adds `buildOmniRouteOpenCodeConfig` for full-document scaffolding.
- `normalizeBaseURL` deduplicates trailing `/` and `/v1`, accepts both forms,
and rejects malformed URLs and empty inputs.
- 13 unit tests covering URL normalisation, input validation, default model
catalog, custom models + labels, dedup/trim behaviour, and JSON round-trip.
- Adds LICENSE, full package.json (repository, engines, scripts, exports),
.gitignore, .npmignore, tsconfig.json, and a comprehensive README.
- Resets version to 0.1.0 to signal the pre-1.0 reset (1.0.0 was never on npm).
Documentation:
- New `docs/frameworks/OPENCODE.md` covering both integration paths (CLI vs npm),
URL normalisation, auth modes, troubleshooting, and runtime flow.
- README.md links the package and points to the new doc.
- CHANGELOG entry under Unreleased > Changed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -100,3 +100,4 @@ test-results/
|
|||||||
playwright-report/
|
playwright-report/
|
||||||
blob-report/
|
blob-report/
|
||||||
coverage/
|
coverage/
|
||||||
|
@omniroute/
|
||||||
|
|||||||
4
@omniroute/opencode-provider/.gitignore
vendored
Normal file
4
@omniroute/opencode-provider/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
7
@omniroute/opencode-provider/.npmignore
Normal file
7
@omniroute/opencode-provider/.npmignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
src
|
||||||
|
tests
|
||||||
|
tsconfig.json
|
||||||
|
tsup.config.ts
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
21
@omniroute/opencode-provider/LICENSE
Normal file
21
@omniroute/opencode-provider/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 OmniRoute contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,45 +1,138 @@
|
|||||||
# @omniroute/opencode-provider
|
# @omniroute/opencode-provider
|
||||||
|
|
||||||
Provider plugin for connecting [OpenCode](https://github.com/anomalyco/opencode) to [OmniRoute](https://github.com/diegosouzapw/OmniRoute).
|
Helper for connecting [OpenCode](https://opencode.ai) to a running [OmniRoute](https://github.com/diegosouzapw/OmniRoute) AI gateway.
|
||||||
|
|
||||||
|
The package emits a **schema-valid entry** for `opencode.json` (`https://opencode.ai/config.json`) that delegates the actual runtime to [`@ai-sdk/openai-compatible`](https://www.npmjs.com/package/@ai-sdk/openai-compatible). It does not ship any new HTTP client — OmniRoute already exposes an OpenAI-compatible surface, and OpenCode already speaks it through the AI SDK.
|
||||||
|
|
||||||
|
> Pre-1.0. The API may still change. See `CHANGELOG` in the OmniRoute repo for breaking notes.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @omniroute/opencode-provider
|
npm install --save-dev @omniroute/opencode-provider
|
||||||
|
# or
|
||||||
|
pnpm add -D @omniroute/opencode-provider
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
You also need OpenCode's own runtime dep, but that's a transitive concern — OpenCode itself ships with `@ai-sdk/openai-compatible`. This package only **generates configuration**.
|
||||||
|
|
||||||
```javascript
|
## Quick start
|
||||||
import { createOmniRouteProvider } from "@omniroute/opencode-provider";
|
|
||||||
|
|
||||||
const provider = createOmniRouteProvider({
|
### 1. Scaffold a fresh `opencode.json`
|
||||||
baseURL: "http://localhost:20128/v1",
|
|
||||||
apiKey: "your-omniroute-api-key",
|
```ts
|
||||||
|
import { writeFileSync } from "node:fs";
|
||||||
|
import { buildOmniRouteOpenCodeConfig } from "@omniroute/opencode-provider";
|
||||||
|
|
||||||
|
const config = buildOmniRouteOpenCodeConfig({
|
||||||
|
baseURL: "http://localhost:20128", // or your OmniRoute deployment URL
|
||||||
|
apiKey: process.env.OMNIROUTE_API_KEY ?? "sk_omniroute",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
writeFileSync("opencode.json", JSON.stringify(config, null, 2));
|
||||||
```
|
```
|
||||||
|
|
||||||
Then configure OpenCode to use the provider:
|
The resulting `opencode.json`:
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
// OpenCode settings
|
|
||||||
{
|
{
|
||||||
"provider": provider
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"provider": {
|
||||||
|
"omniroute": {
|
||||||
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
|
"name": "OmniRoute",
|
||||||
|
"options": {
|
||||||
|
"baseURL": "http://localhost:20128/v1",
|
||||||
|
"apiKey": "sk_omniroute",
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"claude-opus-4-5-thinking": { "name": "claude-opus-4-5-thinking" },
|
||||||
|
"claude-sonnet-4-5-thinking": { "name": "claude-sonnet-4-5-thinking" },
|
||||||
|
"gemini-3.1-pro-high": { "name": "gemini-3.1-pro-high" },
|
||||||
|
"gemini-3-flash": { "name": "gemini-3-flash" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 2. Merge into an existing `opencode.json`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createOmniRouteProvider } from "@omniroute/opencode-provider";
|
||||||
|
|
||||||
|
const provider = createOmniRouteProvider({
|
||||||
|
baseURL: "http://localhost:20128",
|
||||||
|
apiKey: process.env.OMNIROUTE_API_KEY!,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Place `provider` under provider.omniroute in your opencode.json
|
||||||
|
```
|
||||||
|
|
||||||
|
If you already have an `opencode.json` on disk and want a non-destructive merge from the OmniRoute side, use `omniroute config opencode` from the CLI (ships with the main OmniRoute install) — it preserves comments and unrelated keys.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
### `createOmniRouteProvider(options)`
|
### `createOmniRouteProvider(options): OpenCodeProviderEntry`
|
||||||
|
|
||||||
Creates an OpenCode-compatible provider object that routes requests through OmniRoute.
|
Returns the value to place under `provider.omniroute` inside `opencode.json`.
|
||||||
|
|
||||||
**Options:**
|
| Option | Type | Required | Description |
|
||||||
|
| ------------- | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| `baseURL` | `string` | Yes | OmniRoute base URL. Accepts `http://host:port` **or** `http://host:port/v1`. Trailing slashes are tolerated. |
|
||||||
|
| `apiKey` | `string` | Yes | OmniRoute API key. Use `sk_omniroute` for local installs that have `REQUIRE_API_KEY=false`. |
|
||||||
|
| `displayName` | `string` | No | Custom name shown in the OpenCode UI. Default: `"OmniRoute"`. |
|
||||||
|
| `models` | `string[]` | No | Override the surfaced model catalog. Default: 4 curated models — see `OMNIROUTE_DEFAULT_OPENCODE_MODELS`. |
|
||||||
|
| `modelLabels` | `Record<string,string>` | No | Human-readable labels keyed by model id. |
|
||||||
|
|
||||||
| Option | Type | Required | Description |
|
Throws on empty/invalid input — `baseURL` must be a real URL, `apiKey` must be a non-empty string.
|
||||||
| --------- | -------- | -------- | ---------------------------------------------------------- |
|
|
||||||
| `baseURL` | `string` | Yes | OmniRoute API base URL (e.g., `http://localhost:20128/v1`) |
|
|
||||||
| `apiKey` | `string` | Yes | OmniRoute API key |
|
|
||||||
| `model` | `string` | No | Model identifier (default: `"opencode"`) |
|
|
||||||
|
|
||||||
**Returns:** An OpenCode-compatible provider object with `id`, `name`, `npm`, `options`, and `auth` fields.
|
### `buildOmniRouteOpenCodeConfig(options): OpenCodeConfigDocument`
|
||||||
|
|
||||||
|
Same options as above, but returns a full document with `$schema` and the `provider.omniroute` wrapper, ready to write to `opencode.json`.
|
||||||
|
|
||||||
|
### `normalizeBaseURL(input): string`
|
||||||
|
|
||||||
|
Exported for completeness. Strips trailing `/`, deduplicates a trailing `/v1`, and re-appends exactly one `/v1`. Throws on empty / non-URL input.
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
|
||||||
|
- `OMNIROUTE_PROVIDER_KEY` — `"omniroute"` (the key used under `provider.*`).
|
||||||
|
- `OMNIROUTE_PROVIDER_NPM` — `"@ai-sdk/openai-compatible"` (the runtime delegate).
|
||||||
|
- `OPENCODE_CONFIG_SCHEMA` — `"https://opencode.ai/config.json"`.
|
||||||
|
- `OMNIROUTE_DEFAULT_OPENCODE_MODELS` — readonly list of 4 default model ids.
|
||||||
|
|
||||||
|
## Custom model catalog
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createOmniRouteProvider } from "@omniroute/opencode-provider";
|
||||||
|
|
||||||
|
createOmniRouteProvider({
|
||||||
|
baseURL: "http://localhost:20128",
|
||||||
|
apiKey: "sk_omniroute",
|
||||||
|
models: ["auto", "claude-opus-4-7", "gpt-5.5"],
|
||||||
|
modelLabels: {
|
||||||
|
auto: "Auto-Combo (recommended)",
|
||||||
|
"claude-opus-4-7": "Claude Opus 4.7",
|
||||||
|
"gpt-5.5": "GPT-5.5",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Duplicates and empty strings are dropped automatically, and order is preserved.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Requests 404 with `/v1/v1/...`** — you're on an old version (≤1.0.0). Update to `≥0.1.0` of this re-released package. The new build normalises `baseURL` automatically.
|
||||||
|
- **`401 Invalid API key`** — your OmniRoute instance has `REQUIRE_API_KEY=true` but the key you supplied doesn't exist there. Create one via the dashboard or set `REQUIRE_API_KEY=false` and use `sk_omniroute`.
|
||||||
|
- **OpenCode complains the provider has no models** — supply an explicit `models` list; the default 4 may be hidden by your provider visibility settings.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [OmniRoute](https://github.com/diegosouzapw/OmniRoute) — the AI gateway this plugin targets.
|
||||||
|
- [OpenCode](https://opencode.ai) — the agentic CLI consumer.
|
||||||
|
- [`@ai-sdk/openai-compatible`](https://www.npmjs.com/package/@ai-sdk/openai-compatible) — the runtime delegate that actually speaks HTTP.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [`LICENSE`](./LICENSE).
|
||||||
|
|||||||
3
@omniroute/opencode-provider/index.d.ts
vendored
3
@omniroute/opencode-provider/index.d.ts
vendored
@@ -1,3 +0,0 @@
|
|||||||
import OmniRouteProvider from "./index.js";
|
|
||||||
export { OmniRouteProvider };
|
|
||||||
export default OmniRouteProvider;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { createOmniRouteProvider, default as default } from "./index.ts";
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
/**
|
|
||||||
* OpenCode provider plugin for OmniRoute AI Gateway
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* import { createOmniRouteProvider } from "@omniroute/opencode-provider";
|
|
||||||
* const provider = createOmniRouteProvider({
|
|
||||||
* baseURL: "http://localhost:20128/v1",
|
|
||||||
* apiKey: "your-api-key",
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* Then add to OpenCode settings:
|
|
||||||
* { "provider": provider }
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface OmniRouteProviderOptions {
|
|
||||||
baseURL: string;
|
|
||||||
apiKey: string;
|
|
||||||
model?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OmniRouteProvider {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
npm: string;
|
|
||||||
options: Record<string, unknown>;
|
|
||||||
auth: { type: string; apiKey: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createOmniRouteProvider(options: OmniRouteProviderOptions): OmniRouteProvider {
|
|
||||||
if (!options.baseURL) {
|
|
||||||
throw new Error("baseURL is required");
|
|
||||||
}
|
|
||||||
if (!options.apiKey) {
|
|
||||||
throw new Error("apiKey is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseURL = options.baseURL.replace(/\/+$/, "");
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: "omniroute",
|
|
||||||
name: "OmniRoute AI Gateway",
|
|
||||||
npm: "@omniroute/opencode-provider",
|
|
||||||
options: {
|
|
||||||
baseURL: `${baseURL}/v1`,
|
|
||||||
model: options.model || "opencode",
|
|
||||||
},
|
|
||||||
auth: {
|
|
||||||
type: "apiKey",
|
|
||||||
apiKey: options.apiKey,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createOmniRouteProvider;
|
|
||||||
2019
@omniroute/opencode-provider/package-lock.json
generated
Normal file
2019
@omniroute/opencode-provider/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,59 @@
|
|||||||
{
|
{
|
||||||
"name": "@omniroute/opencode-provider",
|
"name": "@omniroute/opencode-provider",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0",
|
||||||
"description": "OpenCode provider plugin for OmniRoute AI Gateway",
|
"description": "OpenCode provider helper for the OmniRoute AI Gateway. Generates a schema-valid provider entry for opencode.json that delegates the runtime to @ai-sdk/openai-compatible.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "index.js",
|
"main": "./dist/index.cjs",
|
||||||
"types": "index.d.ts",
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"index.js",
|
"dist",
|
||||||
"index.d.ts",
|
"README.md",
|
||||||
"README.md"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"clean": "rm -rf dist",
|
||||||
|
"test": "node --import tsx/esm --test tests/**/*.test.ts",
|
||||||
|
"prepublishOnly": "npm run clean && npm run build && npm test"
|
||||||
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"omniroute",
|
"omniroute",
|
||||||
"opencode",
|
"opencode",
|
||||||
"provider"
|
"opencode-ai",
|
||||||
|
"ai-sdk",
|
||||||
|
"openai-compatible",
|
||||||
|
"provider",
|
||||||
|
"ai-gateway",
|
||||||
|
"llm-router"
|
||||||
],
|
],
|
||||||
|
"author": "OmniRoute contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {}
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/diegosouzapw/OmniRoute.git",
|
||||||
|
"directory": "@omniroute/opencode-provider"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/diegosouzapw/OmniRoute/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/diegosouzapw/OmniRoute/tree/main/%40omniroute/opencode-provider#readme",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.20.2"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tsup": "^8.5.0",
|
||||||
|
"tsx": "^4.20.0",
|
||||||
|
"typescript": "^5.9.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
160
@omniroute/opencode-provider/src/index.ts
Normal file
160
@omniroute/opencode-provider/src/index.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
/**
|
||||||
|
* OpenCode provider plugin for OmniRoute AI Gateway.
|
||||||
|
*
|
||||||
|
* Generates an OpenCode-compatible provider object that points to a running
|
||||||
|
* OmniRoute instance. The output follows the OpenCode config schema
|
||||||
|
* (https://opencode.ai/config.json) and delegates the runtime to
|
||||||
|
* `@ai-sdk/openai-compatible` so OpenCode can drive any OmniRoute-exposed
|
||||||
|
* model through its standard OpenAI-compatible client.
|
||||||
|
*
|
||||||
|
* Two ways to consume the helper:
|
||||||
|
*
|
||||||
|
* 1. As code, when you build your own opencode.json programmatically:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* import { buildOmniRouteOpenCodeConfig } from "@omniroute/opencode-provider";
|
||||||
|
* const config = buildOmniRouteOpenCodeConfig({
|
||||||
|
* baseURL: "http://localhost:20128",
|
||||||
|
* apiKey: "sk_omniroute",
|
||||||
|
* });
|
||||||
|
* // config -> { $schema, provider: { omniroute: { npm, name, options, models } } }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* 2. As a single-provider entry to merge into an existing opencode.json:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* import { createOmniRouteProvider } from "@omniroute/opencode-provider";
|
||||||
|
* const provider = createOmniRouteProvider({ baseURL, apiKey });
|
||||||
|
* // provider -> the value to place under provider.omniroute in opencode.json
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Note: `baseURL` accepts both `http://host:port` and `http://host:port/v1`.
|
||||||
|
* The helper normalises trailing slashes / `/v1` so you never get `/v1/v1`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const OMNIROUTE_PROVIDER_KEY = "omniroute" as const;
|
||||||
|
export const OMNIROUTE_PROVIDER_NPM = "@ai-sdk/openai-compatible" as const;
|
||||||
|
export const OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json" as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default catalog of models surfaced to OpenCode when the caller does not
|
||||||
|
* supply an explicit `models` list. Mirrors the curated set used by the
|
||||||
|
* OmniRoute dashboard's "OpenCode" config generator.
|
||||||
|
*/
|
||||||
|
export const OMNIROUTE_DEFAULT_OPENCODE_MODELS = [
|
||||||
|
"claude-opus-4-5-thinking",
|
||||||
|
"claude-sonnet-4-5-thinking",
|
||||||
|
"gemini-3.1-pro-high",
|
||||||
|
"gemini-3-flash",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export interface OmniRouteProviderOptions {
|
||||||
|
/** OmniRoute base URL, with or without trailing `/v1`. Required. */
|
||||||
|
baseURL: string;
|
||||||
|
/** OmniRoute API key. Required. Use `sk_omniroute` for local instances without REQUIRE_API_KEY. */
|
||||||
|
apiKey: string;
|
||||||
|
/** Override the display name shown in OpenCode. Default: `"OmniRoute"`. */
|
||||||
|
displayName?: string;
|
||||||
|
/** Override the model catalog. Defaults to `OMNIROUTE_DEFAULT_OPENCODE_MODELS`. */
|
||||||
|
models?: readonly string[];
|
||||||
|
/** Optional human-readable labels keyed by model id. */
|
||||||
|
modelLabels?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenCodeProviderEntry {
|
||||||
|
/** Identifier of the OpenCode runtime package that will speak to OmniRoute. */
|
||||||
|
npm: typeof OMNIROUTE_PROVIDER_NPM;
|
||||||
|
/** Display name in the OpenCode UI. */
|
||||||
|
name: string;
|
||||||
|
/** Options forwarded to `@ai-sdk/openai-compatible`. */
|
||||||
|
options: {
|
||||||
|
baseURL: string;
|
||||||
|
apiKey: string;
|
||||||
|
};
|
||||||
|
/** Model catalog surfaced to OpenCode. */
|
||||||
|
models: Record<string, { name: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OpenCodeConfigDocument {
|
||||||
|
$schema: typeof OPENCODE_CONFIG_SCHEMA;
|
||||||
|
provider: {
|
||||||
|
[OMNIROUTE_PROVIDER_KEY]: OpenCodeProviderEntry;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireNonEmpty(value: unknown, field: string): string {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
throw new TypeError(`@omniroute/opencode-provider: ${field} must be a string`);
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
throw new Error(`@omniroute/opencode-provider: ${field} is required and cannot be empty`);
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalise the user-supplied baseURL so the final `options.baseURL` always
|
||||||
|
* ends in exactly one `/v1`. Accepts both `http://host` and `http://host/v1`.
|
||||||
|
*/
|
||||||
|
export function normalizeBaseURL(rawBaseURL: string): string {
|
||||||
|
const trimmed = requireNonEmpty(rawBaseURL, "baseURL");
|
||||||
|
try {
|
||||||
|
// Reject malformed URLs early.
|
||||||
|
new URL(trimmed);
|
||||||
|
} catch {
|
||||||
|
throw new Error(
|
||||||
|
`@omniroute/opencode-provider: baseURL is not a valid URL: ${JSON.stringify(rawBaseURL)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return trimmed.replace(/\/+$/, "").replace(/\/v1$/, "") + "/v1";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the `provider.omniroute` entry for an OpenCode config document.
|
||||||
|
* The returned object is JSON-serialisable and safe to embed verbatim.
|
||||||
|
*/
|
||||||
|
export function createOmniRouteProvider(options: OmniRouteProviderOptions): OpenCodeProviderEntry {
|
||||||
|
const baseURL = normalizeBaseURL(options.baseURL);
|
||||||
|
const apiKey = requireNonEmpty(options.apiKey, "apiKey");
|
||||||
|
|
||||||
|
const modelList =
|
||||||
|
options.models && options.models.length > 0
|
||||||
|
? [...options.models]
|
||||||
|
: [...OMNIROUTE_DEFAULT_OPENCODE_MODELS];
|
||||||
|
|
||||||
|
const labels = options.modelLabels ?? {};
|
||||||
|
const models: Record<string, { name: string }> = {};
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const raw of modelList) {
|
||||||
|
const id = typeof raw === "string" ? raw.trim() : "";
|
||||||
|
if (!id || seen.has(id)) continue;
|
||||||
|
seen.add(id);
|
||||||
|
const label = typeof labels[id] === "string" && labels[id].trim() ? labels[id].trim() : id;
|
||||||
|
models[id] = { name: label };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
npm: OMNIROUTE_PROVIDER_NPM,
|
||||||
|
name: options.displayName?.trim() || "OmniRoute",
|
||||||
|
options: { baseURL, apiKey },
|
||||||
|
models,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a full OpenCode config document (with `$schema` + `provider.omniroute`).
|
||||||
|
* Useful when scaffolding a fresh `opencode.json`.
|
||||||
|
*/
|
||||||
|
export function buildOmniRouteOpenCodeConfig(
|
||||||
|
options: OmniRouteProviderOptions
|
||||||
|
): OpenCodeConfigDocument {
|
||||||
|
return {
|
||||||
|
$schema: OPENCODE_CONFIG_SCHEMA,
|
||||||
|
provider: {
|
||||||
|
[OMNIROUTE_PROVIDER_KEY]: createOmniRouteProvider(options),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createOmniRouteProvider;
|
||||||
121
@omniroute/opencode-provider/tests/index.test.ts
Normal file
121
@omniroute/opencode-provider/tests/index.test.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildOmniRouteOpenCodeConfig,
|
||||||
|
createOmniRouteProvider,
|
||||||
|
normalizeBaseURL,
|
||||||
|
OMNIROUTE_DEFAULT_OPENCODE_MODELS,
|
||||||
|
OMNIROUTE_PROVIDER_NPM,
|
||||||
|
OPENCODE_CONFIG_SCHEMA,
|
||||||
|
} from "../src/index.ts";
|
||||||
|
|
||||||
|
test("normalizeBaseURL preserves a bare host:port", () => {
|
||||||
|
assert.equal(normalizeBaseURL("http://localhost:20128"), "http://localhost:20128/v1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeBaseURL strips trailing slashes", () => {
|
||||||
|
assert.equal(normalizeBaseURL("http://localhost:20128////"), "http://localhost:20128/v1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeBaseURL deduplicates an existing /v1 suffix", () => {
|
||||||
|
assert.equal(normalizeBaseURL("http://localhost:20128/v1"), "http://localhost:20128/v1");
|
||||||
|
assert.equal(normalizeBaseURL("http://localhost:20128/v1/"), "http://localhost:20128/v1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeBaseURL rejects empty input", () => {
|
||||||
|
assert.throws(() => normalizeBaseURL(" "), /baseURL is required/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeBaseURL rejects malformed URLs", () => {
|
||||||
|
assert.throws(() => normalizeBaseURL("not a url"), /not a valid URL/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createOmniRouteProvider validates required fields", () => {
|
||||||
|
assert.throws(
|
||||||
|
() => createOmniRouteProvider({ baseURL: "", apiKey: "x" } as never),
|
||||||
|
/baseURL is required/
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => createOmniRouteProvider({ baseURL: "http://x", apiKey: "" } as never),
|
||||||
|
/apiKey is required/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createOmniRouteProvider produces the OpenCode-compatible shape", () => {
|
||||||
|
const provider = createOmniRouteProvider({
|
||||||
|
baseURL: "http://localhost:20128",
|
||||||
|
apiKey: "sk_omniroute",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(provider.npm, OMNIROUTE_PROVIDER_NPM);
|
||||||
|
assert.equal(provider.name, "OmniRoute");
|
||||||
|
assert.equal(provider.options.baseURL, "http://localhost:20128/v1");
|
||||||
|
assert.equal(provider.options.apiKey, "sk_omniroute");
|
||||||
|
assert.equal(typeof provider.models, "object");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createOmniRouteProvider seeds the default model catalog", () => {
|
||||||
|
const provider = createOmniRouteProvider({
|
||||||
|
baseURL: "http://localhost:20128",
|
||||||
|
apiKey: "sk_omniroute",
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelIds = Object.keys(provider.models).sort();
|
||||||
|
const defaultIds = [...OMNIROUTE_DEFAULT_OPENCODE_MODELS].sort();
|
||||||
|
assert.deepEqual(modelIds, defaultIds);
|
||||||
|
for (const id of defaultIds) {
|
||||||
|
assert.equal(provider.models[id]?.name, id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createOmniRouteProvider honours a custom models list and labels", () => {
|
||||||
|
const provider = createOmniRouteProvider({
|
||||||
|
baseURL: "http://localhost:20128",
|
||||||
|
apiKey: "sk_omniroute",
|
||||||
|
models: ["auto", "claude-opus-4-7"],
|
||||||
|
modelLabels: { auto: "Auto-Combo", "claude-opus-4-7": "Opus 4.7" },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(Object.keys(provider.models), ["auto", "claude-opus-4-7"]);
|
||||||
|
assert.equal(provider.models.auto.name, "Auto-Combo");
|
||||||
|
assert.equal(provider.models["claude-opus-4-7"].name, "Opus 4.7");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createOmniRouteProvider deduplicates and trims model ids", () => {
|
||||||
|
const provider = createOmniRouteProvider({
|
||||||
|
baseURL: "http://localhost:20128",
|
||||||
|
apiKey: "sk_omniroute",
|
||||||
|
models: [" auto ", "auto", "", "claude-opus-4-7"],
|
||||||
|
});
|
||||||
|
assert.deepEqual(Object.keys(provider.models), ["auto", "claude-opus-4-7"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createOmniRouteProvider honours displayName override", () => {
|
||||||
|
const provider = createOmniRouteProvider({
|
||||||
|
baseURL: "http://localhost:20128",
|
||||||
|
apiKey: "sk_omniroute",
|
||||||
|
displayName: "Local OmniRoute",
|
||||||
|
});
|
||||||
|
assert.equal(provider.name, "Local OmniRoute");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildOmniRouteOpenCodeConfig wraps the provider with the OpenCode schema", () => {
|
||||||
|
const doc = buildOmniRouteOpenCodeConfig({
|
||||||
|
baseURL: "http://localhost:20128/v1",
|
||||||
|
apiKey: "sk_omniroute",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(doc.$schema, OPENCODE_CONFIG_SCHEMA);
|
||||||
|
assert.equal(typeof doc.provider.omniroute, "object");
|
||||||
|
assert.equal(doc.provider.omniroute.options.baseURL, "http://localhost:20128/v1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("config document is JSON-serialisable", () => {
|
||||||
|
const doc = buildOmniRouteOpenCodeConfig({
|
||||||
|
baseURL: "http://localhost:20128",
|
||||||
|
apiKey: "sk_omniroute",
|
||||||
|
});
|
||||||
|
const round = JSON.parse(JSON.stringify(doc));
|
||||||
|
assert.deepEqual(round, doc);
|
||||||
|
});
|
||||||
19
@omniroute/opencode-provider/tsconfig.json
Normal file
19
@omniroute/opencode-provider/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"declaration": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noUncheckedIndexedAccess": false,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["dist", "node_modules", "tests"]
|
||||||
|
}
|
||||||
18
@omniroute/opencode-provider/tsup.config.ts
Normal file
18
@omniroute/opencode-provider/tsup.config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig } from "tsup";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ["src/index.ts"],
|
||||||
|
format: ["esm", "cjs"],
|
||||||
|
dts: true,
|
||||||
|
clean: true,
|
||||||
|
sourcemap: false,
|
||||||
|
splitting: false,
|
||||||
|
treeshake: true,
|
||||||
|
target: "node20",
|
||||||
|
outDir: "dist",
|
||||||
|
minify: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// CJS consumers should prefer named imports (`require(pkg).createOmniRouteProvider`).
|
||||||
|
// The `default` export is also exposed for ESM ergonomics, which makes tsup warn
|
||||||
|
// about mixed exports — that's expected and harmless for this package.
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **refactor(@omniroute/opencode-provider):** complete rewrite of the npm helper. The `1.0.0` artifact was non-functional — `index.js` re-exported from `.ts` (unrunnable at install time) and the emitted shape didn't match the OpenCode `https://opencode.ai/config.json` schema. The new release ships a real `tsup` build (CJS + ESM + `.d.ts`), schema-correct output (`npm: "@ai-sdk/openai-compatible"`, with `models` catalog), `baseURL` deduplication (no more `/v1/v1`), input validation, 13 unit tests, and full documentation in [`docs/frameworks/OPENCODE.md`](docs/frameworks/OPENCODE.md). Versioned as `0.1.0` to signal the pre-1.0 reset.
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- **fix(oauth/windsurf):** Windsurf Firebase token refresh now reads `WINDSURF_CONFIG.firebaseApiKey` instead of `process.env.WINDSURF_FIREBASE_API_KEY` directly. The literal was removed from `.env.example` in this release, so the previous direct read would have silently skipped refresh for browser-flow Windsurf/Devin sessions (forcing re-auth instead of renewing). Operators with a legacy `WINDSURF_FIREBASE_API_KEY` value in their `.env` keep working — the env override path is preserved by `resolvePublicCred()`. See [`docs/security/PUBLIC_CREDS.md`](docs/security/PUBLIC_CREDS.md).
|
- **fix(oauth/windsurf):** Windsurf Firebase token refresh now reads `WINDSURF_CONFIG.firebaseApiKey` instead of `process.env.WINDSURF_FIREBASE_API_KEY` directly. The literal was removed from `.env.example` in this release, so the previous direct read would have silently skipped refresh for browser-flow Windsurf/Devin sessions (forcing re-auth instead of renewing). Operators with a legacy `WINDSURF_FIREBASE_API_KEY` value in their `.env` keep working — the env override path is preserved by `resolvePublicCred()`. See [`docs/security/PUBLIC_CREDS.md`](docs/security/PUBLIC_CREDS.md).
|
||||||
|
|||||||
@@ -291,6 +291,8 @@ OmniRoute works seamlessly with **16+ AI coding tools** — one config, all tool
|
|||||||
|
|
||||||
📖 Full setup for each tool: [`docs/CLI-TOOLS.md`](docs/CLI-TOOLS.md)
|
📖 Full setup for each tool: [`docs/CLI-TOOLS.md`](docs/CLI-TOOLS.md)
|
||||||
|
|
||||||
|
> **OpenCode tip:** the [`@omniroute/opencode-provider`](./@omniroute/opencode-provider) npm package emits a schema-valid `opencode.json` entry. Use `omniroute config opencode` (CLI) or `buildOmniRouteOpenCodeConfig()` (library) — see [`docs/frameworks/OPENCODE.md`](docs/frameworks/OPENCODE.md) for the full integration guide.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🌐 Supported Providers — 160+
|
## 🌐 Supported Providers — 160+
|
||||||
|
|||||||
165
docs/frameworks/OPENCODE.md
Normal file
165
docs/frameworks/OPENCODE.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
---
|
||||||
|
title: "OpenCode Integration"
|
||||||
|
version: 3.8.0
|
||||||
|
lastUpdated: 2026-05-14
|
||||||
|
---
|
||||||
|
|
||||||
|
# OpenCode Integration
|
||||||
|
|
||||||
|
> **Status:** Generally available.
|
||||||
|
> **Audience:** Operators wiring OpenCode to an OmniRoute deployment.
|
||||||
|
> **Source of truth (config schema):** `src/shared/services/opencodeConfig.ts`
|
||||||
|
> **Source of truth (npm package):** `@omniroute/opencode-provider/` (publishable workspace)
|
||||||
|
|
||||||
|
[OpenCode](https://opencode.ai) is an agentic CLI/desktop AI client. It reads its provider catalog from `~/.config/opencode/opencode.json` (or `opencode.jsonc`) and follows the schema at `https://opencode.ai/config.json`. OmniRoute exposes itself to OpenCode as one of those providers — every request flows through OmniRoute's standard OpenAI-compatible `/v1` surface, so OpenCode automatically benefits from Auto-Combo routing, circuit breakers, key policies, observability, etc.
|
||||||
|
|
||||||
|
There are **two supported integration paths**. Pick one — they generate the same config.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Path 1 — CLI generator (no npm install)
|
||||||
|
|
||||||
|
Recommended for end users. Ships with OmniRoute. Writes `opencode.json` in place.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# After installing OmniRoute (npm i -g @omniroute/cli or local clone)
|
||||||
|
omniroute config opencode \
|
||||||
|
--baseUrl http://localhost:20128 \
|
||||||
|
--apiKey "$OMNIROUTE_API_KEY"
|
||||||
|
```
|
||||||
|
|
||||||
|
Behind the scenes the CLI calls `mergeOpenCodeConfigText()` (`src/shared/services/opencodeConfig.ts:104`), so an existing `opencode.json` keeps its other providers and comments. The OmniRoute entry is added/replaced atomically.
|
||||||
|
|
||||||
|
Resulting file (default model catalog):
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"provider": {
|
||||||
|
"omniroute": {
|
||||||
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
|
"name": "OmniRoute",
|
||||||
|
"options": {
|
||||||
|
"baseURL": "http://localhost:20128/v1",
|
||||||
|
"apiKey": "<your-key>",
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"claude-opus-4-5-thinking": { "name": "claude-opus-4-5-thinking" },
|
||||||
|
"claude-sonnet-4-5-thinking": { "name": "claude-sonnet-4-5-thinking" },
|
||||||
|
"gemini-3.1-pro-high": { "name": "gemini-3.1-pro-high" },
|
||||||
|
"gemini-3-flash": { "name": "gemini-3-flash" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Path 2 — npm package `@omniroute/opencode-provider`
|
||||||
|
|
||||||
|
Recommended when you're scripting the config from Node/TS (CI pipelines, monorepos, custom installer flows).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save-dev @omniroute/opencode-provider
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { writeFileSync } from "node:fs";
|
||||||
|
import { buildOmniRouteOpenCodeConfig } from "@omniroute/opencode-provider";
|
||||||
|
|
||||||
|
const config = buildOmniRouteOpenCodeConfig({
|
||||||
|
baseURL: "http://localhost:20128",
|
||||||
|
apiKey: process.env.OMNIROUTE_API_KEY ?? "sk_omniroute",
|
||||||
|
// Optional: override the model catalog exposed to OpenCode
|
||||||
|
models: ["auto", "claude-opus-4-7", "gpt-5.5"],
|
||||||
|
modelLabels: { auto: "Auto-Combo" },
|
||||||
|
});
|
||||||
|
|
||||||
|
writeFileSync("opencode.json", JSON.stringify(config, null, 2));
|
||||||
|
```
|
||||||
|
|
||||||
|
For a non-destructive merge against an existing file, replicate `mergeOpenCodeConfigText()` from `opencodeConfig.ts` or call the CLI generator.
|
||||||
|
|
||||||
|
See the [package README](../../%40omniroute/opencode-provider/README.md) for the full API.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What the runtime actually does
|
||||||
|
|
||||||
|
Both paths produce the same `provider.omniroute.npm: "@ai-sdk/openai-compatible"`. At runtime, OpenCode loads `@ai-sdk/openai-compatible` (already a transitive dependency of OpenCode) and configures it with `baseURL` + `apiKey`. From there:
|
||||||
|
|
||||||
|
```
|
||||||
|
OpenCode UI/agent
|
||||||
|
→ @ai-sdk/openai-compatible
|
||||||
|
→ HTTP POST {baseURL}/chat/completions (OmniRoute OpenAI surface)
|
||||||
|
→ OmniRoute /v1/chat/completions handler (open-sse/handlers/chatCore.ts)
|
||||||
|
→ combo routing / Auto-Combo / executor
|
||||||
|
→ upstream provider
|
||||||
|
```
|
||||||
|
|
||||||
|
The plugin never touches HTTP. It only emits configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Model catalog defaults
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const OMNIROUTE_DEFAULT_OPENCODE_MODELS = [
|
||||||
|
"claude-opus-4-5-thinking",
|
||||||
|
"claude-sonnet-4-5-thinking",
|
||||||
|
"gemini-3.1-pro-high",
|
||||||
|
"gemini-3-flash",
|
||||||
|
] as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
You can override via `models: [...]`. Recommended additions:
|
||||||
|
|
||||||
|
- `"auto"` — surfaces OmniRoute's [Auto-Combo](../routing/AUTO-COMBO.md) zero-config router. Lets OpenCode pick "the best available model" without you hard-coding the catalog.
|
||||||
|
- `"<combo-name>"` — any combo you've defined in the dashboard; OmniRoute resolves it transparently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## URL normalisation
|
||||||
|
|
||||||
|
The helper accepts both forms and emits exactly one `/v1`:
|
||||||
|
|
||||||
|
| Input | Output (`options.baseURL`) |
|
||||||
|
| ------------------------------ | --------------------------- |
|
||||||
|
| `http://localhost:20128` | `http://localhost:20128/v1` |
|
||||||
|
| `http://localhost:20128/` | `http://localhost:20128/v1` |
|
||||||
|
| `http://localhost:20128/v1` | `http://localhost:20128/v1` |
|
||||||
|
| `http://localhost:20128/v1///` | `http://localhost:20128/v1` |
|
||||||
|
|
||||||
|
This deduplication is **the most common breakage** seen in older configs. If you have an `opencode.json` from before v3.8.0 that points at `/v1/v1/...`, re-run the generator or call `createOmniRouteProvider` again.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication modes
|
||||||
|
|
||||||
|
| OmniRoute setting | Recommended `apiKey` value |
|
||||||
|
| ------------------------------------------- | ----------------------------------------------------- |
|
||||||
|
| `REQUIRE_API_KEY=false` (default for local) | `sk_omniroute` (literal placeholder) |
|
||||||
|
| `REQUIRE_API_KEY=true` | A real per-user API key from Dashboard → API Manager. |
|
||||||
|
|
||||||
|
For Anthropic-style clients that send `x-api-key` + `anthropic-version`, OmniRoute's `extractApiKey` also honours the key from `x-api-key`. OpenCode uses the OpenAI surface, so it'll always send `Authorization: Bearer ${apiKey}` — no Anthropic special-case applies here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause | Fix |
|
||||||
|
| ---------------------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||||
|
| `404` on every request with URL containing `/v1/v1/` | Stale config from pre-v3.8 plugin that double-suffixed `/v1`. | Regenerate via Path 1 or 2. |
|
||||||
|
| `401 Invalid API key` | OmniRoute has `REQUIRE_API_KEY=true` and the key is unknown. | Create the key in the dashboard, or set `REQUIRE_API_KEY=false` (local only) and use `sk_omniroute`. |
|
||||||
|
| Model list empty in OpenCode UI | All 4 default models are hidden in OmniRoute's provider visibility. | Pass `models: ["auto", ...]` to surface ones you've enabled. |
|
||||||
|
| OpenCode 500 with `cannot read property 'models'` | Older OpenCode (< 0.1.x) didn't accept inline `models`. | Upgrade OpenCode to a version that follows the v1 schema (`opencode.ai/config.json`). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [API reference](../reference/API_REFERENCE.md) — full OmniRoute REST surface
|
||||||
|
- [Auto-Combo](../routing/AUTO-COMBO.md) — what `model: "auto"` means
|
||||||
|
- [`@omniroute/opencode-provider` README](../../%40omniroute/opencode-provider/README.md)
|
||||||
|
- Source: `src/shared/services/opencodeConfig.ts`, `src/lib/cli-helper/config-generator/opencode.ts`, `@omniroute/opencode-provider/src/index.ts`
|
||||||
Reference in New Issue
Block a user