cursor-opencode-provider 0.8.0 → 0.8.3
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 +174 -0
- package/README.md +230 -44
- package/SECURITY.md +60 -12
- package/dist/activity.d.ts +73 -1
- package/dist/activity.js +231 -5
- package/dist/agent-url.js +37 -9
- package/dist/auth-renewal.d.ts +1 -1
- package/dist/auth-renewal.js +58 -26
- package/dist/billing.d.ts +105 -0
- package/dist/billing.js +212 -0
- package/dist/classic-tool-results.d.ts +1 -0
- package/dist/classic-tool-results.js +42 -0
- package/dist/context/build.d.ts +23 -4
- package/dist/context/build.js +65 -10
- package/dist/context/dynamic-catalog.js +6 -1
- package/dist/context/env.js +27 -3
- package/dist/context/epoch.js +9 -5
- package/dist/context/frozen.js +6 -2
- package/dist/context/git.d.ts +5 -2
- package/dist/context/git.js +17 -7
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/context/paths.d.ts +45 -18
- package/dist/context/paths.js +72 -22
- package/dist/context/skills-bridge.d.ts +22 -0
- package/dist/context/skills-bridge.js +20 -0
- package/dist/context/skills.d.ts +34 -0
- package/dist/context/skills.js +211 -0
- package/dist/debug.js +13 -2
- package/dist/early-tool-rows.d.ts +60 -0
- package/dist/early-tool-rows.js +96 -0
- package/dist/errors.d.ts +49 -7
- package/dist/errors.js +163 -8
- package/dist/host-agent-mode.d.ts +53 -4
- package/dist/host-agent-mode.js +117 -13
- package/dist/host-interrupt.d.ts +12 -0
- package/dist/host-interrupt.js +18 -0
- package/dist/host-plan-file.d.ts +28 -0
- package/dist/host-plan-file.js +76 -0
- package/dist/host-steer.d.ts +71 -0
- package/dist/host-steer.js +232 -0
- package/dist/image-input.d.ts +18 -0
- package/dist/image-input.js +122 -30
- package/dist/image-save.d.ts +10 -2
- package/dist/image-save.js +33 -21
- package/dist/index.d.ts +2 -2
- package/dist/language-model.d.ts +149 -30
- package/dist/language-model.js +3127 -673
- package/dist/lost-request.d.ts +39 -0
- package/dist/lost-request.js +111 -0
- package/dist/model-config.d.ts +12 -1
- package/dist/model-config.js +78 -30
- package/dist/models.d.ts +26 -3
- package/dist/models.js +101 -45
- package/dist/opencode2/catalog.d.ts +1 -0
- package/dist/opencode2/catalog.js +2 -0
- package/dist/opencode2/image-save-tool.d.ts +8 -0
- package/dist/opencode2/image-save-tool.js +84 -0
- package/dist/opencode2/tool-results.d.ts +18 -0
- package/dist/opencode2/tool-results.js +66 -0
- package/dist/opencode2/types.d.ts +15 -0
- package/dist/parallel-step.d.ts +65 -0
- package/dist/parallel-step.js +102 -0
- package/dist/plugin-opencode2.js +233 -65
- package/dist/plugin.js +97 -23
- package/dist/pricing-data.d.ts +40 -3
- package/dist/pricing-data.js +40 -3
- package/dist/pricing.d.ts +1 -1
- package/dist/pricing.js +7 -4
- package/dist/process-shared.d.ts +14 -0
- package/dist/process-shared.js +18 -0
- package/dist/protocol/apply-patch.d.ts +25 -13
- package/dist/protocol/apply-patch.js +79 -30
- package/dist/protocol/ask-question.d.ts +12 -1
- package/dist/protocol/ask-question.js +77 -3
- package/dist/protocol/blob-store.d.ts +5 -1
- package/dist/protocol/blob-store.js +10 -6
- package/dist/protocol/client-version.js +6 -4
- package/dist/protocol/conversation-cache-header.d.ts +23 -0
- package/dist/protocol/conversation-cache-header.js +149 -0
- package/dist/protocol/conversation-persistence.d.ts +27 -1
- package/dist/protocol/conversation-persistence.js +628 -126
- package/dist/protocol/conversation-state.d.ts +8 -1
- package/dist/protocol/conversation-state.js +28 -7
- package/dist/protocol/create-plan.d.ts +47 -25
- package/dist/protocol/create-plan.js +33 -23
- package/dist/protocol/host-notes.d.ts +31 -0
- package/dist/protocol/host-notes.js +85 -0
- package/dist/protocol/interactions.d.ts +26 -17
- package/dist/protocol/interactions.js +80 -31
- package/dist/protocol/messages.js +164 -21
- package/dist/protocol/request.d.ts +40 -19
- package/dist/protocol/request.js +55 -28
- package/dist/protocol/sent-history-images.d.ts +49 -0
- package/dist/protocol/sent-history-images.js +102 -0
- package/dist/protocol/switch-mode.d.ts +66 -27
- package/dist/protocol/switch-mode.js +147 -73
- package/dist/protocol/token-details.d.ts +2 -0
- package/dist/protocol/token-details.js +11 -1
- package/dist/protocol/tool-call-bridge.d.ts +6 -0
- package/dist/protocol/tool-call-bridge.js +24 -0
- package/dist/protocol/tools.d.ts +90 -9
- package/dist/protocol/tools.js +543 -151
- package/dist/protocol/turn-provenance.d.ts +19 -4
- package/dist/protocol/turn-provenance.js +77 -20
- package/dist/replay-safety.d.ts +22 -4
- package/dist/replay-safety.js +86 -27
- package/dist/retry-budget.d.ts +21 -0
- package/dist/retry-budget.js +94 -0
- package/dist/runtime-files.d.ts +23 -0
- package/dist/runtime-files.js +581 -0
- package/dist/session.d.ts +129 -10
- package/dist/session.js +198 -35
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/shell-timeout.d.ts +54 -10
- package/dist/shell-timeout.js +362 -92
- package/dist/stream-write.d.ts +20 -0
- package/dist/stream-write.js +87 -0
- package/dist/terminal-runner.d.ts +15 -0
- package/dist/terminal-runner.js +528 -0
- package/dist/todo-store.d.ts +2 -0
- package/dist/todo-store.js +19 -0
- package/dist/transport/connect.d.ts +10 -1
- package/dist/transport/connect.js +124 -27
- package/dist/usage.d.ts +37 -17
- package/dist/usage.js +85 -34
- package/package.json +5 -3
- package/dist/plan-execution-kickoff.d.ts +0 -57
- package/dist/plan-execution-kickoff.js +0 -186
- package/dist/protocol/thinking.d.ts +0 -15
- package/dist/protocol/thinking.js +0 -17
package/dist/models.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { MODEL_CACHE_FILE, MODEL_CACHE_SCHEMA_VERSION, MODEL_CACHE_TTL_MS } from "./shared.js";
|
|
2
2
|
import { unaryAvailableModels } from "./transport/connect.js";
|
|
3
|
-
import { buildRequestedModelParams } from "./protocol/thinking.js";
|
|
4
3
|
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
/** Dedicated OpenCode provider-option key for a selected Cursor variant. */
|
|
@@ -59,11 +58,15 @@ export function normalizeModelParameterValues(value) {
|
|
|
59
58
|
if (!Array.isArray(value))
|
|
60
59
|
return null;
|
|
61
60
|
const parameters = [];
|
|
61
|
+
const ids = new Set();
|
|
62
62
|
for (const parameter of value) {
|
|
63
63
|
if (!isPlainRecord(parameter))
|
|
64
64
|
return null;
|
|
65
65
|
if (!isNonEmptyString(parameter.id) || typeof parameter.value !== "string")
|
|
66
66
|
return null;
|
|
67
|
+
if (ids.has(parameter.id))
|
|
68
|
+
return null;
|
|
69
|
+
ids.add(parameter.id);
|
|
67
70
|
parameters.push({ id: parameter.id, value: parameter.value });
|
|
68
71
|
}
|
|
69
72
|
return parameters;
|
|
@@ -78,10 +81,12 @@ function normalizeVariant(value, fallbackName) {
|
|
|
78
81
|
const displayName = optionalString(value, ["displayName", "display_name"]);
|
|
79
82
|
const isDefaultNonMax = optionalBoolean(value, ["isDefaultNonMax", "isDefaultNonMaxConfig", "is_default_non_max_config"]);
|
|
80
83
|
const isDefaultMax = optionalBoolean(value, ["isDefaultMax", "isDefaultMaxConfig", "is_default_max_config"]);
|
|
84
|
+
const isMaxMode = optionalBoolean(value, ["isMaxMode", "is_max_mode"]);
|
|
81
85
|
if (key === null ||
|
|
82
86
|
displayName === null ||
|
|
83
87
|
isDefaultNonMax === null ||
|
|
84
|
-
isDefaultMax === null
|
|
88
|
+
isDefaultMax === null ||
|
|
89
|
+
isMaxMode === null) {
|
|
85
90
|
return null;
|
|
86
91
|
}
|
|
87
92
|
if ((key !== undefined && !isNonEmptyString(key)) ||
|
|
@@ -94,6 +99,7 @@ function normalizeVariant(value, fallbackName) {
|
|
|
94
99
|
parameterValues: parameters,
|
|
95
100
|
isDefaultNonMax: isDefaultNonMax ?? false,
|
|
96
101
|
isDefaultMax: isDefaultMax ?? false,
|
|
102
|
+
...(isMaxMode === undefined ? {} : { isMaxMode }),
|
|
97
103
|
};
|
|
98
104
|
}
|
|
99
105
|
function normalizeModelInfo(value) {
|
|
@@ -111,6 +117,7 @@ function normalizeModelInfo(value) {
|
|
|
111
117
|
const supportsAgent = optionalBoolean(value, ["supportsAgent", "supports_agent"]);
|
|
112
118
|
const supportsImages = optionalBoolean(value, ["supportsImages", "supports_images"]);
|
|
113
119
|
const supportsMaxMode = optionalBoolean(value, ["supportsMaxMode", "supports_max_mode"]);
|
|
120
|
+
const supportsNonMaxMode = optionalBoolean(value, ["supportsNonMaxMode", "supports_non_max_mode"]);
|
|
114
121
|
const maxContext = optionalPositiveNumber(value, ["maxContext", "contextTokenLimit", "context_token_limit"]);
|
|
115
122
|
const maxContextForMaxMode = optionalPositiveNumber(value, ["maxContextForMaxMode", "contextTokenLimitForMaxMode", "context_token_limit_for_max_mode"]);
|
|
116
123
|
if (displayName === null ||
|
|
@@ -119,6 +126,7 @@ function normalizeModelInfo(value) {
|
|
|
119
126
|
supportsAgent === null ||
|
|
120
127
|
supportsImages === null ||
|
|
121
128
|
supportsMaxMode === null ||
|
|
129
|
+
supportsNonMaxMode === null ||
|
|
122
130
|
maxContext === null ||
|
|
123
131
|
maxContextForMaxMode === null) {
|
|
124
132
|
return null;
|
|
@@ -133,6 +141,7 @@ function normalizeModelInfo(value) {
|
|
|
133
141
|
...(maxContext === undefined ? {} : { maxContext }),
|
|
134
142
|
...(maxContextForMaxMode === undefined ? {} : { maxContextForMaxMode }),
|
|
135
143
|
...(supportsMaxMode === undefined ? {} : { supportsMaxMode }),
|
|
144
|
+
...(supportsNonMaxMode === undefined ? {} : { supportsNonMaxMode }),
|
|
136
145
|
variants: variants,
|
|
137
146
|
};
|
|
138
147
|
}
|
|
@@ -176,17 +185,37 @@ export function resolveCursorWireModelId(providerOptions, fallback) {
|
|
|
176
185
|
const value = providerOptions?.[CURSOR_WIRE_MODEL_ID_KEY];
|
|
177
186
|
return typeof value === "string" && value.trim() ? value : fallback;
|
|
178
187
|
}
|
|
179
|
-
/**
|
|
188
|
+
/**
|
|
189
|
+
* True when resolved params select the long-context (1m) tier. Fallback for
|
|
190
|
+
* variants without Cursor's `isMaxMode` marker.
|
|
191
|
+
*/
|
|
180
192
|
export function paramsImplyMaxMode(params) {
|
|
181
193
|
return params.some((parameter) => parameter.id === "context" &&
|
|
182
194
|
parseCursorContextLimit(parameter.value) === 1_000_000);
|
|
183
195
|
}
|
|
196
|
+
function sameParameterTuple(a, b) {
|
|
197
|
+
const byId = new Map(b.map((parameter) => [parameter.id, parameter.value]));
|
|
198
|
+
return (a.length === b.length &&
|
|
199
|
+
new Set(a.map((parameter) => parameter.id)).size === a.length &&
|
|
200
|
+
byId.size === b.length &&
|
|
201
|
+
a.every((parameter) => byId.get(parameter.id) === parameter.value));
|
|
202
|
+
}
|
|
203
|
+
/** Cursor's per-variant max-mode marker, else the 1m-context fallback. */
|
|
204
|
+
function variantIsMaxMode(v) {
|
|
205
|
+
return v.isMaxMode ?? paramsImplyMaxMode(v.parameterValues);
|
|
206
|
+
}
|
|
184
207
|
/**
|
|
185
|
-
* Resolve the request-level max_mode bit.
|
|
186
|
-
*
|
|
208
|
+
* Resolve the request-level max_mode bit. Like Cursor CLI, a cataloged variant
|
|
209
|
+
* requires max mode when Cursor marks it `isMaxMode` (Grok 4.7's 500k tier as
|
|
210
|
+
* well as 1m tiers) or the model does not support non-max mode. An explicit
|
|
211
|
+
* variant pick owns the context tier; otherwise a standalone maxMode hint may
|
|
212
|
+
* select max mode.
|
|
187
213
|
*/
|
|
188
214
|
export function resolveVariantMaxMode(params, opts = {}) {
|
|
189
|
-
|
|
215
|
+
if (opts.model?.supportsNonMaxMode === false)
|
|
216
|
+
return true;
|
|
217
|
+
const marked = opts.model?.variants.find((v) => sameParameterTuple(v.parameterValues, params))?.isMaxMode;
|
|
218
|
+
return (marked ?? paramsImplyMaxMode(params)) || (opts.picked === undefined && opts.maxMode === true);
|
|
190
219
|
}
|
|
191
220
|
// Cursor encodes a model's context window as a variant parameter `id: "context"`
|
|
192
221
|
// whose value is a tier string — the same 200k / 272k / 300k / 1m the IDE's
|
|
@@ -212,8 +241,28 @@ function variantContextTokens(v) {
|
|
|
212
241
|
const raw = v?.parameterValues.find((p) => p.id === "context")?.value;
|
|
213
242
|
return parseCursorContextLimit(raw);
|
|
214
243
|
}
|
|
215
|
-
|
|
216
|
-
|
|
244
|
+
/** Same parameters as `target` except the ignored ids. */
|
|
245
|
+
function isTwinOf(v, target, ignored) {
|
|
246
|
+
return (v.parameterValues.length === target.parameterValues.length &&
|
|
247
|
+
sameParameterTuple(v.parameterValues.filter((p) => !ignored.includes(p.id)), target.parameterValues.filter((p) => !ignored.includes(p.id))));
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Find Cursor's declared default within a catalog subset, retaining its effort:
|
|
251
|
+
* the declared variant, else its speed twin, else its twin in another context
|
|
252
|
+
* tier (a 500k entry when the declared max default is 1m).
|
|
253
|
+
*/
|
|
254
|
+
export function findDefaultVariant(model, candidates, maxMode) {
|
|
255
|
+
const isDeclared = (v) => maxMode ? v.isDefaultMax : v.isDefaultNonMax;
|
|
256
|
+
const declared = model.variants.find(isDeclared);
|
|
257
|
+
return candidates.find(isDeclared) ??
|
|
258
|
+
(declared === undefined
|
|
259
|
+
? undefined
|
|
260
|
+
: candidates.find((v) => isTwinOf(v, declared, ["fast"])) ??
|
|
261
|
+
candidates.find((v) => isTwinOf(v, declared, ["fast", "context"])));
|
|
262
|
+
}
|
|
263
|
+
/** Send an advertised tuple verbatim; never rewrite one of its values. */
|
|
264
|
+
function copyParameters(parameters) {
|
|
265
|
+
return parameters.map((parameter) => ({ ...parameter }));
|
|
217
266
|
}
|
|
218
267
|
function positiveNumber(value) {
|
|
219
268
|
return typeof value === "number" && Number.isFinite(value) && value > 0
|
|
@@ -286,27 +335,38 @@ export function mapAvailableModelsResponse(raw) {
|
|
|
286
335
|
if (!parameterValues) {
|
|
287
336
|
throw new Error(`AvailableModels returned invalid parameter values for ${name}`);
|
|
288
337
|
}
|
|
338
|
+
const isMaxMode = optionalBoolean(v, ["isMaxMode", "is_max_mode"]);
|
|
339
|
+
if (isMaxMode === null) {
|
|
340
|
+
throw new Error(`AvailableModels returned a non-boolean isMaxMode for ${name}`);
|
|
341
|
+
}
|
|
289
342
|
variants.push({
|
|
290
343
|
key: name,
|
|
291
344
|
parameterValues,
|
|
292
345
|
displayName: (v.displayName ?? v.display_name ?? name),
|
|
293
346
|
isDefaultNonMax: apiBoolean(v, ["isDefaultNonMaxConfig", "is_default_non_max_config"]),
|
|
294
347
|
isDefaultMax: apiBoolean(v, ["isDefaultMaxConfig", "is_default_max_config"]),
|
|
348
|
+
...(isMaxMode === undefined ? {} : { isMaxMode }),
|
|
295
349
|
});
|
|
296
350
|
}
|
|
297
351
|
// Derive the context window from the variant `context` param (primary
|
|
298
352
|
// source). Base tier ← default non-max variant; max tier ← default max
|
|
299
353
|
// variant. Fall back to the (often empty) proto fields, then undefined.
|
|
300
|
-
// Prefer a non-
|
|
354
|
+
// Prefer a non-max context when isDefaultNonMax is missing so a leading
|
|
301
355
|
// max-mode variant cannot inflate the base window.
|
|
302
356
|
const variantBaseContext = variantContextTokens(variants.find((v) => v.isDefaultNonMax)) ??
|
|
303
|
-
variantContextTokens(variants.find((v) => variantContextTokens(v) !==
|
|
357
|
+
variantContextTokens(variants.find((v) => !variantIsMaxMode(v) && variantContextTokens(v) !== undefined &&
|
|
358
|
+
variantContextTokens(v) !== 1_000_000)) ??
|
|
304
359
|
variantContextTokens(variants.find((v) => v.parameterValues.some((p) => p.id === "context")));
|
|
305
|
-
const variantMaxContext = variantContextTokens(variants.find((v) => v.isDefaultMax))
|
|
360
|
+
const variantMaxContext = variantContextTokens(variants.find((v) => v.isDefaultMax)) ??
|
|
361
|
+
variantContextTokens(variants.find((v) => variantIsMaxMode(v) && variantContextTokens(v) !== undefined));
|
|
306
362
|
const supportsImages = optionalBoolean(e, ["supportsImages", "supports_images"]);
|
|
307
363
|
if (supportsImages === null) {
|
|
308
364
|
throw new Error(`AvailableModels returned invalid supportsImages for ${name}`);
|
|
309
365
|
}
|
|
366
|
+
const supportsNonMaxMode = optionalBoolean(e, ["supportsNonMaxMode", "supports_non_max_mode"]);
|
|
367
|
+
if (supportsNonMaxMode === null) {
|
|
368
|
+
throw new Error(`AvailableModels returned invalid supportsNonMaxMode for ${name}`);
|
|
369
|
+
}
|
|
310
370
|
models.push({
|
|
311
371
|
id: name,
|
|
312
372
|
displayName: (e.clientDisplayName ?? e.client_display_name ?? name),
|
|
@@ -317,6 +377,7 @@ export function mapAvailableModelsResponse(raw) {
|
|
|
317
377
|
maxContextForMaxMode: variantMaxContext ??
|
|
318
378
|
positiveNumber(e.contextTokenLimitForMaxMode ?? e.context_token_limit_for_max_mode),
|
|
319
379
|
supportsMaxMode: apiBoolean(e, ["supportsMaxMode", "supports_max_mode"]),
|
|
380
|
+
...(supportsNonMaxMode === undefined ? {} : { supportsNonMaxMode }),
|
|
320
381
|
variants,
|
|
321
382
|
});
|
|
322
383
|
}
|
|
@@ -344,51 +405,46 @@ export function resolveVariantParameters(model, opts = {}) {
|
|
|
344
405
|
}
|
|
345
406
|
return opts.reasoningEffort ? [{ id: "effort", value: opts.reasoningEffort }] : [];
|
|
346
407
|
}
|
|
347
|
-
const effortOf = (v) => v.parameterValues.find((p) => p.id === "effort" || p.id === "reasoning")?.value;
|
|
408
|
+
const effortOf = (v) => v.parameterValues.find((p) => p.id === "effort" || p.id === "reasoning" || p.id === "reasoning_effort")?.value;
|
|
348
409
|
const isFast = (v) => v.parameterValues.find((p) => p.id === "fast")?.value === "true";
|
|
349
|
-
const
|
|
350
|
-
const wantMax = opts.maxMode ?? false;
|
|
351
|
-
// Non-fast pool for hint-based resolution only. Exact `picked` matching
|
|
352
|
-
// searches all variants so Fast selections are not silently dropped.
|
|
353
|
-
const pool = model.variants.filter((v) => !isFast(v));
|
|
354
|
-
const scoped = pool.length > 0 ? pool : model.variants;
|
|
410
|
+
const wantMax = (opts.maxMode ?? false) || model.supportsNonMaxMode === false;
|
|
355
411
|
// 1. Variant explicitly picked by opencode (verbatim — preserves context, fast, …).
|
|
356
412
|
if (picked !== undefined) {
|
|
357
|
-
const
|
|
358
|
-
const exact = model.variants.find((v) => v.parameterValues.length === picked.length &&
|
|
359
|
-
pickedById.size === picked.length &&
|
|
360
|
-
v.parameterValues.every((parameter) => pickedById.get(parameter.id) === parameter.value));
|
|
413
|
+
const exact = model.variants.find((v) => sameParameterTuple(v.parameterValues, picked));
|
|
361
414
|
if (exact) {
|
|
362
|
-
return
|
|
415
|
+
return copyParameters(exact.parameterValues);
|
|
363
416
|
}
|
|
364
417
|
throw new CursorVariantSelectionError(`is stale for model ${JSON.stringify(model.id)}: its exact parameter tuple is unavailable`);
|
|
365
418
|
}
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
419
|
+
// Choose the tier before preferring non-Fast variants. Otherwise a Fast-only
|
|
420
|
+
// max tier disappears, or a leading max variant steals an effort-only hint.
|
|
421
|
+
// isDefaultMax is a preference, not a requirement to run in max mode.
|
|
422
|
+
const tier = model.variants.filter((v) => variantIsMaxMode(v) === wantMax);
|
|
423
|
+
const candidates = tier.length > 0 ? tier : model.variants;
|
|
424
|
+
const nonFast = candidates.filter((v) => !isFast(v));
|
|
425
|
+
const scoped = nonFast.length > 0 ? nonFast : candidates;
|
|
426
|
+
// Cursor can declare a Fast variant as a default (Grok, GPT max defaults);
|
|
427
|
+
// the tier default retains the declared default's effort when its speed
|
|
428
|
+
// twin is needed (same parameters except `fast`).
|
|
429
|
+
const byDefault = (wantMax
|
|
430
|
+
? findDefaultVariant(model, scoped, true) ?? scoped.find(variantIsMaxMode)
|
|
431
|
+
: undefined) ?? findDefaultVariant(model, scoped, false) ?? scoped[0];
|
|
432
|
+
// 2. Effort hint (e.g. user passed reasoningEffort through the CLI). Like
|
|
433
|
+
// Cursor CLI, break ties toward the tier default so the hint changes only
|
|
434
|
+
// effort, not thinking or other parameters (Claude lists thinking=false first).
|
|
371
435
|
if (opts.reasoningEffort) {
|
|
372
|
-
const
|
|
436
|
+
const shared = (v) => v.parameterValues.filter((parameter) => byDefault.parameterValues.some((d) => d.id === parameter.id && d.value === parameter.value)).length;
|
|
437
|
+
const match = scoped
|
|
438
|
+
.filter((v) => effortOf(v) === opts.reasoningEffort)
|
|
439
|
+
.reduce((best, v) => best && shared(best) >= shared(v) ? best : v, undefined);
|
|
373
440
|
if (match) {
|
|
374
|
-
return
|
|
375
|
-
reasoningEffort: opts.reasoningEffort,
|
|
376
|
-
maxMode: wantMax,
|
|
377
|
-
});
|
|
441
|
+
return copyParameters(match.parameterValues);
|
|
378
442
|
}
|
|
379
443
|
}
|
|
380
|
-
// 3. Max-mode hint →
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
return buildRequestedModelParams(max.parameterValues, { maxMode: true });
|
|
385
|
-
}
|
|
386
|
-
// 4. Default non-max variant.
|
|
387
|
-
const byDefault = scoped.find((v) => v.isDefaultNonMax) ?? scoped[0];
|
|
388
|
-
return buildRequestedModelParams(byDefault.parameterValues, {
|
|
389
|
-
reasoningEffort: opts.reasoningEffort,
|
|
390
|
-
maxMode: false,
|
|
391
|
-
});
|
|
444
|
+
// 3. Max-mode hint → the default max variant (the max tier).
|
|
445
|
+
// 4. Otherwise the default non-max variant, else the first candidate.
|
|
446
|
+
// An unsupported effort must not synthesize a tuple Cursor never advertised.
|
|
447
|
+
return copyParameters(byDefault.parameterValues);
|
|
392
448
|
}
|
|
393
449
|
// ── Fetch + cache orchestration ──
|
|
394
450
|
export async function fetchModels(token, options = {}) {
|
|
@@ -69,6 +69,8 @@ export function modelConfigEntryToInfo(id, entry) {
|
|
|
69
69
|
time: { released: 0 },
|
|
70
70
|
cost: toOpenCode2Costs(entry.cost),
|
|
71
71
|
};
|
|
72
|
+
if (typeof entry.family === "string" && entry.family.trim())
|
|
73
|
+
info.family = entry.family.trim();
|
|
72
74
|
if (options)
|
|
73
75
|
info.settings = { ...options };
|
|
74
76
|
return info;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PluginContext, ToolDraft } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Register the handle-only image commit tool on the OpenCode 2.0 direct
|
|
4
|
+
* catalog. Catalog `permission: "edit"` hides it when the host agent denies
|
|
5
|
+
* edits. Per-call `ask` is used when the runtime context provides it;
|
|
6
|
+
* otherwise containment still gates the write (opaque `image_id` only).
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerCursorImageSaveTool(draft: ToolDraft, ctx: PluginContext): void;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { executeCursorImageSave } from "../image-save.js";
|
|
2
|
+
import { cursorImageSaveTool } from "../image-save-tool.js";
|
|
3
|
+
import { CURSOR_IMAGE_SAVE_TOOL } from "../protocol/generate-image.js";
|
|
4
|
+
import { getSessionDirectory } from "../session-directory.js";
|
|
5
|
+
import { hostHasTool, OPENCODE2_DIRECT_TOOL_OPTIONS } from "./todo-tools.js";
|
|
6
|
+
const IMAGE_SAVE_INPUT_SCHEMA = {
|
|
7
|
+
type: "object",
|
|
8
|
+
additionalProperties: false,
|
|
9
|
+
properties: {
|
|
10
|
+
image_id: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "Id of the pending Cursor-generated image to save",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
required: ["image_id"],
|
|
16
|
+
};
|
|
17
|
+
const IMAGE_SAVE_OUTPUT_SCHEMA = {
|
|
18
|
+
type: "object",
|
|
19
|
+
additionalProperties: false,
|
|
20
|
+
properties: {
|
|
21
|
+
path: { type: "string" },
|
|
22
|
+
bytes: { type: "number" },
|
|
23
|
+
message: { type: "string" },
|
|
24
|
+
},
|
|
25
|
+
required: ["path", "bytes", "message"],
|
|
26
|
+
};
|
|
27
|
+
function toolAsk(context) {
|
|
28
|
+
if (!context || typeof context !== "object")
|
|
29
|
+
return undefined;
|
|
30
|
+
const ask = context.ask;
|
|
31
|
+
return typeof ask === "function"
|
|
32
|
+
? ask
|
|
33
|
+
: undefined;
|
|
34
|
+
}
|
|
35
|
+
async function workspaceForSession(ctx, sessionID) {
|
|
36
|
+
const marked = getSessionDirectory(sessionID);
|
|
37
|
+
if (marked)
|
|
38
|
+
return marked;
|
|
39
|
+
try {
|
|
40
|
+
return (await ctx.session.get({ sessionID })).location.directory;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return ctx.location?.directory || process.cwd();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Register the handle-only image commit tool on the OpenCode 2.0 direct
|
|
48
|
+
* catalog. Catalog `permission: "edit"` hides it when the host agent denies
|
|
49
|
+
* edits. Per-call `ask` is used when the runtime context provides it;
|
|
50
|
+
* otherwise containment still gates the write (opaque `image_id` only).
|
|
51
|
+
*/
|
|
52
|
+
export function registerCursorImageSaveTool(draft, ctx) {
|
|
53
|
+
if (hostHasTool(draft, CURSOR_IMAGE_SAVE_TOOL))
|
|
54
|
+
return;
|
|
55
|
+
draft.add({
|
|
56
|
+
name: CURSOR_IMAGE_SAVE_TOOL,
|
|
57
|
+
description: cursorImageSaveTool.description,
|
|
58
|
+
input: IMAGE_SAVE_INPUT_SCHEMA,
|
|
59
|
+
output: IMAGE_SAVE_OUTPUT_SCHEMA,
|
|
60
|
+
options: { ...OPENCODE2_DIRECT_TOOL_OPTIONS, permission: "edit" },
|
|
61
|
+
execute: async (input, context) => {
|
|
62
|
+
const sessionID = context.sessionID;
|
|
63
|
+
if (typeof sessionID !== "string" || !sessionID) {
|
|
64
|
+
throw new Error("OpenCode 2.0 image-save context did not provide a sessionID");
|
|
65
|
+
}
|
|
66
|
+
const directory = await workspaceForSession(ctx, sessionID);
|
|
67
|
+
const result = await executeCursorImageSave({ image_id: input?.image_id }, {
|
|
68
|
+
worktree: directory,
|
|
69
|
+
directory,
|
|
70
|
+
// The public 2.0 ToolContext has no permission prompt; catalog
|
|
71
|
+
// `permission: "edit"` and containment gate the write instead.
|
|
72
|
+
ask: toolAsk(context) ?? null,
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
output: {
|
|
76
|
+
path: result.title,
|
|
77
|
+
bytes: result.bytes,
|
|
78
|
+
message: result.output,
|
|
79
|
+
},
|
|
80
|
+
content: result.output,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preserve native result status before OpenCode's AI SDK lowers errors to text.
|
|
3
|
+
* Upstream: https://github.com/anomalyco/opencode/blob/v2.0.27/packages/core/src/aisdk.ts#L652-L685
|
|
4
|
+
* Native part.providerMetadata becomes SDK part.providerOptions (same file, L717-L720).
|
|
5
|
+
*/
|
|
6
|
+
export type ToolRefusal = "permission_denied" | "rejected";
|
|
7
|
+
export type NativeToolFailure = {
|
|
8
|
+
type: string;
|
|
9
|
+
text: string;
|
|
10
|
+
refusal?: ToolRefusal;
|
|
11
|
+
};
|
|
12
|
+
export declare const TOOL_RESULT_STATUS_KEY = "opencodeToolResult";
|
|
13
|
+
export declare function nativeToolFailure(value: unknown, refusal?: ToolRefusal): NativeToolFailure;
|
|
14
|
+
/** The native tool hook preserves the underlying typed permission cause. */
|
|
15
|
+
export declare function nativeToolRefusal(error: unknown): ToolRefusal | undefined;
|
|
16
|
+
/** Metadata travels with each part, including history and independently loaded SDK copies. */
|
|
17
|
+
export declare function preserveNativeToolResults(messages: readonly unknown[] | undefined, refusals?: ReadonlyMap<string, ToolRefusal>): any[];
|
|
18
|
+
export declare function toolResultFailure(options: unknown, callID: string): NativeToolFailure | undefined;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const TOOL_RESULT_STATUS_KEY = "opencodeToolResult";
|
|
2
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
3
|
+
? value : undefined;
|
|
4
|
+
export function nativeToolFailure(value, refusal) {
|
|
5
|
+
const envelope = record(value);
|
|
6
|
+
const error = record(envelope?.error);
|
|
7
|
+
const type = typeof error?.type === "string" && error.type ? error.type : "tool.execution";
|
|
8
|
+
const message = typeof error?.message === "string" && error.message
|
|
9
|
+
? error.message : typeof value === "string" && value ? value : `OpenCode tool failed (${type})`;
|
|
10
|
+
const content = Array.isArray(envelope?.content) ? envelope.content.flatMap(item => {
|
|
11
|
+
const part = record(item);
|
|
12
|
+
return part?.type === "text" && typeof part.text === "string" && part.text ? [part.text] : [];
|
|
13
|
+
}).join("\n") : "";
|
|
14
|
+
return { type, text: content ? `${message}\n\n${content}` : message,
|
|
15
|
+
...(refusal ? { refusal } : type === "permission.rejected" ? { refusal: "rejected" } : {}) };
|
|
16
|
+
}
|
|
17
|
+
/** The native tool hook preserves the underlying typed permission cause. */
|
|
18
|
+
export function nativeToolRefusal(error) {
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
for (let depth = 0; depth < 16 && !seen.has(error); depth++) {
|
|
21
|
+
seen.add(error);
|
|
22
|
+
const cause = record(error);
|
|
23
|
+
if (!cause)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (cause._tag === "Permission.BlockedError")
|
|
26
|
+
return "permission_denied";
|
|
27
|
+
if (cause._tag === "Permission.CorrectedError" || cause._tag === "Permission.DeclinedError")
|
|
28
|
+
return "rejected";
|
|
29
|
+
error = cause.error;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
/** Metadata travels with each part, including history and independently loaded SDK copies. */
|
|
34
|
+
export function preserveNativeToolResults(messages, refusals = new Map()) {
|
|
35
|
+
if (!Array.isArray(messages))
|
|
36
|
+
return [];
|
|
37
|
+
return messages.map(message => {
|
|
38
|
+
const msg = record(message);
|
|
39
|
+
if (msg?.role !== "tool" || !Array.isArray(msg.content))
|
|
40
|
+
return message;
|
|
41
|
+
return { ...msg, content: msg.content.map(item => {
|
|
42
|
+
const part = record(item);
|
|
43
|
+
const result = record(part?.result);
|
|
44
|
+
if (part?.type !== "tool-result" || typeof part.id !== "string" || !result
|
|
45
|
+
|| typeof result.type !== "string" || !["error", "text", "json", "content"].includes(result.type))
|
|
46
|
+
return item;
|
|
47
|
+
const metadata = record(part.providerMetadata);
|
|
48
|
+
const cursor = record(metadata?.cursor);
|
|
49
|
+
const status = result.type === "error"
|
|
50
|
+
? { callID: part.id, status: "error", failure: nativeToolFailure(result.value, refusals.get(part.id)) }
|
|
51
|
+
: { callID: part.id, status: "completed" };
|
|
52
|
+
return { ...part, providerMetadata: { ...metadata, cursor: { ...cursor, [TOOL_RESULT_STATUS_KEY]: status } } };
|
|
53
|
+
}) };
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export function toolResultFailure(options, callID) {
|
|
57
|
+
const status = record(record(record(options)?.cursor)?.[TOOL_RESULT_STATUS_KEY]);
|
|
58
|
+
if (status?.callID !== callID || status.status !== "error")
|
|
59
|
+
return undefined;
|
|
60
|
+
const failure = record(status.failure);
|
|
61
|
+
if (!failure || typeof failure.type !== "string" || typeof failure.text !== "string")
|
|
62
|
+
return undefined;
|
|
63
|
+
const refusal = failure.refusal === "permission_denied" || failure.refusal === "rejected" ? failure.refusal : undefined;
|
|
64
|
+
return { type: failure.type, text: failure.text || `OpenCode tool failed (${failure.type || "tool.execution"})`,
|
|
65
|
+
...(refusal ? { refusal } : {}) };
|
|
66
|
+
}
|
|
@@ -43,6 +43,7 @@ export type ModelInfo2 = {
|
|
|
43
43
|
modelID: string;
|
|
44
44
|
providerID: string;
|
|
45
45
|
name: string;
|
|
46
|
+
family?: string;
|
|
46
47
|
capabilities: {
|
|
47
48
|
tools: boolean;
|
|
48
49
|
input: readonly string[];
|
|
@@ -415,6 +416,19 @@ export type McpEditor = {
|
|
|
415
416
|
export type McpDomain = {
|
|
416
417
|
readonly transform: Transform<McpEditor>;
|
|
417
418
|
};
|
|
419
|
+
/** Only the fields this plugin reads from a registered skill. */
|
|
420
|
+
export type SkillInfo = {
|
|
421
|
+
readonly id: string;
|
|
422
|
+
readonly path: string;
|
|
423
|
+
};
|
|
424
|
+
export type SkillDomain = {
|
|
425
|
+
readonly list: () => Promise<{
|
|
426
|
+
readonly location: {
|
|
427
|
+
readonly directory: string;
|
|
428
|
+
};
|
|
429
|
+
readonly data: readonly SkillInfo[];
|
|
430
|
+
}>;
|
|
431
|
+
};
|
|
418
432
|
export type PluginLocation = {
|
|
419
433
|
readonly directory: string;
|
|
420
434
|
};
|
|
@@ -429,6 +443,7 @@ export type PluginContext = {
|
|
|
429
443
|
readonly shell?: ShellDomain;
|
|
430
444
|
readonly websearch?: WebSearchDomain;
|
|
431
445
|
readonly mcp?: McpDomain;
|
|
446
|
+
readonly skill?: SkillDomain;
|
|
432
447
|
};
|
|
433
448
|
export type Cleanup = () => Promise<void> | void;
|
|
434
449
|
export type Plugin2 = {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel tool-call step boundary for OpenCode's AI SDK step model.
|
|
3
|
+
*
|
|
4
|
+
* Cursor fires execs as they are generated; CLI demuxes them concurrently and
|
|
5
|
+
* has no step boundary. OpenCode needs one `finish: tool-calls` after every
|
|
6
|
+
* call of the generation. Cursor's `tool_requests_listed` (InteractionUpdate
|
|
7
|
+
* field 27) carries that count. See tasks/plans/parallel-tool-calls-cli-parity.md.
|
|
8
|
+
*/
|
|
9
|
+
/** Idle without a progress frame while holding → close the step (ms). */
|
|
10
|
+
export declare const PARALLEL_STEP_IDLE_MS = 1500;
|
|
11
|
+
export type ParallelStepState = {
|
|
12
|
+
/** From field 27; undefined until Cursor lists the count for this step. */
|
|
13
|
+
listedCount?: number;
|
|
14
|
+
/** Model tool_call_ids seen this step. */
|
|
15
|
+
callIds: Set<string>;
|
|
16
|
+
/** Ids with a final client disposition this step. */
|
|
17
|
+
resolved: Set<string>;
|
|
18
|
+
/** Host tool-call parts emitted this step. */
|
|
19
|
+
emitted: number;
|
|
20
|
+
/** Wall time of the last progress frame (for the liveness guard). */
|
|
21
|
+
lastProgressAt: number;
|
|
22
|
+
};
|
|
23
|
+
export declare function noteToolRequestsListedSeen(): void;
|
|
24
|
+
export declare function hasSeenToolRequestsListed(): boolean;
|
|
25
|
+
/** Test-only: reset the process capability gate. */
|
|
26
|
+
export declare function resetToolRequestsListedSeenForTests(): void;
|
|
27
|
+
export declare function createParallelStep(now?: number): ParallelStepState;
|
|
28
|
+
export declare function noteParallelStepProgress(step: ParallelStepState, now?: number): void;
|
|
29
|
+
export declare function noteListedCallCount(step: ParallelStepState, callCount: number, now?: number): void;
|
|
30
|
+
/**
|
|
31
|
+
* Record a final disposition for `callId`. Returns true when newly resolved.
|
|
32
|
+
* Empty ids are ignored. Prior-step ids in `prior` are ignored.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveParallelStepCall(step: ParallelStepState, callId: string | undefined, prior?: ReadonlySet<string>, now?: number): boolean;
|
|
35
|
+
export declare function recordParallelStepEmission(step: ParallelStepState, now?: number): void;
|
|
36
|
+
/** True when the listed count is known and every listed call is resolved. */
|
|
37
|
+
export declare function parallelStepCountMet(step: ParallelStepState): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Whether to keep pumping for more calls of this step instead of finishing.
|
|
40
|
+
*
|
|
41
|
+
* - Gate off and no count yet → do not hold (today's one-call finish).
|
|
42
|
+
* - Human-gated interaction with unknown count → do not hold (prompt must show).
|
|
43
|
+
* - Otherwise hold until the count is met (or the liveness guard fires).
|
|
44
|
+
*/
|
|
45
|
+
export declare function shouldHoldParallelStep(step: ParallelStepState, options?: {
|
|
46
|
+
fieldSeen?: boolean;
|
|
47
|
+
humanGatedWithoutCount?: boolean;
|
|
48
|
+
}): boolean;
|
|
49
|
+
/** True when the hold has made no progress for `idleMs`. */
|
|
50
|
+
export declare function shouldGuardCloseParallelStep(step: ParallelStepState, now?: number, idleMs?: number): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Frames that reset the liveness guard while holding. Heartbeat, KV, and
|
|
53
|
+
* checkpoint are not progress — they say nothing about further tool calls.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isParallelStepProgressFrame(kinds: {
|
|
56
|
+
toolRequestsListed?: boolean;
|
|
57
|
+
partialToolCall?: boolean;
|
|
58
|
+
toolCallDelta?: boolean;
|
|
59
|
+
toolCallStarted?: boolean;
|
|
60
|
+
toolCallCompleted?: boolean;
|
|
61
|
+
exec?: boolean;
|
|
62
|
+
interactionQuery?: boolean;
|
|
63
|
+
textDelta?: boolean;
|
|
64
|
+
thinkingDelta?: boolean;
|
|
65
|
+
}): boolean;
|