@darknesskiller/opencode-cliproxyapi 0.2.1 → 0.3.1

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 CHANGED
@@ -5,6 +5,33 @@ All notable changes to this project are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
6
  and this project follows [Semantic Versioning](https://semver.org/).
7
7
 
8
+ ## [0.3.1] - Unreleased
9
+
10
+ ### Fixed
11
+
12
+ - Image attachments now reach discovered Claude, Gemini, and GPT models.
13
+ OpenCode only forwards image parts to models that declare image input, and
14
+ the plugin previously declared it only for `*-image-*` generation models, so
15
+ other models received a `Cannot read "<file>"` placeholder instead. V1 now
16
+ sets `modalities: { input: ["text", "image"], output: ["text"] }` and V2
17
+ sets the matching `capabilities.input`, both from one shared policy.
18
+ Text-only `gpt-oss-*` models are no longer marked as attachment-capable.
19
+
20
+ ## [0.3.0] - 2026-10-05
21
+
22
+ ### Added
23
+
24
+ - OpenCode v2 support. The package now default-exports a definition object with
25
+ `id`, `setup`, and `server`: OpenCode v2 loads it through `setup(ctx)` and
26
+ registers the provider with `ctx.provider.transform` (discovered catalog,
27
+ `@opencode/ai/providers/*` driver package, `baseURL`/`apiKey` settings),
28
+ while OpenCode v1 (1.18.29 and newer) keeps calling `server(ctx)`.
29
+ Anthropic-endpoint models are routed through
30
+ `@opencode/ai/providers/anthropic` via the per-model `package` field, and
31
+ v2 variants list exactly the effort levels the server reported.
32
+ - `index.ts` at the repository root so a local checkout can be used as the
33
+ plugin path without a build step.
34
+
8
35
  ## [0.2.1] - 2026-09-09
9
36
 
10
37
  ### Fixed
@@ -63,7 +90,8 @@ and this project follows [Semantic Versioning](https://semver.org/).
63
90
  - Automatic model names and capability hints in OpenCode's model picker.
64
91
  - Local plugin and npm package installation flows.
65
92
 
66
- [Unreleased]: https://github.com/DarknessKiller/opencode-cliproxyapi/compare/v0.2.1...HEAD
93
+ [Unreleased]: https://github.com/DarknessKiller/opencode-cliproxyapi/compare/v0.3.0...HEAD
94
+ [0.3.0]: https://github.com/DarknessKiller/opencode-cliproxyapi/compare/v0.2.1...v0.3.0
67
95
  [0.2.1]: https://github.com/DarknessKiller/opencode-cliproxyapi/compare/v0.2.0...v0.2.1
68
96
  [0.2.0]: https://github.com/DarknessKiller/opencode-cliproxyapi/compare/v0.1.2...v0.2.0
69
97
  [0.1.2]: https://github.com/DarknessKiller/opencode-cliproxyapi/compare/v0.1.1...v0.1.2
package/README.md CHANGED
@@ -173,6 +173,50 @@ can be customized:
173
173
  }
174
174
  ```
175
175
 
176
+ ### OpenCode v2
177
+
178
+ OpenCode v2 replaced the `plugin` array with `plugins` and no longer runs v1
179
+ plugin code. This package ships a dual entrypoint: v2 calls `setup(ctx)` and
180
+ v1 (1.18.29 and newer) calls `server(ctx)`, so one configuration works on both.
181
+
182
+ ```jsonc
183
+ // OpenCode v2 (~/.config/opencode/opencode.jsonc)
184
+ {
185
+ "$schema": "https://opencode.ai/config.json",
186
+ "plugins": [
187
+ {
188
+ "package": "@darknesskiller/opencode-cliproxyapi",
189
+ "options": {
190
+ "baseURL": "http://your-server:8317",
191
+ "apiKey": "your-cli-proxy-api-key"
192
+ }
193
+ }
194
+ ]
195
+ }
196
+ ```
197
+
198
+ On v2 the plugin registers the provider itself: it publishes the discovered
199
+ catalog through `ctx.provider.transform`, selects
200
+ `@opencode/ai/providers/openai-compatible` (or `@opencode/ai/providers/openai`
201
+ with `protocol: "responses"`), and routes Anthropic-endpoint models through
202
+ `@opencode/ai/providers/anthropic`. Every option in the table above works
203
+ unchanged inside the plugin entry's `options`.
204
+
205
+ To run a checkout instead of the published package, point `package` at the
206
+ repository directory (OpenCode resolves its root `index.ts`) and use the local
207
+ path:
208
+
209
+ ```jsonc
210
+ {
211
+ "plugins": [
212
+ {
213
+ "package": "/path/to/opencode-cliproxyapi",
214
+ "options": { "baseURL": "http://your-server:8317" }
215
+ }
216
+ ]
217
+ }
218
+ ```
219
+
176
220
  ## Troubleshooting
177
221
 
178
222
  ### `Missing API key`
@@ -196,6 +240,28 @@ Then restart OpenCode and run:
196
240
  opencode models cliproxyapi
197
241
  ```
198
242
 
243
+ ### The model says it cannot read an attached image
244
+
245
+ CLIProxyAPI's `/v1/models` only reports model ids, so image input is inferred
246
+ from the model family: discovered models whose id starts with `claude`,
247
+ `gemini`, or `gpt` (except text-only `gpt-oss-*`) accept images, and
248
+ `*-image-*` models are treated as image generators. New releases in those
249
+ families are covered automatically. On OpenCode v1, declare the modalities of
250
+ a vision model outside these families under the provider's `models` in
251
+ `opencode.json`; user entries override the discovered metadata:
252
+
253
+ ```jsonc
254
+ {
255
+ "provider": {
256
+ "cliproxyapi": {
257
+ "models": {
258
+ "your-vision-model": { "attachment": true, "modalities": { "input": ["text", "image"], "output": ["text"] } }
259
+ }
260
+ }
261
+ }
262
+ }
263
+ ```
264
+
199
265
  ### Environment configuration works in one terminal but not another
200
266
 
201
267
  Move the connection to the recommended global OpenCode config, or add the
package/dist/index.d.ts CHANGED
@@ -1,8 +1,14 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin";
2
+ import { setup } from "./plugin-v2.js";
2
3
  export declare const CLIProxyAPIPlugin: Plugin;
4
+ /**
5
+ * Dual V1/V2 entry point. OpenCode V2 calls `setup(ctx)`; V1 (1.18.29 and
6
+ * newer) calls `server(ctx)` for the classic hooks object.
7
+ */
3
8
  declare const _default: {
4
9
  id: string;
5
10
  server: Plugin;
11
+ setup: typeof setup;
6
12
  };
7
13
  export default _default;
8
14
  export { discoverModelProtocols, discoverModels, discoverReasoningEfforts, normalizeBaseURL, parseCatalog, parseModelProtocolCatalog, parseReasoningEffortCatalog, } from "./catalog.js";
@@ -1 +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;AAmCtF,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,MAAM,EAAgB,MAAM,qBAAqB,CAAA;AAwBvE,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAWtC,eAAO,MAAM,iBAAiB,EAAE,MAiE/B,CAAA;AAED;;;GAGG;;;;;;AACH,wBAIkD;AAClD,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 CHANGED
@@ -103,12 +103,185 @@ function isRecord(value) {
103
103
  return typeof value === "object" && value !== null && !Array.isArray(value);
104
104
  }
105
105
 
106
- // src/index.ts
106
+ // src/shared.ts
107
107
  var DEFAULT_BASE_URL = "http://localhost:8317/v1";
108
108
  var DEFAULT_PROVIDER_ID = "cliproxyapi";
109
109
  var DEFAULT_PROVIDER_NAME = "CLIProxyAPI";
110
110
  var DEFAULT_DISCOVERY_TIMEOUT_MS = 1e4;
111
111
  var DEFAULT_MODEL_METADATA_URL = "https://models.dev/api.json";
112
+ var V1_CHAT_PACKAGE = "@ai-sdk/openai-compatible";
113
+ var V1_RESPONSES_PACKAGE = "@ai-sdk/openai";
114
+ var V1_ANTHROPIC_PACKAGE = "@ai-sdk/anthropic";
115
+ var V2_CHAT_PACKAGE = "@opencode/ai/providers/openai-compatible";
116
+ var V2_RESPONSES_PACKAGE = "@opencode/ai/providers/openai";
117
+ var V2_ANTHROPIC_PACKAGE = "@opencode/ai/providers/anthropic";
118
+ function readOptions(input) {
119
+ if (!input)
120
+ return {};
121
+ return {
122
+ baseURL: stringOption(input.baseURL),
123
+ apiKey: stringOption(input.apiKey),
124
+ providerID: stringOption(input.providerID),
125
+ providerName: stringOption(input.providerName),
126
+ protocol: input.protocol === "responses" ? "responses" : input.protocol === "chat" ? "chat" : undefined,
127
+ modelMetadataURL: input.modelMetadataURL === false ? false : stringOption(input.modelMetadataURL),
128
+ discoveryTimeoutMs: typeof input.discoveryTimeoutMs === "number" && input.discoveryTimeoutMs > 0 ? input.discoveryTimeoutMs : undefined
129
+ };
130
+ }
131
+ function stringOption(value) {
132
+ return typeof value === "string" && value.trim() !== "" ? value : undefined;
133
+ }
134
+ function displayName(modelID) {
135
+ return modelID.split("-").map((part) => {
136
+ const lower = part.toLowerCase();
137
+ if (lower === "gpt")
138
+ return "GPT";
139
+ if (lower === "oss")
140
+ return "OSS";
141
+ if (lower === "codex")
142
+ return "Codex";
143
+ return part.charAt(0).toUpperCase() + part.slice(1);
144
+ }).join(" ");
145
+ }
146
+ function isImageModel(modelID) {
147
+ return /(?:^|-)image(?:-|$)/i.test(modelID);
148
+ }
149
+ function isReasoningModel(modelID, efforts) {
150
+ if (efforts?.levels.length)
151
+ return efforts.levels.some((level) => level !== "none");
152
+ return /thinking|reasoning|codex|gpt-(?:5|oss)/i.test(modelID);
153
+ }
154
+ function supportsAttachments(modelID) {
155
+ return /^(?:claude|gemini|gpt)/i.test(modelID) && !/^gpt-oss(?:-|$)/i.test(modelID);
156
+ }
157
+ function modelModalities(modelID) {
158
+ const imageOutput = isImageModel(modelID);
159
+ return {
160
+ input: imageOutput || supportsAttachments(modelID) ? ["text", "image"] : ["text"],
161
+ output: imageOutput ? ["image"] : ["text"]
162
+ };
163
+ }
164
+ function resolveModelNpm(catalog, providerID, modelID) {
165
+ const provider = catalog[providerID];
166
+ return provider?.models[modelID] ?? provider?.npm;
167
+ }
168
+ function isAnthropicRouted(model, protocolCatalog) {
169
+ return model.ownedBy !== undefined && resolveModelNpm(protocolCatalog, model.ownedBy, model.id) === V1_ANTHROPIC_PACKAGE;
170
+ }
171
+ function supportedEffortLevels(model, input) {
172
+ const efforts = input.effortCatalog[model.id];
173
+ if (!efforts)
174
+ return;
175
+ if (isAnthropicRouted(model, input.protocolCatalog))
176
+ return;
177
+ const levels = efforts.levels.filter((level) => level !== "none");
178
+ return levels.length > 0 ? levels : undefined;
179
+ }
180
+ async function discoverCatalog(input) {
181
+ const [catalog, protocolDiscovery, effortDiscovery] = await Promise.all([
182
+ discoverModels({ baseURL: input.baseURL, apiKey: input.apiKey, timeoutMs: input.timeoutMs }),
183
+ discoverProtocols({ url: input.modelMetadataURL, timeoutMs: input.timeoutMs }),
184
+ discoverEffortLevels({ baseURL: input.baseURL, apiKey: input.apiKey, timeoutMs: input.timeoutMs })
185
+ ]);
186
+ return {
187
+ catalog,
188
+ protocolCatalog: protocolDiscovery.catalog,
189
+ effortCatalog: effortDiscovery.catalog,
190
+ errors: [protocolDiscovery.error, effortDiscovery.error].filter((error) => error !== undefined)
191
+ };
192
+ }
193
+ function discoverProtocols(input) {
194
+ if (input.url === false)
195
+ return Promise.resolve({ catalog: {} });
196
+ return discoverModelProtocols({
197
+ url: input.url,
198
+ timeoutMs: input.timeoutMs
199
+ }).then((catalog) => ({ catalog })).catch((error) => ({
200
+ catalog: {},
201
+ error: error instanceof Error ? error.message : String(error)
202
+ }));
203
+ }
204
+ function discoverEffortLevels(input) {
205
+ return discoverReasoningEfforts(input).then((catalog) => ({ catalog })).catch((error) => ({
206
+ catalog: {},
207
+ error: error instanceof Error ? error.message : String(error)
208
+ }));
209
+ }
210
+
211
+ // src/plugin-v2.ts
212
+ var DEFAULT_CONTEXT_LIMIT = 128000;
213
+ var DEFAULT_OUTPUT_LIMIT = 4096;
214
+ function toV2Model(model, input) {
215
+ const image = isImageModel(model.id);
216
+ const modalities = modelModalities(model.id);
217
+ const levels = supportedEffortLevels(model, input.discovery);
218
+ const anthropic = isAnthropicRouted(model, input.discovery.protocolCatalog);
219
+ return {
220
+ id: model.id,
221
+ modelID: model.id,
222
+ providerID: input.providerID,
223
+ name: displayName(model.id),
224
+ ...anthropic ? { package: V2_ANTHROPIC_PACKAGE } : {},
225
+ capabilities: {
226
+ tools: !image,
227
+ input: modalities.input,
228
+ output: modalities.output
229
+ },
230
+ variants: (levels ?? []).map((level) => ({
231
+ id: level,
232
+ settings: { reasoningEffort: level }
233
+ })),
234
+ time: { released: 0 },
235
+ cost: [{ input: 0, output: 0, cache: { read: 0, write: 0 } }],
236
+ status: "active",
237
+ enabled: true,
238
+ limit: { context: DEFAULT_CONTEXT_LIMIT, output: DEFAULT_OUTPUT_LIMIT }
239
+ };
240
+ }
241
+ function toV2Models(input) {
242
+ return input.discovery.catalog.map((model) => toV2Model(model, { providerID: input.providerID, discovery: input.discovery }));
243
+ }
244
+ async function setup(ctx) {
245
+ const options = readOptions(ctx.options);
246
+ const providerID = options.providerID ?? DEFAULT_PROVIDER_ID;
247
+ const baseURL = normalizeBaseURL(options.baseURL ?? process.env.CLIPROXY_BASE_URL ?? DEFAULT_BASE_URL);
248
+ const apiKey = options.apiKey ?? process.env.CLIPROXY_API_KEY;
249
+ const timeoutMs = options.discoveryTimeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS;
250
+ const modelMetadataURL = options.modelMetadataURL ?? DEFAULT_MODEL_METADATA_URL;
251
+ const discovery = await discoverCatalog({ baseURL, apiKey, modelMetadataURL, timeoutMs });
252
+ for (const error of discovery.errors) {
253
+ console.warn(`[cliproxyapi] ${error}`);
254
+ }
255
+ const models = toV2Models({ providerID, discovery });
256
+ const providerPackage = options.protocol === "responses" ? V2_RESPONSES_PACKAGE : V2_CHAT_PACKAGE;
257
+ await ctx.provider.transform((editor) => {
258
+ const settings = { baseURL };
259
+ if (apiKey)
260
+ settings.apiKey = apiKey;
261
+ const existing = editor.get(providerID);
262
+ if (existing) {
263
+ editor.update(providerID, (provider) => {
264
+ provider.name = provider.name || options.providerName || DEFAULT_PROVIDER_NAME;
265
+ provider.activation = "enabled";
266
+ provider.package = providerPackage;
267
+ provider.settings = { ...provider.settings, ...settings };
268
+ });
269
+ editor.models.set(providerID, models);
270
+ return;
271
+ }
272
+ const info = {
273
+ id: providerID,
274
+ name: options.providerName ?? DEFAULT_PROVIDER_NAME,
275
+ activation: "enabled",
276
+ package: providerPackage,
277
+ settings
278
+ };
279
+ editor.add({ info, models });
280
+ });
281
+ return async () => {};
282
+ }
283
+
284
+ // src/index.ts
112
285
  var CLIProxyAPIPlugin = async ({ client }, rawOptions) => {
113
286
  const options = readOptions(rawOptions);
114
287
  return {
@@ -119,24 +292,13 @@ var CLIProxyAPIPlugin = async ({ client }, rawOptions) => {
119
292
  const apiKey = options.apiKey ?? process.env.CLIPROXY_API_KEY ?? stringOption(existing?.options?.apiKey);
120
293
  try {
121
294
  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) {
295
+ const discovery = await discoverCatalog({
296
+ baseURL,
297
+ apiKey,
298
+ modelMetadataURL: options.modelMetadataURL ?? DEFAULT_MODEL_METADATA_URL,
299
+ timeoutMs
300
+ });
301
+ for (const message of discovery.errors) {
140
302
  await client.app.log({
141
303
  body: {
142
304
  service: "opencode-cliproxyapi",
@@ -151,15 +313,15 @@ var CLIProxyAPIPlugin = async ({ client }, rawOptions) => {
151
313
  baseURL,
152
314
  apiKey,
153
315
  protocol: options.protocol ?? "chat",
154
- catalog,
155
- protocolCatalog: protocolDiscovery.catalog,
156
- effortCatalog: effortDiscovery.catalog
316
+ catalog: discovery.catalog,
317
+ protocolCatalog: discovery.protocolCatalog,
318
+ effortCatalog: discovery.effortCatalog
157
319
  });
158
320
  await client.app.log({
159
321
  body: {
160
322
  service: "opencode-cliproxyapi",
161
323
  level: "info",
162
- message: `Discovered ${catalog.length} CLIProxyAPI models from ${baseURL}`
324
+ message: `Discovered ${discovery.catalog.length} CLIProxyAPI models from ${baseURL}`
163
325
  }
164
326
  });
165
327
  } catch (error) {
@@ -177,7 +339,8 @@ var CLIProxyAPIPlugin = async ({ client }, rawOptions) => {
177
339
  };
178
340
  var src_default = {
179
341
  id: "opencode-cliproxyapi",
180
- server: CLIProxyAPIPlugin
342
+ server: CLIProxyAPIPlugin,
343
+ setup
181
344
  };
182
345
  function addProvider(config, input) {
183
346
  const existing = config.provider?.[input.providerID];
@@ -188,17 +351,12 @@ function addProvider(config, input) {
188
351
  tool_call: !isImageModel(model.id),
189
352
  reasoning: isReasoningModel(model.id, input.effortCatalog[model.id]),
190
353
  attachment: supportsAttachments(model.id),
191
- ...model.ownedBy && resolveModelNpm(input.protocolCatalog, model.ownedBy, model.id) === "@ai-sdk/anthropic" ? {
354
+ ...model.ownedBy && resolveModelNpm(input.protocolCatalog, model.ownedBy, model.id) === V1_ANTHROPIC_PACKAGE ? {
192
355
  provider: {
193
- npm: "@ai-sdk/anthropic"
194
- }
195
- } : {},
196
- ...isImageModel(model.id) ? {
197
- modalities: {
198
- input: ["text", "image"],
199
- output: ["image"]
356
+ npm: V1_ANTHROPIC_PACKAGE
200
357
  }
201
358
  } : {},
359
+ ...imageModalities(model.id),
202
360
  ...reasoningEffortConfig(model, input)
203
361
  }
204
362
  ]));
@@ -207,7 +365,7 @@ function addProvider(config, input) {
207
365
  [input.providerID]: {
208
366
  ...existing,
209
367
  name: existing?.name ?? input.providerName,
210
- npm: existing?.npm ?? (input.protocol === "responses" ? "@ai-sdk/openai" : "@ai-sdk/openai-compatible"),
368
+ npm: existing?.npm ?? (input.protocol === "responses" ? V1_RESPONSES_PACKAGE : V1_CHAT_PACKAGE),
211
369
  env: [...new Set([...existing?.env ?? [], "CLIPROXY_API_KEY"])],
212
370
  options: {
213
371
  ...existing?.options,
@@ -218,15 +376,18 @@ function addProvider(config, input) {
218
376
  }
219
377
  };
220
378
  }
379
+ function imageModalities(modelID) {
380
+ const modalities = modelModalities(modelID);
381
+ if (!modalities.input.includes("image") && !modalities.output.includes("image"))
382
+ return {};
383
+ return { modalities };
384
+ }
221
385
  function reasoningEffortConfig(model, input) {
222
386
  const efforts = input.effortCatalog[model.id];
223
387
  if (!efforts)
224
388
  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)
389
+ const levels = supportedEffortLevels(model, input);
390
+ if (!levels)
230
391
  return {};
231
392
  const defaultLevel = efforts.defaultLevel !== undefined && efforts.levels.includes(efforts.defaultLevel) ? efforts.defaultLevel : undefined;
232
393
  return {
@@ -245,10 +406,6 @@ function disabledVariants(supported) {
245
406
  { disabled: true }
246
407
  ]));
247
408
  }
248
- function resolveModelNpm(catalog, providerID, modelID) {
249
- const provider = catalog[providerID];
250
- return provider?.models[modelID] ?? provider?.npm;
251
- }
252
409
  function mergeModels(discovered, existing) {
253
410
  const merged = { ...discovered };
254
411
  for (const [modelID, model] of Object.entries(existing ?? {})) {
@@ -262,62 +419,6 @@ function mergeModels(discovered, existing) {
262
419
  }
263
420
  return merged;
264
421
  }
265
- function readOptions(input) {
266
- if (!input)
267
- return {};
268
- return {
269
- baseURL: stringOption(input.baseURL),
270
- apiKey: stringOption(input.apiKey),
271
- providerID: stringOption(input.providerID),
272
- providerName: stringOption(input.providerName),
273
- protocol: input.protocol === "responses" ? "responses" : input.protocol === "chat" ? "chat" : undefined,
274
- modelMetadataURL: input.modelMetadataURL === false ? false : stringOption(input.modelMetadataURL),
275
- discoveryTimeoutMs: typeof input.discoveryTimeoutMs === "number" && input.discoveryTimeoutMs > 0 ? input.discoveryTimeoutMs : undefined
276
- };
277
- }
278
- function discoverProtocols(input) {
279
- if (input.url === false)
280
- return Promise.resolve({ catalog: {} });
281
- return discoverModelProtocols({
282
- url: input.url,
283
- timeoutMs: input.timeoutMs
284
- }).then((catalog) => ({ catalog })).catch((error) => ({
285
- catalog: {},
286
- error: error instanceof Error ? error.message : String(error)
287
- }));
288
- }
289
- function discoverEffortLevels(input) {
290
- return discoverReasoningEfforts(input).then((catalog) => ({ catalog })).catch((error) => ({
291
- catalog: {},
292
- error: error instanceof Error ? error.message : String(error)
293
- }));
294
- }
295
- function stringOption(value) {
296
- return typeof value === "string" && value.trim() !== "" ? value : undefined;
297
- }
298
- function displayName(modelID) {
299
- return modelID.split("-").map((part) => {
300
- const lower = part.toLowerCase();
301
- if (lower === "gpt")
302
- return "GPT";
303
- if (lower === "oss")
304
- return "OSS";
305
- if (lower === "codex")
306
- return "Codex";
307
- return part.charAt(0).toUpperCase() + part.slice(1);
308
- }).join(" ");
309
- }
310
- function isImageModel(modelID) {
311
- return /(?:^|-)image(?:-|$)/i.test(modelID);
312
- }
313
- function isReasoningModel(modelID, efforts) {
314
- if (efforts?.levels.length)
315
- return efforts.levels.some((level) => level !== "none");
316
- return /thinking|reasoning|codex|gpt-(?:5|oss)/i.test(modelID);
317
- }
318
- function supportsAttachments(modelID) {
319
- return /^(?:claude|gemini|gpt)/i.test(modelID);
320
- }
321
422
  export {
322
423
  CLIProxyAPIPlugin,
323
424
  src_default as default,
@@ -0,0 +1,68 @@
1
+ /**
2
+ * OpenCode V2 entrypoint for CLIProxyAPI.
3
+ *
4
+ * V2 plugins default-export a definition with an `id` and a `setup(ctx)`
5
+ * function; V1 plugin functions do not run on V2. This module ports the
6
+ * discovery pipeline to the V2 provider API: it resolves the connector
7
+ * options, runs the same catalog/protocol/effort discovery as V1, and
8
+ * publishes the resulting provider through `ctx.provider.transform`.
9
+ *
10
+ * All `@opencode/plugin` imports are type-only, so the built plugin has no
11
+ * runtime dependency on the V2 SDK.
12
+ */
13
+ import type { Plugin } from "@opencode/plugin";
14
+ import { type CatalogDiscovery } from "./shared.js";
15
+ type Context = Plugin.Context;
16
+ export interface V2ModelVariant {
17
+ id: string;
18
+ settings?: Record<string, unknown>;
19
+ }
20
+ export interface V2ModelInfo {
21
+ id: string;
22
+ modelID: string;
23
+ providerID: string;
24
+ name: string;
25
+ package?: string;
26
+ capabilities: {
27
+ tools: boolean;
28
+ input: string[];
29
+ output: string[];
30
+ };
31
+ variants: V2ModelVariant[];
32
+ time: {
33
+ released: number;
34
+ };
35
+ cost: Array<{
36
+ input: number;
37
+ output: number;
38
+ cache: {
39
+ read: number;
40
+ write: number;
41
+ };
42
+ }>;
43
+ status: "active";
44
+ enabled: boolean;
45
+ limit: {
46
+ context: number;
47
+ output: number;
48
+ };
49
+ }
50
+ export declare function toV2Model(model: CatalogDiscovery["catalog"][number], input: {
51
+ providerID: string;
52
+ discovery: CatalogDiscovery;
53
+ }): V2ModelInfo;
54
+ export declare function toV2Models(input: {
55
+ providerID: string;
56
+ discovery: CatalogDiscovery;
57
+ }): V2ModelInfo[];
58
+ /**
59
+ * V2 setup. Registers the provider from the discovered catalog and returns a
60
+ * no-op cleanup (V2 keeps no timers or subscriptions for this plugin).
61
+ */
62
+ export declare function setup(ctx: Context): Promise<() => Promise<void>>;
63
+ export declare const v2Definition: {
64
+ id: string;
65
+ setup: typeof setup;
66
+ };
67
+ export {};
68
+ //# sourceMappingURL=plugin-v2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-v2.d.ts","sourceRoot":"","sources":["../src/plugin-v2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAS,MAAM,EAAY,MAAM,kBAAkB,CAAA;AAE/D,OAAO,EAgBL,KAAK,gBAAgB,EAEtB,MAAM,aAAa,CAAA;AAEpB,KAAK,OAAO,GAAG,MAAM,CAAC,OAAO,CAAA;AAK7B,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAA;QACd,KAAK,EAAE,MAAM,EAAE,CAAA;QACf,MAAM,EAAE,MAAM,EAAE,CAAA;KACjB,CAAA;IACD,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,IAAI,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAA;KACjB,CAAA;IACD,IAAI,EAAE,KAAK,CAAC;QACV,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;QACd,KAAK,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAA;KACvC,CAAC,CAAA;IACF,MAAM,EAAE,QAAQ,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE;QACL,OAAO,EAAE,MAAM,CAAA;QACf,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;CACF;AAED,wBAAgB,SAAS,CACvB,KAAK,EAAE,gBAAgB,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,EAC1C,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,gBAAgB,CAAA;CAAE,GACzD,WAAW,CA6Bb;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE;IAChC,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,gBAAgB,CAAA;CAC5B,GAAG,WAAW,EAAE,CAIhB;AAED;;;GAGG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CA8CtE;AAED,eAAO,MAAM,YAAY;;;CAGxB,CAAA"}
@@ -0,0 +1,55 @@
1
+ import type { PluginOptions } from "@opencode-ai/plugin";
2
+ import { type CatalogModel, type ModelProtocolCatalog, type ReasoningEffortCatalog } from "./catalog.js";
3
+ export declare const DEFAULT_BASE_URL = "http://localhost:8317/v1";
4
+ export declare const DEFAULT_PROVIDER_ID = "cliproxyapi";
5
+ export declare const DEFAULT_PROVIDER_NAME = "CLIProxyAPI";
6
+ export declare const DEFAULT_DISCOVERY_TIMEOUT_MS = 10000;
7
+ export declare const DEFAULT_MODEL_METADATA_URL = "https://models.dev/api.json";
8
+ export declare const V1_CHAT_PACKAGE = "@ai-sdk/openai-compatible";
9
+ export declare const V1_RESPONSES_PACKAGE = "@ai-sdk/openai";
10
+ export declare const V1_ANTHROPIC_PACKAGE = "@ai-sdk/anthropic";
11
+ export declare const V2_CHAT_PACKAGE = "@opencode/ai/providers/openai-compatible";
12
+ export declare const V2_RESPONSES_PACKAGE = "@opencode/ai/providers/openai";
13
+ export declare const V2_ANTHROPIC_PACKAGE = "@opencode/ai/providers/anthropic";
14
+ export type ConnectorOptions = {
15
+ baseURL?: string;
16
+ apiKey?: string;
17
+ providerID?: string;
18
+ providerName?: string;
19
+ protocol?: "chat" | "responses";
20
+ modelMetadataURL?: string | false;
21
+ discoveryTimeoutMs?: number;
22
+ };
23
+ export type CatalogDiscovery = {
24
+ catalog: CatalogModel[];
25
+ protocolCatalog: ModelProtocolCatalog;
26
+ effortCatalog: ReasoningEffortCatalog;
27
+ errors: string[];
28
+ };
29
+ export declare function readOptions(input?: PluginOptions): ConnectorOptions;
30
+ export declare function stringOption(value: unknown): string | undefined;
31
+ export declare function displayName(modelID: string): string;
32
+ export declare function isImageModel(modelID: string): boolean;
33
+ export declare function isReasoningModel(modelID: string, efforts?: {
34
+ levels: string[];
35
+ defaultLevel?: string;
36
+ }): boolean;
37
+ export declare function supportsAttachments(modelID: string): boolean;
38
+ export type ModelModalities = {
39
+ input: Array<"text" | "image">;
40
+ output: Array<"text" | "image">;
41
+ };
42
+ export declare function modelModalities(modelID: string): ModelModalities;
43
+ export declare function resolveModelNpm(catalog: ModelProtocolCatalog, providerID: string, modelID: string): string | undefined;
44
+ export declare function isAnthropicRouted(model: CatalogModel, protocolCatalog: ModelProtocolCatalog): boolean;
45
+ export declare function supportedEffortLevels(model: CatalogModel, input: {
46
+ protocolCatalog: ModelProtocolCatalog;
47
+ effortCatalog: ReasoningEffortCatalog;
48
+ }): string[] | undefined;
49
+ export declare function discoverCatalog(input: {
50
+ baseURL: string;
51
+ apiKey?: string;
52
+ modelMetadataURL: string | false;
53
+ timeoutMs: number;
54
+ }): Promise<CatalogDiscovery>;
55
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@darknesskiller/opencode-cliproxyapi",
4
- "version": "0.2.1",
4
+ "version": "0.3.1",
5
5
  "description": "Fork of opencode-cliproxyapi that discovers CLIProxyAPI models and auto-configures reasoning-effort variants in OpenCode.",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -29,7 +29,7 @@
29
29
  "LICENSE"
30
30
  ],
31
31
  "scripts": {
32
- "build": "bun build ./src/index.ts --outdir ./dist --target bun --format esm --external @opencode-ai/plugin && tsc --emitDeclarationOnly",
32
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun --format esm --external @opencode-ai/plugin --external @opencode/plugin && tsc --emitDeclarationOnly",
33
33
  "typecheck": "tsc --noEmit",
34
34
  "test": "bun test ./src",
35
35
  "check": "bun run typecheck && bun run test && bun run build",
@@ -43,6 +43,7 @@
43
43
  ],
44
44
  "devDependencies": {
45
45
  "@opencode-ai/plugin": "1.18.29",
46
+ "@opencode/plugin": "^2.0.18",
46
47
  "@types/bun": "^1.3.5",
47
48
  "typescript": "^5.9.3"
48
49
  },