@darknesskiller/opencode-cliproxyapi 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,59 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
+ and this project follows [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [0.2.0] - 2026-09-09
9
+
10
+ ### Added
11
+
12
+ - Auto-configure reasoning effort per model from CLIProxyAPI's own model
13
+ catalog. Models the server reports with supported reasoning levels are
14
+ marked as reasoning models, get their default effort configured, and expose
15
+ every supported effort as an OpenCode model variant for the variant-cycle
16
+ keybind.
17
+ - Query CLIProxyAPI's codex-client model catalog (`/v1/models?client_version=`)
18
+ to discover per-model reasoning levels at startup.
19
+
20
+ ### Changed
21
+
22
+ - A model's `reasoning` flag now prefers CLIProxyAPI's reported effort levels
23
+ over the previous id-based heuristic; the heuristic remains the fallback for
24
+ servers that do not report levels.
25
+
26
+ ## [0.1.2] - 2026-07-28
27
+
28
+ ### Fixed
29
+
30
+ - Dynamically route models with Anthropic protocol metadata through
31
+ `/v1/messages` instead of OpenAI-compatible chat completions, without
32
+ hard-coding model IDs.
33
+ - Preserve discovered model protocol metadata when users customize individual
34
+ model settings.
35
+
36
+ ## [0.1.1] - 2026-07-25
37
+
38
+ ### Changed
39
+
40
+ - Made persistent global OpenCode configuration the recommended setup flow.
41
+ - Moved temporary environment-variable setup to an optional alternative.
42
+ - Clarified API-key handling and troubleshooting.
43
+
44
+ ## [0.1.0] - 2026-07-25
45
+
46
+ ### Added
47
+
48
+ - Dynamic model discovery from CLIProxyAPI's `/v1/models` endpoint.
49
+ - OpenCode provider configuration for OpenAI-compatible chat completions.
50
+ - Custom server URL and API key support through environment variables or
51
+ plugin options.
52
+ - Automatic model names and capability hints in OpenCode's model picker.
53
+ - Local plugin and npm package installation flows.
54
+
55
+ [Unreleased]: https://github.com/DarknessKiller/opencode-cliproxyapi/compare/v0.2.0...HEAD
56
+ [0.2.0]: https://github.com/DarknessKiller/opencode-cliproxyapi/compare/v0.1.2...v0.2.0
57
+ [0.1.2]: https://github.com/DarknessKiller/opencode-cliproxyapi/compare/v0.1.1...v0.1.2
58
+ [0.1.1]: https://github.com/DarknessKiller/opencode-cliproxyapi/compare/v0.1.0...v0.1.1
59
+ [0.1.0]: https://github.com/DarknessKiller/opencode-cliproxyapi/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 İbrahim BABAL
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # OpenCode CLIProxyAPI
2
+
3
+ [![CI](https://github.com/DarknessKiller/opencode-cliproxyapi/actions/workflows/ci.yml/badge.svg)](https://github.com/DarknessKiller/opencode-cliproxyapi/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@darknesskiller/opencode-cliproxyapi)](https://www.npmjs.com/package/@darknesskiller/opencode-cliproxyapi)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
7
+ Fork of [opencode-cliproxyapi](https://github.com/yourcasualdev/opencode-cliproxyapi)
8
+ that uses every model exposed by [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)
9
+ directly in [OpenCode](https://opencode.ai/). This fork adds automatic
10
+ reasoning-effort configuration queried from the CLIProxyAPI server itself.
11
+
12
+ The plugin discovers CLIProxyAPI's live `/v1/models` catalog whenever OpenCode
13
+ starts. Available models appear in the normal `/models` picker under
14
+ **CLIProxyAPI**. OpenCode Go models that expose Anthropic-compatible endpoints
15
+ are automatically routed through `/v1/messages` using live model metadata from
16
+ [models.dev](https://models.dev/); the remaining discovered models continue to
17
+ use the provider's configured default protocol. No model IDs are hard-coded.
18
+
19
+ For every model whose reasoning levels are known to the CLIProxyAPI server
20
+ itself, the plugin also:
21
+
22
+ - marks the model as a reasoning model,
23
+ - sets the server-reported default reasoning effort as the model default, and
24
+ - registers each supported effort (`low`, `medium`, `high`, …) as a model
25
+ variant you can cycle with the variant keybind.
26
+
27
+ Effort levels are taken from CLIProxyAPI's codex-client catalog (the same data
28
+ its Codex clients use), so models that the server cannot describe keep the
29
+ plugin's previous id-based heuristics.
30
+
31
+ ## Quick start
32
+
33
+ You need OpenCode, a running CLIProxyAPI server, and one of its API keys.
34
+
35
+ ### 1. Install from npm
36
+
37
+ The plugin is published as `@darknesskiller/opencode-cliproxyapi`. OpenCode
38
+ installs npm plugins automatically when it starts, so listing the package in
39
+ your config (next step) is enough — no manual download or build.
40
+
41
+ ### 2. Save your connection
42
+
43
+ Open your global OpenCode config:
44
+
45
+ ```text
46
+ ~/.config/opencode/opencode.json
47
+ ```
48
+
49
+ Create it if it does not exist; either `opencode.json` or `opencode.jsonc`
50
+ works. Configure the plugin entry with the npm package name, plus your
51
+ persistent server URL and API key:
52
+
53
+ ```json
54
+ {
55
+ "$schema": "https://opencode.ai/config.json",
56
+ "plugin": [
57
+ [
58
+ "@darknesskiller/opencode-cliproxyapi",
59
+ {
60
+ "baseURL": "http://your-server:8317",
61
+ "apiKey": "your-cli-proxy-api-key"
62
+ }
63
+ ]
64
+ ]
65
+ }
66
+ ```
67
+
68
+ The first OpenCode start downloads the package; later releases are picked up
69
+ after OpenCode restarts. Do not install the original
70
+ `opencode-cliproxyapi` package at the same time — both register the same
71
+ `cliproxyapi` provider.
72
+
73
+ The URL may include `/v1`, but it is not required. If `baseURL` is omitted, the
74
+ plugin uses `http://localhost:8317/v1`.
75
+
76
+ Keep this global config private because it contains your API key. Do not copy
77
+ the connection into a project's `opencode.json` or commit it to a repository.
78
+
79
+ ### 3. Verify
80
+
81
+ ```bash
82
+ opencode models cliproxyapi
83
+ ```
84
+
85
+ You should see models reported by your server:
86
+
87
+ ```text
88
+ cliproxyapi/claude-sonnet-4-6
89
+ cliproxyapi/gpt-5.6-terra
90
+ cliproxyapi/gemini-3.1-pro-low
91
+ ```
92
+
93
+ ### 4. Select a model
94
+
95
+ Start OpenCode and run `/models`:
96
+
97
+ ```bash
98
+ opencode
99
+ ```
100
+
101
+ Choose **CLIProxyAPI**, select a model, and use OpenCode normally. Restart
102
+ OpenCode whenever the model catalog on CLIProxyAPI changes.
103
+
104
+ > This release stores the connection in OpenCode's global config instead of
105
+ > using the `/connect` screen. Model selection itself uses the standard
106
+ > `/models` experience.
107
+
108
+ ## Configuration
109
+
110
+ The recommended configuration is the global plugin entry shown above (with
111
+ the npm package name):
112
+
113
+ ```json
114
+ {
115
+ "$schema": "https://opencode.ai/config.json",
116
+ "plugin": [
117
+ [
118
+ "@darknesskiller/opencode-cliproxyapi",
119
+ {
120
+ "baseURL": "http://your-server:8317",
121
+ "apiKey": "your-cli-proxy-api-key",
122
+ "providerName": "My CLIProxyAPI"
123
+ }
124
+ ]
125
+ ]
126
+ }
127
+ ```
128
+
129
+ | Plugin option | Default | Purpose |
130
+ | --- | --- | --- |
131
+ | `baseURL` | `CLIPROXY_BASE_URL` or `http://localhost:8317/v1` | CLIProxyAPI URL |
132
+ | `apiKey` | `CLIPROXY_API_KEY` | CLIProxyAPI key |
133
+ | `providerID` | `cliproxyapi` | ID used in `provider/model` names |
134
+ | `providerName` | `CLIProxyAPI` | Name displayed in the model picker |
135
+ | `protocol` | `chat` | Default protocol: `chat` uses `/chat/completions`; `responses` uses `/responses`. Models marked as Anthropic-compatible by dynamic metadata override this per model. |
136
+ | `modelMetadataURL` | `https://models.dev/api.json` | Dynamic model-level protocol metadata. Set to `false` to disable enrichment and use only the default protocol. |
137
+ | `discoveryTimeoutMs` | `10000` | Startup model-discovery timeout |
138
+
139
+ If model metadata cannot be reached, the plugin logs a warning and keeps the
140
+ CLIProxyAPI-discovered models available with the configured default protocol.
141
+
142
+ ### Optional environment variables
143
+
144
+ Environment variables remain available for containers, CI, or users who prefer
145
+ not to place a key in the config:
146
+
147
+ ```bash
148
+ export CLIPROXY_BASE_URL="http://your-server:8317"
149
+ export CLIPROXY_API_KEY="your-cli-proxy-api-key"
150
+ ```
151
+
152
+ Put these lines in your shell profile if you want them to persist. Explicit
153
+ plugin options in `opencode.json` take precedence over environment variables.
154
+
155
+ Existing `provider.cliproxyapi` settings are preserved, so individual models
156
+ can be customized:
157
+
158
+ ```json
159
+ {
160
+ "provider": {
161
+ "cliproxyapi": {
162
+ "models": {
163
+ "gpt-5.6-terra": {
164
+ "name": "Terra",
165
+ "limit": {
166
+ "context": 200000,
167
+ "output": 65536
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+ ```
175
+
176
+ ## Troubleshooting
177
+
178
+ ### `Missing API key`
179
+
180
+ Check that the global plugin entry contains a non-empty `apiKey`, then restart
181
+ OpenCode. If you chose environment variables instead, ensure
182
+ `CLIPROXY_API_KEY` is available to the process that starts OpenCode.
183
+
184
+ ### No CLIProxyAPI models appear
185
+
186
+ First check the API directly:
187
+
188
+ ```bash
189
+ curl -H "Authorization: Bearer your-cli-proxy-api-key" \
190
+ "http://your-server:8317/v1/models"
191
+ ```
192
+
193
+ Then restart OpenCode and run:
194
+
195
+ ```bash
196
+ opencode models cliproxyapi
197
+ ```
198
+
199
+ ### Environment configuration works in one terminal but not another
200
+
201
+ Move the connection to the recommended global OpenCode config, or add the
202
+ environment variables to your shell profile.
203
+
204
+ ### The plugin does not load after updating
205
+
206
+ Restart OpenCode so it picks up the newest published version of the npm
207
+ package. If the package was never installed, check that the name in the
208
+ `plugin` list is exactly `@darknesskiller/opencode-cliproxyapi` and that you
209
+ are not also loading the original `opencode-cliproxyapi` package (both
210
+ register the same `cliproxyapi` provider).
211
+
212
+ ## Development
213
+
214
+ ```bash
215
+ git clone https://github.com/DarknessKiller/opencode-cliproxyapi.git
216
+ cd opencode-cliproxyapi
217
+ bun install
218
+ bun run check
219
+ ```
220
+
221
+ The repository's `opencode.json` loads the local build for integration testing:
222
+
223
+ ```bash
224
+ bun run build
225
+ export CLIPROXY_BASE_URL="http://your-server:8317"
226
+ export CLIPROXY_API_KEY="your-cli-proxy-api-key"
227
+ opencode models cliproxyapi
228
+ ```
229
+
230
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines and
231
+ [SECURITY.md](SECURITY.md) for private vulnerability reporting.
232
+
233
+ ## License
234
+
235
+ [MIT](LICENSE)
@@ -0,0 +1,35 @@
1
+ export type CatalogModel = {
2
+ id: string;
3
+ ownedBy?: string;
4
+ };
5
+ export type ModelProtocolCatalog = Record<string, {
6
+ npm?: string;
7
+ models: Record<string, string>;
8
+ }>;
9
+ export type ReasoningEffortLevels = {
10
+ levels: string[];
11
+ defaultLevel?: string;
12
+ };
13
+ export type ReasoningEffortCatalog = Record<string, ReasoningEffortLevels>;
14
+ export declare function normalizeBaseURL(value: string): string;
15
+ export declare function parseCatalog(input: unknown): CatalogModel[];
16
+ export declare function parseReasoningEffortCatalog(input: unknown): ReasoningEffortCatalog;
17
+ export declare function discoverReasoningEfforts(input: {
18
+ baseURL: string;
19
+ apiKey?: string;
20
+ timeoutMs: number;
21
+ fetcher?: typeof fetch;
22
+ }): Promise<ReasoningEffortCatalog>;
23
+ export declare function discoverModels(input: {
24
+ baseURL: string;
25
+ apiKey?: string;
26
+ timeoutMs: number;
27
+ fetcher?: typeof fetch;
28
+ }): Promise<CatalogModel[]>;
29
+ export declare function parseModelProtocolCatalog(input: unknown): ModelProtocolCatalog;
30
+ export declare function discoverModelProtocols(input: {
31
+ url: string;
32
+ timeoutMs: number;
33
+ fetcher?: typeof fetch;
34
+ }): Promise<ModelProtocolCatalog>;
35
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../src/catalog.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG,MAAM,CACvC,MAAM,EACN;IACE,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC/B,CACF,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAGD,MAAM,MAAM,sBAAsB,GAAG,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAA;AAW1E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,UAK7C;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,EAAE,CAuB3D;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB,CAqBlF;AAED,wBAAsB,wBAAwB,CAAC,KAAK,EAAE;IACpD,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAA;CACvB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAiBlC;AAED,wBAAsB,cAAc,CAAC,KAAK,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAA;CACvB,2BAcA;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,oBAAoB,CAsB9E;AAED,wBAAsB,sBAAsB,CAAC,KAAK,EAAE;IAClD,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAA;CACvB,iCAaA"}
@@ -0,0 +1,10 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ export declare const CLIProxyAPIPlugin: Plugin;
3
+ declare const _default: {
4
+ id: string;
5
+ server: Plugin;
6
+ };
7
+ export default _default;
8
+ export { discoverModelProtocols, discoverModels, discoverReasoningEfforts, normalizeBaseURL, parseCatalog, parseModelProtocolCatalog, parseReasoningEffortCatalog, } from "./catalog.js";
9
+ export type { CatalogModel, ModelProtocolCatalog, ReasoningEffortCatalog } from "./catalog.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,MAAM,EAA+B,MAAM,qBAAqB,CAAA;AAkCtF,eAAO,MAAM,iBAAiB,EAAE,MA8E/B,CAAA;;;;;AAED,wBAGwB;AACxB,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,wBAAwB,EACxB,gBAAgB,EAChB,YAAY,EACZ,yBAAyB,EACzB,2BAA2B,GAC5B,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,YAAY,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,320 @@
1
+ // @bun
2
+ // src/catalog.ts
3
+ var REASONING_LEVELS_CLIENT_VERSION = "1.0.0";
4
+ function normalizeBaseURL(value) {
5
+ const url = new URL(value);
6
+ const pathname = url.pathname.replace(/\/+$/, "");
7
+ url.pathname = pathname.endsWith("/v1") ? pathname : `${pathname}/v1`;
8
+ return url.toString().replace(/\/$/, "");
9
+ }
10
+ function parseCatalog(input) {
11
+ if (!isRecord(input))
12
+ throw new Error("CLIProxyAPI returned a non-object model catalog");
13
+ const response = input;
14
+ if (!Array.isArray(response.data))
15
+ throw new Error("CLIProxyAPI model catalog is missing the data array");
16
+ const models = response.data.map((item) => {
17
+ if (!isRecord(item) || typeof item.id !== "string" || item.id.trim() === "")
18
+ return;
19
+ return {
20
+ id: item.id,
21
+ ...typeof item.owned_by === "string" ? { ownedBy: item.owned_by } : {}
22
+ };
23
+ }).filter((item) => item !== undefined);
24
+ if (models.length === 0)
25
+ throw new Error("CLIProxyAPI returned no usable models");
26
+ const unique = new Map;
27
+ for (const model of models) {
28
+ if (!unique.has(model.id))
29
+ unique.set(model.id, model);
30
+ }
31
+ return [...unique.values()];
32
+ }
33
+ function parseReasoningEffortCatalog(input) {
34
+ if (!isRecord(input) || !Array.isArray(input.models))
35
+ return {};
36
+ const catalog = {};
37
+ for (const item of input.models) {
38
+ if (!isRecord(item) || typeof item.slug !== "string" || item.slug.trim() === "")
39
+ continue;
40
+ const rawLevels = item.supported_reasoning_levels;
41
+ if (!Array.isArray(rawLevels))
42
+ continue;
43
+ const levels = rawLevels.map((entry) => isRecord(entry) && typeof entry.effort === "string" ? entry.effort : undefined).filter((level) => level !== undefined);
44
+ if (levels.length === 0)
45
+ continue;
46
+ const defaultLevel = typeof item.default_reasoning_level === "string" ? item.default_reasoning_level : undefined;
47
+ catalog[item.slug] = {
48
+ levels: [...new Set(levels)],
49
+ ...defaultLevel ? { defaultLevel } : {}
50
+ };
51
+ }
52
+ return catalog;
53
+ }
54
+ async function discoverReasoningEfforts(input) {
55
+ const response = await (input.fetcher ?? fetch)(`${input.baseURL}/models?client_version=${REASONING_LEVELS_CLIENT_VERSION}`, {
56
+ headers: input.apiKey ? { Authorization: `Bearer ${input.apiKey}` } : undefined,
57
+ signal: AbortSignal.timeout(input.timeoutMs)
58
+ });
59
+ if (!response.ok) {
60
+ const detail = (await response.text()).trim().slice(0, 300);
61
+ throw new Error(`CLIProxyAPI reasoning-level discovery failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
62
+ }
63
+ return parseReasoningEffortCatalog(await response.json());
64
+ }
65
+ async function discoverModels(input) {
66
+ const response = await (input.fetcher ?? fetch)(`${input.baseURL}/models`, {
67
+ headers: input.apiKey ? { Authorization: `Bearer ${input.apiKey}` } : undefined,
68
+ signal: AbortSignal.timeout(input.timeoutMs)
69
+ });
70
+ if (!response.ok) {
71
+ const detail = (await response.text()).trim().slice(0, 300);
72
+ throw new Error(`CLIProxyAPI model discovery failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
73
+ }
74
+ return parseCatalog(await response.json());
75
+ }
76
+ function parseModelProtocolCatalog(input) {
77
+ if (!isRecord(input))
78
+ throw new Error("Model metadata service returned a non-object catalog");
79
+ return Object.fromEntries(Object.entries(input).flatMap(([providerID, provider]) => {
80
+ if (!isRecord(provider))
81
+ return [];
82
+ const models = Object.fromEntries(Object.entries(isRecord(provider.models) ? provider.models : {}).flatMap(([modelID, model]) => {
83
+ if (!isRecord(model) || !isRecord(model.provider) || typeof model.provider.npm !== "string") {
84
+ return [];
85
+ }
86
+ return [[modelID, model.provider.npm]];
87
+ }));
88
+ const npm = typeof provider.npm === "string" ? provider.npm : undefined;
89
+ return npm || Object.keys(models).length > 0 ? [[providerID, { ...npm ? { npm } : {}, models }]] : [];
90
+ }));
91
+ }
92
+ async function discoverModelProtocols(input) {
93
+ const response = await (input.fetcher ?? fetch)(input.url, {
94
+ signal: AbortSignal.timeout(input.timeoutMs)
95
+ });
96
+ if (!response.ok) {
97
+ const detail = (await response.text()).trim().slice(0, 300);
98
+ throw new Error(`Model protocol discovery failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}`);
99
+ }
100
+ return parseModelProtocolCatalog(await response.json());
101
+ }
102
+ function isRecord(value) {
103
+ return typeof value === "object" && value !== null && !Array.isArray(value);
104
+ }
105
+
106
+ // src/index.ts
107
+ var DEFAULT_BASE_URL = "http://localhost:8317/v1";
108
+ var DEFAULT_PROVIDER_ID = "cliproxyapi";
109
+ var DEFAULT_PROVIDER_NAME = "CLIProxyAPI";
110
+ var DEFAULT_DISCOVERY_TIMEOUT_MS = 1e4;
111
+ var DEFAULT_MODEL_METADATA_URL = "https://models.dev/api.json";
112
+ var CLIProxyAPIPlugin = async ({ client }, rawOptions) => {
113
+ const options = readOptions(rawOptions);
114
+ return {
115
+ config: async (config) => {
116
+ const providerID = options.providerID ?? DEFAULT_PROVIDER_ID;
117
+ const existing = config.provider?.[providerID];
118
+ const baseURL = normalizeBaseURL(options.baseURL ?? process.env.CLIPROXY_BASE_URL ?? stringOption(existing?.options?.baseURL) ?? DEFAULT_BASE_URL);
119
+ const apiKey = options.apiKey ?? process.env.CLIPROXY_API_KEY ?? stringOption(existing?.options?.apiKey);
120
+ try {
121
+ const timeoutMs = options.discoveryTimeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS;
122
+ const [catalog, protocolDiscovery, effortDiscovery] = await Promise.all([
123
+ discoverModels({
124
+ baseURL,
125
+ apiKey,
126
+ timeoutMs
127
+ }),
128
+ discoverProtocols({
129
+ url: options.modelMetadataURL ?? DEFAULT_MODEL_METADATA_URL,
130
+ timeoutMs
131
+ }),
132
+ discoverEffortLevels({
133
+ baseURL,
134
+ apiKey,
135
+ timeoutMs
136
+ })
137
+ ]);
138
+ const discoveryErrors = [protocolDiscovery.error, effortDiscovery.error].filter((error) => error !== undefined);
139
+ for (const message of discoveryErrors) {
140
+ await client.app.log({
141
+ body: {
142
+ service: "opencode-cliproxyapi",
143
+ level: "warn",
144
+ message
145
+ }
146
+ });
147
+ }
148
+ addProvider(config, {
149
+ providerID,
150
+ providerName: options.providerName ?? DEFAULT_PROVIDER_NAME,
151
+ baseURL,
152
+ apiKey,
153
+ protocol: options.protocol ?? "chat",
154
+ catalog,
155
+ protocolCatalog: protocolDiscovery.catalog,
156
+ effortCatalog: effortDiscovery.catalog
157
+ });
158
+ await client.app.log({
159
+ body: {
160
+ service: "opencode-cliproxyapi",
161
+ level: "info",
162
+ message: `Discovered ${catalog.length} CLIProxyAPI models from ${baseURL}`
163
+ }
164
+ });
165
+ } catch (error) {
166
+ await client.app.log({
167
+ body: {
168
+ service: "opencode-cliproxyapi",
169
+ level: "error",
170
+ message: error instanceof Error ? error.message : String(error)
171
+ }
172
+ });
173
+ throw error;
174
+ }
175
+ }
176
+ };
177
+ };
178
+ var src_default = {
179
+ id: "opencode-cliproxyapi",
180
+ server: CLIProxyAPIPlugin
181
+ };
182
+ function addProvider(config, input) {
183
+ const existing = config.provider?.[input.providerID];
184
+ const discovered = Object.fromEntries(input.catalog.map((model) => [
185
+ model.id,
186
+ {
187
+ name: displayName(model.id),
188
+ tool_call: !isImageModel(model.id),
189
+ reasoning: isReasoningModel(model.id, input.effortCatalog[model.id]),
190
+ attachment: supportsAttachments(model.id),
191
+ ...model.ownedBy && resolveModelNpm(input.protocolCatalog, model.ownedBy, model.id) === "@ai-sdk/anthropic" ? {
192
+ provider: {
193
+ npm: "@ai-sdk/anthropic"
194
+ }
195
+ } : {},
196
+ ...isImageModel(model.id) ? {
197
+ modalities: {
198
+ input: ["text", "image"],
199
+ output: ["image"]
200
+ }
201
+ } : {},
202
+ ...reasoningEffortConfig(model, input)
203
+ }
204
+ ]));
205
+ config.provider = {
206
+ ...config.provider,
207
+ [input.providerID]: {
208
+ ...existing,
209
+ name: existing?.name ?? input.providerName,
210
+ npm: existing?.npm ?? (input.protocol === "responses" ? "@ai-sdk/openai" : "@ai-sdk/openai-compatible"),
211
+ env: [...new Set([...existing?.env ?? [], "CLIPROXY_API_KEY"])],
212
+ options: {
213
+ ...existing?.options,
214
+ baseURL: input.baseURL,
215
+ ...input.apiKey ? { apiKey: input.apiKey } : {}
216
+ },
217
+ models: mergeModels(discovered, existing?.models)
218
+ }
219
+ };
220
+ }
221
+ function reasoningEffortConfig(model, input) {
222
+ const efforts = input.effortCatalog[model.id];
223
+ if (!efforts)
224
+ return {};
225
+ const anthropicRouted = model.ownedBy !== undefined && resolveModelNpm(input.protocolCatalog, model.ownedBy, model.id) === "@ai-sdk/anthropic";
226
+ if (anthropicRouted)
227
+ return {};
228
+ const levels = efforts.levels.filter((level) => level !== "none");
229
+ if (levels.length === 0)
230
+ return {};
231
+ const defaultLevel = efforts.defaultLevel !== undefined && efforts.levels.includes(efforts.defaultLevel) ? efforts.defaultLevel : undefined;
232
+ return {
233
+ ...defaultLevel ? { options: { reasoningEffort: defaultLevel } } : {},
234
+ variants: Object.fromEntries(levels.map((level) => [level, { reasoningEffort: level }]))
235
+ };
236
+ }
237
+ function resolveModelNpm(catalog, providerID, modelID) {
238
+ const provider = catalog[providerID];
239
+ return provider?.models[modelID] ?? provider?.npm;
240
+ }
241
+ function mergeModels(discovered, existing) {
242
+ const merged = { ...discovered };
243
+ for (const [modelID, model] of Object.entries(existing ?? {})) {
244
+ const discoveredModel = merged[modelID];
245
+ const providerNpm = model.provider?.npm ?? discoveredModel?.provider?.npm;
246
+ merged[modelID] = {
247
+ ...discoveredModel,
248
+ ...model,
249
+ ...providerNpm ? { provider: { npm: providerNpm } } : {}
250
+ };
251
+ }
252
+ return merged;
253
+ }
254
+ function readOptions(input) {
255
+ if (!input)
256
+ return {};
257
+ return {
258
+ baseURL: stringOption(input.baseURL),
259
+ apiKey: stringOption(input.apiKey),
260
+ providerID: stringOption(input.providerID),
261
+ providerName: stringOption(input.providerName),
262
+ protocol: input.protocol === "responses" ? "responses" : input.protocol === "chat" ? "chat" : undefined,
263
+ modelMetadataURL: input.modelMetadataURL === false ? false : stringOption(input.modelMetadataURL),
264
+ discoveryTimeoutMs: typeof input.discoveryTimeoutMs === "number" && input.discoveryTimeoutMs > 0 ? input.discoveryTimeoutMs : undefined
265
+ };
266
+ }
267
+ function discoverProtocols(input) {
268
+ if (input.url === false)
269
+ return Promise.resolve({ catalog: {} });
270
+ return discoverModelProtocols({
271
+ url: input.url,
272
+ timeoutMs: input.timeoutMs
273
+ }).then((catalog) => ({ catalog })).catch((error) => ({
274
+ catalog: {},
275
+ error: error instanceof Error ? error.message : String(error)
276
+ }));
277
+ }
278
+ function discoverEffortLevels(input) {
279
+ return discoverReasoningEfforts(input).then((catalog) => ({ catalog })).catch((error) => ({
280
+ catalog: {},
281
+ error: error instanceof Error ? error.message : String(error)
282
+ }));
283
+ }
284
+ function stringOption(value) {
285
+ return typeof value === "string" && value.trim() !== "" ? value : undefined;
286
+ }
287
+ function displayName(modelID) {
288
+ return modelID.split("-").map((part) => {
289
+ const lower = part.toLowerCase();
290
+ if (lower === "gpt")
291
+ return "GPT";
292
+ if (lower === "oss")
293
+ return "OSS";
294
+ if (lower === "codex")
295
+ return "Codex";
296
+ return part.charAt(0).toUpperCase() + part.slice(1);
297
+ }).join(" ");
298
+ }
299
+ function isImageModel(modelID) {
300
+ return /(?:^|-)image(?:-|$)/i.test(modelID);
301
+ }
302
+ function isReasoningModel(modelID, efforts) {
303
+ if (efforts?.levels.length)
304
+ return efforts.levels.some((level) => level !== "none");
305
+ return /thinking|reasoning|codex|gpt-(?:5|oss)/i.test(modelID);
306
+ }
307
+ function supportsAttachments(modelID) {
308
+ return /^(?:claude|gemini|gpt)/i.test(modelID);
309
+ }
310
+ export {
311
+ CLIProxyAPIPlugin,
312
+ src_default as default,
313
+ discoverModelProtocols,
314
+ discoverModels,
315
+ discoverReasoningEfforts,
316
+ normalizeBaseURL,
317
+ parseCatalog,
318
+ parseModelProtocolCatalog,
319
+ parseReasoningEffortCatalog
320
+ };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/package.json",
3
+ "name": "@darknesskiller/opencode-cliproxyapi",
4
+ "version": "0.2.0",
5
+ "description": "Fork of opencode-cliproxyapi that discovers CLIProxyAPI models and auto-configures reasoning-effort variants in OpenCode.",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "author": "İbrahim BABAL",
9
+ "homepage": "https://github.com/DarknessKiller/opencode-cliproxyapi#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/DarknessKiller/opencode-cliproxyapi.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/DarknessKiller/opencode-cliproxyapi/issues"
16
+ },
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md",
28
+ "CHANGELOG.md",
29
+ "LICENSE"
30
+ ],
31
+ "scripts": {
32
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun --format esm --external @opencode-ai/plugin && tsc --emitDeclarationOnly",
33
+ "typecheck": "tsc --noEmit",
34
+ "test": "bun test ./src",
35
+ "check": "bun run typecheck && bun run test && bun run build",
36
+ "prepublishOnly": "bun run check"
37
+ },
38
+ "keywords": [
39
+ "opencode",
40
+ "cliproxyapi",
41
+ "llm",
42
+ "plugin"
43
+ ],
44
+ "devDependencies": {
45
+ "@opencode-ai/plugin": "1.18.29",
46
+ "@types/bun": "^1.3.5",
47
+ "typescript": "^5.9.3"
48
+ },
49
+ "engines": {
50
+ "bun": ">=1.3.0"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ }
55
+ }