@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 +59 -0
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/dist/catalog.d.ts +35 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +320 -0
- package/package.json +55 -0
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
|
+
[](https://github.com/DarknessKiller/opencode-cliproxyapi/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@darknesskiller/opencode-cliproxyapi)
|
|
5
|
+
[](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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|