cursor-opencode-provider 0.3.2 → 0.4.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/README.md +38 -7
- package/dist/context/paths.d.ts +21 -4
- package/dist/context/paths.js +47 -5
- package/dist/image-input.d.ts +31 -0
- package/dist/image-input.js +281 -0
- package/dist/language-model.d.ts +1 -1
- package/dist/language-model.js +118 -12
- package/dist/model-config.js +10 -2
- package/dist/model-metadata.d.ts +10 -0
- package/dist/model-metadata.js +12 -0
- package/dist/models.d.ts +1 -0
- package/dist/models.js +8 -0
- package/dist/opencode2/catalog.js +8 -4
- package/dist/opencode2/types.d.ts +9 -3
- package/dist/opencode2/types.js +1 -1
- package/dist/plugin-opencode2.js +13 -5
- package/dist/pricing-data.d.ts +221 -1
- package/dist/pricing-data.js +221 -1
- package/dist/protocol/messages.js +21 -0
- package/dist/protocol/request.d.ts +2 -0
- package/dist/protocol/request.js +14 -5
- package/dist/protocol/tools.d.ts +8 -0
- package/dist/protocol/tools.js +167 -1
- package/dist/session.d.ts +17 -2
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/usage.d.ts +12 -3
- package/dist/usage.js +20 -16
- package/package.json +16 -4
package/dist/language-model.js
CHANGED
|
@@ -6,7 +6,7 @@ import { trace, traceRequestContextPaths } from "./debug.js";
|
|
|
6
6
|
import { buildRunRequest, buildHeartbeat } from "./protocol/request.js";
|
|
7
7
|
import { decodeFramePayload } from "./protocol/framing.js";
|
|
8
8
|
import { debugWalkTurnEnded, decodeMessage } from "./protocol/messages.js";
|
|
9
|
-
import { parseExecServerMessage, buildToolCallPart, buildExecClientMessages, buildReadRejectionMessages, classifyMissingReadTarget, resolveReadTargetPath, parseExecIdFromToolCallId, detectExecVariantField, buildRequestContextResult, buildMcpStateResult, buildCustomWebToolAliases, extractHostSubagentCatalog, resolveCustomWebToolAlias, remapNativeSubagentForCatalog, remapEditToolsForCatalog, CUSTOM_WEBFETCH_TOOL, CUSTOM_WEBSEARCH_TOOL, } from "./protocol/tools.js";
|
|
9
|
+
import { parseExecServerMessage, buildToolCallPart, buildExecClientMessages, buildReadRejectionMessages, classifyMissingReadTarget, resolveReadTargetPath, parseExecIdFromToolCallId, detectExecVariantField, buildRequestContextResult, buildMcpStateResult, buildCustomWebToolAliases, extractHostSubagentCatalog, resolveCustomWebToolAlias, remapNativeSubagentForCatalog, remapCorrelatedEditWriteForCatalog, remapEditToolsForCatalog, CUSTOM_WEBFETCH_TOOL, CUSTOM_WEBSEARCH_TOOL, } from "./protocol/tools.js";
|
|
10
10
|
import { describeCursorExecVariant } from "./protocol/exec-variants.js";
|
|
11
11
|
import { advertisedToolNamesFromDescriptors, extractExecDisplayCallId, extractProtobufSubmessage, listProtobufFieldNumbers, parseDisplayToolCall, resolveBridgedOpenCodeToolCall, } from "./protocol/tool-call-bridge.js";
|
|
12
12
|
import { handleKvServerMessage } from "./protocol/kv.js";
|
|
@@ -24,6 +24,8 @@ import { resolveAgentUrl } from "./agent-url.js";
|
|
|
24
24
|
import { CURSOR_API_HOST, CURSOR_COMPACTION_OPTION } from "./shared.js";
|
|
25
25
|
import { isCompactionSession } from "./compaction-marker.js";
|
|
26
26
|
import { getSessionDirectory } from "./session-directory.js";
|
|
27
|
+
import { assertCursorUserImageSupport, extractCursorPromptImages } from "./image-input.js";
|
|
28
|
+
import { resolveCursorModelSupportsImages } from "./model-metadata.js";
|
|
27
29
|
import { consumeCursorShellResult, registerCursorShellCall, } from "./shell-timeout.js";
|
|
28
30
|
import { analyzeReplayFrame, AttemptReplaySafety } from "./replay-safety.js";
|
|
29
31
|
import { readAllFieldsStrict } from "./protocol/struct.js";
|
|
@@ -36,12 +38,14 @@ let _availableModelsMtimeMs = -1;
|
|
|
36
38
|
// session so the new Cursor conversation is still born with tool definitions;
|
|
37
39
|
// execution remains disabled for the summary turn itself.
|
|
38
40
|
const toolCatalogBySession = new Map();
|
|
41
|
+
const sentHistoryImageHashesBySession = new Map();
|
|
39
42
|
// A compaction Run uses its own summary-agent system prompt. Its opaque Cursor
|
|
40
43
|
// checkpoint must never become the base for the resumed normal agent: doing so
|
|
41
44
|
// suppresses OpenCode's compacted prompt/system seed and makes Cursor narrate
|
|
42
45
|
// tool use instead of emitting exec requests. Rebase once on the next turn.
|
|
43
46
|
const postCompactionRebaseBySession = new Set();
|
|
44
47
|
export const MAX_TURN_STATE_SESSIONS = 256;
|
|
48
|
+
const MAX_SENT_HISTORY_IMAGES_PER_SESSION = 256;
|
|
45
49
|
const DEFAULT_RETRY_POLICY = {
|
|
46
50
|
maxAttempts: 3,
|
|
47
51
|
baseDelayMs: 500,
|
|
@@ -251,6 +255,39 @@ function rememberPostCompactionRebase(sessionKey) {
|
|
|
251
255
|
postCompactionRebaseBySession.delete(oldest);
|
|
252
256
|
}
|
|
253
257
|
}
|
|
258
|
+
function sentHistoryImageHashes(sessionKey) {
|
|
259
|
+
if (!sessionKey)
|
|
260
|
+
return undefined;
|
|
261
|
+
const hashes = sentHistoryImageHashesBySession.get(sessionKey);
|
|
262
|
+
if (!hashes)
|
|
263
|
+
return undefined;
|
|
264
|
+
sentHistoryImageHashesBySession.delete(sessionKey);
|
|
265
|
+
sentHistoryImageHashesBySession.set(sessionKey, hashes);
|
|
266
|
+
return hashes;
|
|
267
|
+
}
|
|
268
|
+
function rememberSentHistoryImageHashes(sessionKey, hashes) {
|
|
269
|
+
if (!sessionKey || hashes.length === 0)
|
|
270
|
+
return;
|
|
271
|
+
const remembered = sentHistoryImageHashesBySession.get(sessionKey) ?? new Set();
|
|
272
|
+
for (const hash of hashes) {
|
|
273
|
+
remembered.delete(hash);
|
|
274
|
+
remembered.add(hash);
|
|
275
|
+
while (remembered.size > MAX_SENT_HISTORY_IMAGES_PER_SESSION) {
|
|
276
|
+
const oldest = remembered.values().next().value;
|
|
277
|
+
if (!oldest)
|
|
278
|
+
break;
|
|
279
|
+
remembered.delete(oldest);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
sentHistoryImageHashesBySession.delete(sessionKey);
|
|
283
|
+
sentHistoryImageHashesBySession.set(sessionKey, remembered);
|
|
284
|
+
while (sentHistoryImageHashesBySession.size > MAX_TURN_STATE_SESSIONS) {
|
|
285
|
+
const oldest = sentHistoryImageHashesBySession.keys().next().value;
|
|
286
|
+
if (!oldest)
|
|
287
|
+
break;
|
|
288
|
+
sentHistoryImageHashesBySession.delete(oldest);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
254
291
|
export function createCursorLanguageModel(modelId, providerId, options) {
|
|
255
292
|
// Host Path.cache / explicit override wins over XDG heuristics for this process.
|
|
256
293
|
if (options.cacheDir)
|
|
@@ -427,13 +464,19 @@ export async function pumpWithRecovery(input) {
|
|
|
427
464
|
const delayMs = retryDelayMs(failure, attempt + 1, retryPolicy);
|
|
428
465
|
trace(`Run retry backoff: attempt=${attempt + 1}/${maxRecoveries} delayMs=${delayMs}`);
|
|
429
466
|
await sleepForRetry(delayMs, input.abortSignal);
|
|
430
|
-
|
|
467
|
+
const recovery = checkpoint
|
|
431
468
|
? {
|
|
432
469
|
kind: "resume",
|
|
433
470
|
conversationId: pumpedSession.conversationId,
|
|
434
471
|
checkpoint: Uint8Array.from(checkpoint),
|
|
435
472
|
}
|
|
436
|
-
: { kind: "rebase" }
|
|
473
|
+
: { kind: "rebase" };
|
|
474
|
+
session = await input.recover(recovery);
|
|
475
|
+
if (recovery.kind === "resume") {
|
|
476
|
+
session.hadToolCallBoundary = pumpedSession.hadToolCallBoundary;
|
|
477
|
+
session.usageEstimate = { ...pumpedSession.usageEstimate };
|
|
478
|
+
session.editToolCalls = new Map(pumpedSession.editToolCalls);
|
|
479
|
+
}
|
|
437
480
|
input.onSession?.(session);
|
|
438
481
|
}
|
|
439
482
|
finally {
|
|
@@ -484,9 +527,10 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
484
527
|
`sessionKey=${sessionKey ?? "(none)"} ` +
|
|
485
528
|
`previousId=${bound.previousId ?? "-"} → conversationId=${conversationId}`);
|
|
486
529
|
}
|
|
530
|
+
const lastUser = [...prompt].reverse().find((message) => message.role === "user");
|
|
487
531
|
const userText = recovery?.kind === "rebase"
|
|
488
532
|
? "Continue the interrupted turn from the conversation history above. Do not repeat completed work."
|
|
489
|
-
: (extractUserText(
|
|
533
|
+
: (extractUserText(lastUser) || ".");
|
|
490
534
|
// v1 sets `options.workspaceRoot` correctly per invocation (`input.directory`,
|
|
491
535
|
// one plugin instance per project). OpenCode 2.0 runs one daemon across many
|
|
492
536
|
// projects, so its static `options.workspaceRoot` is only a last-resort
|
|
@@ -521,6 +565,30 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
521
565
|
: undefined;
|
|
522
566
|
const hintMaxMode = !!(providerOptions?.maxMode ?? false);
|
|
523
567
|
const modelInfo = _availableModels?.find((m) => m.id === cursorModelId);
|
|
568
|
+
const supportsImages = resolveCursorModelSupportsImages(cursorModelId, modelInfo?.supportsImages);
|
|
569
|
+
if (!resuming) {
|
|
570
|
+
assertCursorUserImageSupport(lastUser, supportsImages, cursorModelId);
|
|
571
|
+
}
|
|
572
|
+
const imageExtraction = resuming
|
|
573
|
+
? { images: [], hashes: [], candidateCount: 0, duplicateCount: 0, userImageCount: 0 }
|
|
574
|
+
: await extractCursorPromptImages(prompt, lastUser, {
|
|
575
|
+
supportsImages,
|
|
576
|
+
// Content hashes are retained for the OpenCode session so growing
|
|
577
|
+
// history does not re-upload old screenshots. A recovery rebase opens
|
|
578
|
+
// a new Cursor conversation, so it must resend the same payload.
|
|
579
|
+
seenHistoryHashes: recovery?.kind === "rebase"
|
|
580
|
+
? undefined
|
|
581
|
+
: sentHistoryImageHashes(sessionKey),
|
|
582
|
+
signal: callOptions.abortSignal,
|
|
583
|
+
});
|
|
584
|
+
if (!supportsImages && imageExtraction.candidateCount > 0) {
|
|
585
|
+
trace(`image input: dropped ${imageExtraction.candidateCount} tool/assistant history image(s); ` +
|
|
586
|
+
`model=${cursorModelId} does not support images`);
|
|
587
|
+
}
|
|
588
|
+
if (imageExtraction.duplicateCount > 0) {
|
|
589
|
+
trace(`image input: skipped ${imageExtraction.duplicateCount} previously sent history image(s)`);
|
|
590
|
+
}
|
|
591
|
+
const images = imageExtraction.images;
|
|
524
592
|
const parameterValues = resolveVariantParameters(modelInfo, {
|
|
525
593
|
reasoningEffort,
|
|
526
594
|
maxMode: hintMaxMode,
|
|
@@ -566,6 +634,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
566
634
|
: (bound.reset ? undefined : getCheckpoint(conversationId));
|
|
567
635
|
const reqBytes = buildRunRequest({
|
|
568
636
|
text: userText,
|
|
637
|
+
images,
|
|
569
638
|
modelId: cursorModelId,
|
|
570
639
|
conversationId,
|
|
571
640
|
systemPrompt: conversationState ? undefined : systemPrompt,
|
|
@@ -601,6 +670,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
601
670
|
`tools=${tools.length} incomingTools=${incomingTools.length} compaction=${isCompaction} ` +
|
|
602
671
|
`skills=${skillsCount} hooks=${hooksCtx ? hooksCtx.split("\n").length : 0} ` +
|
|
603
672
|
`availableModels=${_availableModels?.length ?? 0} userTextLen=${userText.length} ` +
|
|
673
|
+
`images=${images.length} imageBytes=${images.reduce((total, image) => total + image.data.length, 0)} ` +
|
|
604
674
|
`historyMsgs=${history.length} historyChars=${historyChars} ` +
|
|
605
675
|
`checkpointLen=${conversationState?.length ?? 0} reset=${bound.reset} ` +
|
|
606
676
|
`resume=${resuming} ` +
|
|
@@ -614,6 +684,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
614
684
|
trace(`hash checkpoint sha256=${sha(conversationState)}`);
|
|
615
685
|
try {
|
|
616
686
|
await writeWithBackpressure(stream, reqBytes, "initial Run request");
|
|
687
|
+
rememberSentHistoryImageHashes(sessionKey, imageExtraction.hashes);
|
|
617
688
|
}
|
|
618
689
|
catch (error) {
|
|
619
690
|
stream.destroy();
|
|
@@ -628,6 +699,7 @@ async function startSession(modelId, token, callOptions, options, startOptions)
|
|
|
628
699
|
frames: stream.frames()[Symbol.asyncIterator](),
|
|
629
700
|
pending: new Map(),
|
|
630
701
|
displayToolCalls: new Map(),
|
|
702
|
+
editToolCalls: new Map(),
|
|
631
703
|
nextBridgedExecId: 900_000,
|
|
632
704
|
blobs: new Map(),
|
|
633
705
|
toolDescriptors,
|
|
@@ -1080,8 +1152,12 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1080
1152
|
};
|
|
1081
1153
|
const emitFinish = (te, reason) => {
|
|
1082
1154
|
closeOpenSpans();
|
|
1083
|
-
|
|
1084
|
-
|
|
1155
|
+
// TurnEnded is request-local only when this Run did not previously stop at
|
|
1156
|
+
// a tool boundary. On a held multi-step Run Cursor aggregates every model
|
|
1157
|
+
// invocation into these counters; treating that billed total as the final
|
|
1158
|
+
// request's prompt size makes OpenCode compact after ordinary tasks.
|
|
1159
|
+
const cumulativeTurnEnded = !!te && session.hadToolCallBoundary === true;
|
|
1160
|
+
if (te && !cumulativeTurnEnded) {
|
|
1085
1161
|
session.usageEstimate = {
|
|
1086
1162
|
inputTokens: turnEndedCounter(te, "input_tokens"),
|
|
1087
1163
|
outputTokens: turnEndedCounter(te, "output_tokens"),
|
|
@@ -1091,9 +1167,11 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1091
1167
|
};
|
|
1092
1168
|
}
|
|
1093
1169
|
const est = session.usageEstimate;
|
|
1094
|
-
const usage = te
|
|
1170
|
+
const usage = te && !cumulativeTurnEnded
|
|
1095
1171
|
? buildLanguageModelV3UsageFromTurnEnded(te)
|
|
1096
1172
|
: buildLanguageModelV3UsageFromEstimate(est, promptTokens, requestUsage.outputChars, estimateTokens);
|
|
1173
|
+
if (!te)
|
|
1174
|
+
session.hadToolCallBoundary = true;
|
|
1097
1175
|
const providerMetadata = te ? cursorTurnEndedProviderMetadata(te) : undefined;
|
|
1098
1176
|
const reasonLabel = typeof reason === "object" && reason && "unified" in reason
|
|
1099
1177
|
? String(reason.unified ?? "unknown")
|
|
@@ -1104,9 +1182,11 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1104
1182
|
`v3In=${inTotal} v3Out=${outTotal} ` +
|
|
1105
1183
|
`v3CacheRead=${usage.inputTokens?.cacheRead ?? 0} v3CacheWrite=${usage.inputTokens?.cacheWrite ?? 0} ` +
|
|
1106
1184
|
`v3Reasoning=${usage.outputTokens?.reasoning ?? 0} ` +
|
|
1107
|
-
`rawIn=${est.inputTokens}
|
|
1108
|
-
`
|
|
1109
|
-
`
|
|
1185
|
+
`rawIn=${te ? turnEndedCounter(te, "input_tokens") : est.inputTokens} ` +
|
|
1186
|
+
`rawOut=${te ? turnEndedCounter(te, "output_tokens") : est.outputTokens} ` +
|
|
1187
|
+
`rawCacheRead=${te ? turnEndedCounter(te, "cache_read") : est.cacheRead} ` +
|
|
1188
|
+
`rawCacheWrite=${te ? turnEndedCounter(te, "cache_write") : est.cacheWrite} ` +
|
|
1189
|
+
`source=${te ? (cumulativeTurnEnded ? "estimate-cumulative-turn-ended" : "turn_ended") : "estimate"}`);
|
|
1110
1190
|
safeEnqueue({
|
|
1111
1191
|
type: "finish",
|
|
1112
1192
|
usage,
|
|
@@ -1262,6 +1342,15 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1262
1342
|
if (callId && toolCall) {
|
|
1263
1343
|
session.displayToolCalls.set(callId, toolCall);
|
|
1264
1344
|
const variant = Object.keys(toolCall).find((k) => k.endsWith("_tool_call")) ?? "?";
|
|
1345
|
+
const display = parseDisplayToolCall(callId, toolCall);
|
|
1346
|
+
const editPath = display?.variant === "edit_tool_call"
|
|
1347
|
+
&& typeof display.args.path === "string"
|
|
1348
|
+
? display.args.path
|
|
1349
|
+
: undefined;
|
|
1350
|
+
if (editPath) {
|
|
1351
|
+
const editToolCalls = session.editToolCalls ?? (session.editToolCalls = new Map());
|
|
1352
|
+
editToolCalls.set(callId, { path: editPath });
|
|
1353
|
+
}
|
|
1265
1354
|
const callIdLog = callId.replace(/\r?\n/g, "\\n");
|
|
1266
1355
|
let wireFields = "";
|
|
1267
1356
|
if (variant === "?") {
|
|
@@ -1276,6 +1365,8 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1276
1365
|
else if (iu?.tool_call_completed) {
|
|
1277
1366
|
const completed = iu.tool_call_completed;
|
|
1278
1367
|
const callId = typeof completed.call_id === "string" ? completed.call_id : "";
|
|
1368
|
+
if (callId)
|
|
1369
|
+
session.editToolCalls?.delete(callId);
|
|
1279
1370
|
// If exec already claimed this call_id, display map entry is gone — skip.
|
|
1280
1371
|
if (!callId || !session.displayToolCalls.has(callId)) {
|
|
1281
1372
|
if (callId) {
|
|
@@ -1371,6 +1462,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1371
1462
|
}
|
|
1372
1463
|
else {
|
|
1373
1464
|
replaySafety.markBarrier("non-control-exec");
|
|
1465
|
+
const displayCallId = extractExecDisplayCallId(esm);
|
|
1374
1466
|
const parsed = parseExecServerMessage(esm);
|
|
1375
1467
|
if (parsed) {
|
|
1376
1468
|
const executableToolName = resolveCustomWebToolAlias(parsed.toolName, session.toolAliases);
|
|
@@ -1379,12 +1471,24 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1379
1471
|
parsed.toolName = executableToolName;
|
|
1380
1472
|
}
|
|
1381
1473
|
remapNativeSubagentForCatalog(parsed, advertisedToolNameSet, session.subagentCatalog);
|
|
1474
|
+
const editCall = displayCallId
|
|
1475
|
+
? session.editToolCalls?.get(displayCallId)
|
|
1476
|
+
: undefined;
|
|
1477
|
+
if (editCall) {
|
|
1478
|
+
const remapped = remapCorrelatedEditWriteForCatalog(parsed, advertisedToolNameSet, editCall.path, workspaceRootFromRequestContext(session.requestContext));
|
|
1479
|
+
if (parsed.resultField === "write_result" && displayCallId) {
|
|
1480
|
+
session.editToolCalls?.delete(displayCallId);
|
|
1481
|
+
}
|
|
1482
|
+
if (remapped) {
|
|
1483
|
+
trace(`exec: preserved edit intent callId=${displayCallId} ` +
|
|
1484
|
+
`path=${JSON.stringify(editCall.path)}`);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1382
1487
|
// Hosts that advertise `apply_patch` in place of `edit`/`write` (see
|
|
1383
1488
|
// protocol/apply-patch.ts) still receive Cursor's native write/edit
|
|
1384
1489
|
// exec requests. Translate before the unavailable-tool check below.
|
|
1385
1490
|
remapEditToolsForCatalog(parsed, advertisedToolNameSet, workspaceRootFromRequestContext(session.requestContext));
|
|
1386
1491
|
}
|
|
1387
|
-
const displayCallId = extractExecDisplayCallId(esm);
|
|
1388
1492
|
trace(`exec: id=${parsed?.id} variant=${parsed ? Object.keys(parsed).join(",") : "none"} toolName=${parsed?.toolName} resultField=${parsed?.resultField}`);
|
|
1389
1493
|
if (parsed) {
|
|
1390
1494
|
if (parsed.localError) {
|
|
@@ -1604,6 +1708,8 @@ function toolResultOutputToText(output) {
|
|
|
1604
1708
|
return { text: JSON.stringify(o.value ?? null), isError };
|
|
1605
1709
|
}
|
|
1606
1710
|
if (o.type === "content" && Array.isArray(o.value)) {
|
|
1711
|
+
// Held-open continuation exec frames are text-only. Media is intentionally
|
|
1712
|
+
// omitted here and harvested from the full prompt when a fresh Run opens.
|
|
1607
1713
|
const text = o.value
|
|
1608
1714
|
.map((c) => {
|
|
1609
1715
|
const cp = c;
|
|
@@ -1703,7 +1809,7 @@ export function estimatePromptTokens(prompt) {
|
|
|
1703
1809
|
.join("\n");
|
|
1704
1810
|
return estimateTokens(serializedContent.length);
|
|
1705
1811
|
}
|
|
1706
|
-
/** Preserve
|
|
1812
|
+
/** Preserve exact Cursor counters as diagnostics when they are cumulative across a multi-step Run. */
|
|
1707
1813
|
export function cursorTurnEndedProviderMetadata(te) {
|
|
1708
1814
|
return {
|
|
1709
1815
|
cursor: {
|
package/dist/model-config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CURSOR_VARIANT_PARAMETERS_KEY, CURSOR_WIRE_MODEL_ID_KEY, parseCursorContextLimit, } from "./models.js";
|
|
2
2
|
import { applyCursorModelCost } from "./pricing.js";
|
|
3
|
+
import { getDocumentedCursorModelContext, resolveCursorModelSupportsImages, } from "./model-metadata.js";
|
|
3
4
|
/**
|
|
4
5
|
* Cursor `ModelInfo` → OpenCode model-config mapping.
|
|
5
6
|
*
|
|
@@ -147,19 +148,26 @@ export function modelInfoToConfig(mi, options = {}) {
|
|
|
147
148
|
// OpenCode's context limit is static per model entry, while Cursor's context
|
|
148
149
|
// tier is a variant parameter. Long-context choices are therefore emitted as
|
|
149
150
|
// separate OpenCode entries by modelsToConfig.
|
|
151
|
+
const documentedContext = getDocumentedCursorModelContext(mi.id);
|
|
150
152
|
const context = contextTier === "long"
|
|
151
|
-
? (mi.maxContextForMaxMode ?? 1_000_000)
|
|
152
|
-
: (mi.maxContext ?? 200_000);
|
|
153
|
+
? (mi.maxContextForMaxMode ?? documentedContext?.maxContextForMaxMode ?? 1_000_000)
|
|
154
|
+
: (mi.maxContext ?? documentedContext?.maxContext ?? 200_000);
|
|
153
155
|
// OpenCode's overflow/compaction/UI use limit.context; generation and
|
|
154
156
|
// thinking budgets use limit.output. models.dev 1M peers advertise
|
|
155
157
|
// 64k–128k output — a tiny cap makes long-context sessions feel broken
|
|
156
158
|
// even when the 1M input window is correct.
|
|
157
159
|
const output = contextTier === "long" ? 128_000 : 32_000;
|
|
160
|
+
const supportsImages = resolveCursorModelSupportsImages(mi.id, mi.supportsImages);
|
|
158
161
|
const config = {
|
|
159
162
|
name,
|
|
163
|
+
attachment: supportsImages,
|
|
160
164
|
reasoning: mi.supportsThinking ?? false,
|
|
161
165
|
tool_call: mi.supportsAgent ?? true,
|
|
162
166
|
temperature: false,
|
|
167
|
+
modalities: {
|
|
168
|
+
input: supportsImages ? ["text", "image"] : ["text"],
|
|
169
|
+
output: ["text"],
|
|
170
|
+
},
|
|
163
171
|
limit: {
|
|
164
172
|
context,
|
|
165
173
|
output,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type CursorModelContext = {
|
|
2
|
+
maxContext?: number;
|
|
3
|
+
maxContextForMaxMode?: number;
|
|
4
|
+
};
|
|
5
|
+
export type CursorModelCapabilities = {
|
|
6
|
+
supportsImages: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function getDocumentedCursorModelContext(modelId: string): CursorModelContext | undefined;
|
|
9
|
+
export declare function getDocumentedCursorModelCapabilities(modelId: string): CursorModelCapabilities | undefined;
|
|
10
|
+
export declare function resolveCursorModelSupportsImages(modelId: string, availableModelsValue?: boolean): boolean;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CURSOR_MODEL_CAPABILITIES, CURSOR_MODEL_CONTEXTS } from "./pricing-data.js";
|
|
2
|
+
export function getDocumentedCursorModelContext(modelId) {
|
|
3
|
+
const context = CURSOR_MODEL_CONTEXTS[modelId];
|
|
4
|
+
return context ? { ...context } : undefined;
|
|
5
|
+
}
|
|
6
|
+
export function getDocumentedCursorModelCapabilities(modelId) {
|
|
7
|
+
const capabilities = CURSOR_MODEL_CAPABILITIES[modelId];
|
|
8
|
+
return capabilities ? { ...capabilities } : undefined;
|
|
9
|
+
}
|
|
10
|
+
export function resolveCursorModelSupportsImages(modelId, availableModelsValue) {
|
|
11
|
+
return availableModelsValue ?? getDocumentedCursorModelCapabilities(modelId)?.supportsImages ?? false;
|
|
12
|
+
}
|
package/dist/models.d.ts
CHANGED
package/dist/models.js
CHANGED
|
@@ -109,6 +109,7 @@ function normalizeModelInfo(value) {
|
|
|
109
109
|
const family = optionalString(value, ["family"]);
|
|
110
110
|
const supportsThinking = optionalBoolean(value, ["supportsThinking", "supports_thinking"]);
|
|
111
111
|
const supportsAgent = optionalBoolean(value, ["supportsAgent", "supports_agent"]);
|
|
112
|
+
const supportsImages = optionalBoolean(value, ["supportsImages", "supports_images"]);
|
|
112
113
|
const supportsMaxMode = optionalBoolean(value, ["supportsMaxMode", "supports_max_mode"]);
|
|
113
114
|
const maxContext = optionalPositiveNumber(value, ["maxContext", "contextTokenLimit", "context_token_limit"]);
|
|
114
115
|
const maxContextForMaxMode = optionalPositiveNumber(value, ["maxContextForMaxMode", "contextTokenLimitForMaxMode", "context_token_limit_for_max_mode"]);
|
|
@@ -116,6 +117,7 @@ function normalizeModelInfo(value) {
|
|
|
116
117
|
family === null ||
|
|
117
118
|
supportsThinking === null ||
|
|
118
119
|
supportsAgent === null ||
|
|
120
|
+
supportsImages === null ||
|
|
119
121
|
supportsMaxMode === null ||
|
|
120
122
|
maxContext === null ||
|
|
121
123
|
maxContextForMaxMode === null) {
|
|
@@ -127,6 +129,7 @@ function normalizeModelInfo(value) {
|
|
|
127
129
|
...(family === undefined ? {} : { family }),
|
|
128
130
|
...(supportsThinking === undefined ? {} : { supportsThinking }),
|
|
129
131
|
...(supportsAgent === undefined ? {} : { supportsAgent }),
|
|
132
|
+
...(supportsImages === undefined ? {} : { supportsImages }),
|
|
130
133
|
...(maxContext === undefined ? {} : { maxContext }),
|
|
131
134
|
...(maxContextForMaxMode === undefined ? {} : { maxContextForMaxMode }),
|
|
132
135
|
...(supportsMaxMode === undefined ? {} : { supportsMaxMode }),
|
|
@@ -293,11 +296,16 @@ export function mapAvailableModelsResponse(raw) {
|
|
|
293
296
|
variantContextTokens(variants.find((v) => variantContextTokens(v) !== 1_000_000)) ??
|
|
294
297
|
variantContextTokens(variants.find((v) => v.parameterValues.some((p) => p.id === "context")));
|
|
295
298
|
const variantMaxContext = variantContextTokens(variants.find((v) => v.isDefaultMax));
|
|
299
|
+
const supportsImages = optionalBoolean(e, ["supportsImages", "supports_images"]);
|
|
300
|
+
if (supportsImages === null) {
|
|
301
|
+
throw new Error(`AvailableModels returned invalid supportsImages for ${name}`);
|
|
302
|
+
}
|
|
296
303
|
models.push({
|
|
297
304
|
id: name,
|
|
298
305
|
displayName: (e.clientDisplayName ?? e.client_display_name ?? name),
|
|
299
306
|
supportsThinking: apiBoolean(e, ["supportsThinking", "supports_thinking"]),
|
|
300
307
|
supportsAgent: apiBoolean(e, ["supportsAgent", "supports_agent"]),
|
|
308
|
+
...(supportsImages === undefined ? {} : { supportsImages }),
|
|
301
309
|
maxContext: variantBaseContext ?? positiveNumber(e.contextTokenLimit ?? e.context_token_limit),
|
|
302
310
|
maxContextForMaxMode: variantMaxContext ??
|
|
303
311
|
positiveNumber(e.contextTokenLimitForMaxMode ?? e.context_token_limit_for_max_mode),
|
|
@@ -58,12 +58,16 @@ function applyModelEntry(draft, id, entry) {
|
|
|
58
58
|
model.modelID = wireId;
|
|
59
59
|
model.providerID = CURSOR_PROVIDER_ID;
|
|
60
60
|
model.name = entry.name;
|
|
61
|
+
const inputModalities = Array.isArray(entry.modalities?.input)
|
|
62
|
+
? entry.modalities.input.filter((modality) => typeof modality === "string")
|
|
63
|
+
: ["text"];
|
|
64
|
+
const outputModalities = Array.isArray(entry.modalities?.output)
|
|
65
|
+
? entry.modalities.output.filter((modality) => typeof modality === "string")
|
|
66
|
+
: ["text"];
|
|
61
67
|
model.capabilities = {
|
|
62
68
|
tools: entry.tool_call !== false,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
input: ["text"],
|
|
66
|
-
output: ["text"],
|
|
69
|
+
input: inputModalities,
|
|
70
|
+
output: outputModalities,
|
|
67
71
|
};
|
|
68
72
|
model.limit = { context: entry.limit.context, output: entry.limit.output };
|
|
69
73
|
model.variants = variants;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* we touch is more stable than pinning a moving target.
|
|
10
10
|
*
|
|
11
11
|
* These mirror `@opencode-ai/plugin@next` `dist/promise/*.d.ts` as of
|
|
12
|
-
* `0.0.0-next-
|
|
12
|
+
* `0.0.0-next-17155`. `test/opencode2-conformance.types.ts` asserts our
|
|
13
13
|
* default export still satisfies the real 2.0 `Plugin` interface.
|
|
14
14
|
*
|
|
15
15
|
* Effect `Schema` brands (Provider.ID, Model.ID, …) are modelled as plain
|
|
@@ -228,13 +228,19 @@ export type ToolDefinition = {
|
|
|
228
228
|
export type ToolDraft = {
|
|
229
229
|
add(tool: ToolDefinition): void;
|
|
230
230
|
};
|
|
231
|
-
export type
|
|
231
|
+
export type ToolHookBaseFields = {
|
|
232
232
|
readonly tool: string;
|
|
233
233
|
readonly sessionID: string;
|
|
234
234
|
readonly agent: string;
|
|
235
235
|
readonly messageID: string;
|
|
236
|
-
readonly callID: string;
|
|
237
236
|
};
|
|
237
|
+
export type ToolHookBase = ToolHookBaseFields & ({
|
|
238
|
+
readonly id: string;
|
|
239
|
+
readonly callID?: string;
|
|
240
|
+
} | {
|
|
241
|
+
readonly id?: string;
|
|
242
|
+
readonly callID: string;
|
|
243
|
+
});
|
|
238
244
|
export type ToolHooks = {
|
|
239
245
|
readonly "execute.before": ToolHookBase & {
|
|
240
246
|
input: unknown;
|
package/dist/opencode2/types.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* we touch is more stable than pinning a moving target.
|
|
10
10
|
*
|
|
11
11
|
* These mirror `@opencode-ai/plugin@next` `dist/promise/*.d.ts` as of
|
|
12
|
-
* `0.0.0-next-
|
|
12
|
+
* `0.0.0-next-17155`. `test/opencode2-conformance.types.ts` asserts our
|
|
13
13
|
* default export still satisfies the real 2.0 `Plugin` interface.
|
|
14
14
|
*
|
|
15
15
|
* Effect `Schema` brands (Provider.ID, Model.ID, …) are modelled as plain
|
package/dist/plugin-opencode2.js
CHANGED
|
@@ -35,6 +35,12 @@ async function loadModels(cacheDir, accessToken) {
|
|
|
35
35
|
// Preserve offline / stale-cache behavior rather than emptying the picker.
|
|
36
36
|
return cached?.models ?? [];
|
|
37
37
|
}
|
|
38
|
+
function toolExecutionID(event) {
|
|
39
|
+
const id = event.id ?? event.callID;
|
|
40
|
+
if (!id)
|
|
41
|
+
throw new Error("OpenCode 2.0 tool hook did not provide an execution id");
|
|
42
|
+
return id;
|
|
43
|
+
}
|
|
38
44
|
const plugin = {
|
|
39
45
|
id: "cursor.provider",
|
|
40
46
|
setup: async (ctx) => {
|
|
@@ -142,29 +148,31 @@ const plugin = {
|
|
|
142
148
|
await track(ctx.tool.hook("execute.before", (event) => {
|
|
143
149
|
if (event.tool !== "bash")
|
|
144
150
|
return;
|
|
151
|
+
const executionID = toolExecutionID(event);
|
|
145
152
|
// No `shell.env` in 2.0 → always use the wrapper-file command form.
|
|
146
|
-
prepareCursorShellArgs(
|
|
153
|
+
prepareCursorShellArgs(executionID, event.input, {
|
|
147
154
|
preferWrapperCommand: true,
|
|
148
155
|
});
|
|
149
156
|
}));
|
|
150
157
|
await track(ctx.tool.hook("execute.after", (event) => {
|
|
151
158
|
if (event.tool !== "bash")
|
|
152
159
|
return;
|
|
160
|
+
const executionID = toolExecutionID(event);
|
|
153
161
|
try {
|
|
154
162
|
if (event.status !== "completed")
|
|
155
163
|
return;
|
|
156
164
|
const result = event.result;
|
|
157
|
-
result.title = cursorShellOriginalCommand(
|
|
158
|
-
result.output = captureCursorShellResult(
|
|
165
|
+
result.title = cursorShellOriginalCommand(executionID) ?? result.title;
|
|
166
|
+
result.output = captureCursorShellResult(executionID, result.output, result.metadata);
|
|
159
167
|
if (result.metadata && typeof result.metadata === "object") {
|
|
160
168
|
const metadata = result.metadata;
|
|
161
169
|
if (typeof metadata.output === "string") {
|
|
162
|
-
metadata.output = sanitizeRegisteredCursorShellOutput(
|
|
170
|
+
metadata.output = sanitizeRegisteredCursorShellOutput(executionID, metadata.output);
|
|
163
171
|
}
|
|
164
172
|
}
|
|
165
173
|
}
|
|
166
174
|
finally {
|
|
167
|
-
releaseCursorShellEnv(
|
|
175
|
+
releaseCursorShellEnv(executionID);
|
|
168
176
|
}
|
|
169
177
|
}));
|
|
170
178
|
// ── Compaction marker + session directory ───────────────────────────────
|