cursor-opencode-provider 0.4.2 → 0.4.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/README.md +2 -2
- package/dist/language-model.js +11 -2
- package/dist/pricing-data.d.ts +6 -0
- package/dist/pricing-data.js +6 -0
- package/dist/pricing.d.ts +1 -1
- package/dist/pricing.js +1 -1
- package/dist/usage.d.ts +15 -0
- package/dist/usage.js +24 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -460,7 +460,7 @@ The package root intentionally stays plugin-safe for OpenCode's classic loader.
|
|
|
460
460
|
| Visible `<shell_metadata>` timeout text | Rebuild and restart a local install. Cursor's shell timeout is carried on its exec request; the provider now removes OpenCode's internal timeout envelope before it is rendered or stored, then returns Cursor's typed timeout or background-handoff event instead of treating the text as successful stdout. |
|
|
461
461
|
| `Unsupported Cursor exec variant …` | The error names the canonical Cursor CLI request field, its expected result field, and this provider's handling classification. `handling=unsupported` is a known Cursor-native capability without a safe OpenCode AI SDK bridge; `unknown request field` indicates new protocol drift; `handling=opencode-tool` or `provider-control` indicates a provider decoder/dispatch regression. Enable the debug log and report the full named error. |
|
|
462
462
|
| Need wire-level logs | Set `CURSOR_PROVIDER_DEBUG=1` (optional `CURSOR_PROVIDER_DEBUG_FILE`; the default is `debug-<pid>.log` under `$TMPDIR/cursor-provider-logs-<uid>/`) and reproduce the issue. |
|
|
463
|
-
| OpenCode compacts immediately after a tool-heavy task | Upgrade to a version containing the cumulative-usage fix. Cursor's final `TurnEnded` counters cover the entire held multi-step Run; older provider versions exposed that aggregate as the final request's context size, which made OpenCode compact far too early. Fixed versions keep the raw counters in `providerMetadata.cursor` and report a request-local context estimate instead. |
|
|
463
|
+
| OpenCode compacts immediately after a tool-heavy task | Upgrade to a version containing the cumulative-usage fix. Cursor's final `TurnEnded` counters cover the entire held multi-step Run; older provider versions exposed that aggregate as the final request's context size, which made OpenCode compact far too early. Fixed versions keep the raw counters in `providerMetadata.cursor` and report a request-local context estimate instead. An over-budget guard also catches single-step or parallel Runs whose counters are cumulative even without a visible tool boundary. |
|
|
464
464
|
| Cache **write** tokens always show as `0` | Cursor's `TurnEnded` protobuf includes `cache_write` (field 4), but captured Runs set it to `0` even when `cache_read` is large. Single-step Runs expose those counters directly. For multi-step Runs they remain in `providerMetadata.cursor` because Cursor reports only a cumulative whole-Run total, which cannot safely be used as one OpenCode request's context usage. With debug logging, confirm `turn_ended raw wire fields: … f4:wt0=0`. |
|
|
465
465
|
|
|
466
466
|
## Security
|
|
@@ -470,7 +470,7 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
|
|
|
470
470
|
## Known limitations
|
|
471
471
|
|
|
472
472
|
- **Personal use / ToS** — this provider speaks Cursor’s private agent protocol (CLI-shaped client identity). Use only with an account you own; Cursor may change or restrict the API without notice.
|
|
473
|
-
- **`TurnEnded` usage is whole-Run usage** — on a held multi-step Cursor Run, the final `interaction_update.turn_ended` counters aggregate every model invocation across tool boundaries. OpenCode usage is request-local and uses the latest assistant message to decide compaction, so publishing the aggregate there both double-counts earlier steps and causes premature compaction. The provider therefore uses a current-context estimate for multi-step AI SDK usage and preserves the exact cumulative counters in `providerMetadata.cursor`; single-step Runs
|
|
473
|
+
- **`TurnEnded` usage is whole-Run usage** — on a held multi-step Cursor Run, the final `interaction_update.turn_ended` counters aggregate every model invocation across tool boundaries. OpenCode usage is request-local and uses the latest assistant message to decide compaction, so publishing the aggregate there both double-counts earlier steps and causes premature compaction. The provider therefore uses a current-context estimate for multi-step AI SDK usage and preserves the exact cumulative counters in `providerMetadata.cursor`; single-step Runs map the counters directly only when they are close to the request-local estimate, while an over-budget guard handles cumulative no-boundary Runs. Cursor also consistently sends `cache_write=0` in captured Runs, so no alternate write count is available.
|
|
474
474
|
- **`request_context` from OpenCode** — each Run sends Cursor `RequestContext` built from OpenCode project context (workspace env, `AGENTS.md` / `instructions`, `.opencode` agents/skills/plugins, git, layout, plus `.claude`/`.agents` skill fallbacks). Its canonical root is also used by the [injected system guidance](#injected-system-guidance). Same discovery as OpenCode — including `.cursor/` paths only when listed in `instructions`. Cursor-only cloud/sandbox marketplace surfaces are omitted. `env.workspace_paths` / `process_working_directory` stay on the real git workspace; Cursor's metadata root (`project_folder`, MCP `workspace_project_dir`, terminals/transcripts) is advertised under `<host-cache>/projects/<slug>/` (default `~/.cache/opencode/projects/…`) so dumps like `agent-tools/` do not land in the repo. OpenCode remains the permission authority: its coarse allow/ask/deny configuration is not fabricated into Cursor's unrelated allow/block instruction-list messages.
|
|
475
475
|
- **Configured MCP tools keep their upstream server id** — OpenCode builtins and plugin/custom tools are advertised under a synthetic `opencode` MCP server. Tools whose flattened name matches an MCP server in merged `opencode.json` configuration (`github_create_pull_request`, …) are grouped into that server's `mcp_descriptors` / `provider_identifier` (`github`, …). Unknown underscore-containing names stay under `opencode` rather than being guessed incorrectly. Cursor's MCP-state exec probe is answered from the same advertised descriptors before the actual tool request, using the full canonical tool-definition identity required by native `get_mcp_tools`; exec still reconstructs the full OpenCode tool id.
|
|
476
476
|
- **Display completions are notifications, not execution requests** — Cursor `tool_call_*` frames use a typed `ToolCall` oneof. The provider decodes them for diagnostics but only mirrors finalized todo/plan state (`update_todos_tool_call` / `create_plan_tool_call`) into advertised OpenCode `todowrite`; the completed payload already contains the authoritative final list. Interactive, data-returning, and side-effecting completions are never replayed as new tools because their result could not be returned to Cursor. Exec-backed native subagent/Task and Pi read/bash/edit/write/grep/find/ls calls use their typed request/result fields instead. Unknown display variants are logged. All 37 Cursor CLI exec request/result pairs are inventoried by field and name; known-but-unsupported and future unknown exec variants fail explicitly rather than receiving a guessed response that could deadlock the Run.
|
package/dist/language-model.js
CHANGED
|
@@ -29,7 +29,7 @@ import { resolveCursorModelSupportsImages } from "./model-metadata.js";
|
|
|
29
29
|
import { consumeCursorShellResult, registerCursorShellCall, } from "./shell-timeout.js";
|
|
30
30
|
import { analyzeReplayFrame, AttemptReplaySafety } from "./replay-safety.js";
|
|
31
31
|
import { readAllFieldsStrict } from "./protocol/struct.js";
|
|
32
|
-
import { buildLanguageModelV3UsageFromEstimate, buildLanguageModelV3UsageFromTurnEnded, turnEndedCounter, } from "./usage.js";
|
|
32
|
+
import { buildLanguageModelV3UsageFromEstimate, buildLanguageModelV3UsageFromTurnEnded, exceedsRequestLocalBudget, turnEndedCounter, } from "./usage.js";
|
|
33
33
|
let _availableModels;
|
|
34
34
|
// mtime of the cache file the last time we loaded it. Compared on each call
|
|
35
35
|
// so discoverModels' background refresh is picked up without a process restart.
|
|
@@ -1164,7 +1164,12 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1164
1164
|
// a tool boundary. On a held multi-step Run Cursor aggregates every model
|
|
1165
1165
|
// invocation into these counters; treating that billed total as the final
|
|
1166
1166
|
// request's prompt size makes OpenCode compact after ordinary tasks.
|
|
1167
|
-
const
|
|
1167
|
+
const cumulativeByBoundary = !!te && session.hadToolCallBoundary === true;
|
|
1168
|
+
const cumulativeByBudget = !!te && !cumulativeByBoundary && exceedsRequestLocalBudget(te, {
|
|
1169
|
+
inputTokens: session.usageEstimate.inputTokens,
|
|
1170
|
+
outputTokens: session.usageEstimate.outputTokens,
|
|
1171
|
+
});
|
|
1172
|
+
const cumulativeTurnEnded = cumulativeByBoundary || cumulativeByBudget;
|
|
1168
1173
|
if (te && !cumulativeTurnEnded) {
|
|
1169
1174
|
session.usageEstimate = {
|
|
1170
1175
|
inputTokens: turnEndedCounter(te, "input_tokens"),
|
|
@@ -1194,6 +1199,7 @@ export async function pump(session, controller, ids, abortSignal) {
|
|
|
1194
1199
|
`rawOut=${te ? turnEndedCounter(te, "output_tokens") : est.outputTokens} ` +
|
|
1195
1200
|
`rawCacheRead=${te ? turnEndedCounter(te, "cache_read") : est.cacheRead} ` +
|
|
1196
1201
|
`rawCacheWrite=${te ? turnEndedCounter(te, "cache_write") : est.cacheWrite} ` +
|
|
1202
|
+
`guard=${cumulativeByBoundary ? "boundary" : cumulativeByBudget ? "over-budget" : "none"} ` +
|
|
1197
1203
|
`source=${te ? (cumulativeTurnEnded ? "estimate-cumulative-turn-ended" : "turn_ended") : "estimate"}`);
|
|
1198
1204
|
safeEnqueue({
|
|
1199
1205
|
type: "finish",
|
|
@@ -1822,6 +1828,9 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
|
|
|
1822
1828
|
// leaving the turn with no file-editing guidance at all.
|
|
1823
1829
|
instructions.push("- Use OpenCode `apply_patch` for file-content changes; do not use shell, Python, or heredocs to change file content while it is available. Cursor-native write and edit requests are accepted and converted to `apply_patch` automatically.");
|
|
1824
1830
|
}
|
|
1831
|
+
if (names.has("edit") || names.has("write") || names.has("apply_patch")) {
|
|
1832
|
+
instructions.push("- Never use a read result as complete file content when it says the output is capped, partial, or requires another offset. Read the remaining ranges first, or make a targeted edit/patch from complete context; do not pass a partial read back as a whole-file replacement.");
|
|
1833
|
+
}
|
|
1825
1834
|
return [
|
|
1826
1835
|
`OpenCode exposes exactly these executable tools for this turn: ${[...names].map((name) => `\`${name}\``).join(", ")}.`,
|
|
1827
1836
|
`Workspace root: ${JSON.stringify(workspaceRoot)}. Resolve workspace paths against exactly this root; never invent an absolute prefix, and verify uncertain paths with an available tool before using them.`,
|
package/dist/pricing-data.d.ts
CHANGED
|
@@ -303,6 +303,9 @@ export declare const CURSOR_MODEL_CONTEXTS: {
|
|
|
303
303
|
readonly "grok-4.5": {
|
|
304
304
|
readonly maxContext: 256000;
|
|
305
305
|
};
|
|
306
|
+
readonly "grok-4.6": {
|
|
307
|
+
readonly maxContext: 256000;
|
|
308
|
+
};
|
|
306
309
|
readonly "kimi-k2.7-code": {
|
|
307
310
|
readonly maxContext: 262000;
|
|
308
311
|
};
|
|
@@ -405,6 +408,9 @@ export declare const CURSOR_MODEL_CAPABILITIES: {
|
|
|
405
408
|
readonly "grok-4.5": {
|
|
406
409
|
readonly supportsImages: false;
|
|
407
410
|
};
|
|
411
|
+
readonly "grok-4.6": {
|
|
412
|
+
readonly supportsImages: false;
|
|
413
|
+
};
|
|
408
414
|
readonly "kimi-k2.7-code": {
|
|
409
415
|
readonly supportsImages: true;
|
|
410
416
|
};
|
package/dist/pricing-data.js
CHANGED
|
@@ -303,6 +303,9 @@ export const CURSOR_MODEL_CONTEXTS = {
|
|
|
303
303
|
"grok-4.5": {
|
|
304
304
|
"maxContext": 256000
|
|
305
305
|
},
|
|
306
|
+
"grok-4.6": {
|
|
307
|
+
"maxContext": 256000
|
|
308
|
+
},
|
|
306
309
|
"kimi-k2.7-code": {
|
|
307
310
|
"maxContext": 262000
|
|
308
311
|
},
|
|
@@ -405,6 +408,9 @@ export const CURSOR_MODEL_CAPABILITIES = {
|
|
|
405
408
|
"grok-4.5": {
|
|
406
409
|
"supportsImages": false
|
|
407
410
|
},
|
|
411
|
+
"grok-4.6": {
|
|
412
|
+
"supportsImages": false
|
|
413
|
+
},
|
|
408
414
|
"kimi-k2.7-code": {
|
|
409
415
|
"supportsImages": true
|
|
410
416
|
},
|
package/dist/pricing.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export type CursorPricingCoverage = {
|
|
|
34
34
|
* Models we expose that Cursor does not currently publish numeric rates for
|
|
35
35
|
* on the Other Models table (Auto / first-party Cursor Models pool).
|
|
36
36
|
*/
|
|
37
|
-
export declare const CURSOR_UNPRICED_MODEL_IDS: readonly ["default", "composer-2.5", "grok-4.5"];
|
|
37
|
+
export declare const CURSOR_UNPRICED_MODEL_IDS: readonly ["default", "composer-2.5", "grok-4.5", "grok-4.6"];
|
|
38
38
|
export declare function validateOpenCodeModelCost(value: unknown, path?: string): {
|
|
39
39
|
valid: boolean;
|
|
40
40
|
errors: string[];
|
package/dist/pricing.js
CHANGED
|
@@ -10,7 +10,7 @@ import { CURSOR_MODEL_COSTS } from "./pricing-data.js";
|
|
|
10
10
|
* Models we expose that Cursor does not currently publish numeric rates for
|
|
11
11
|
* on the Other Models table (Auto / first-party Cursor Models pool).
|
|
12
12
|
*/
|
|
13
|
-
export const CURSOR_UNPRICED_MODEL_IDS = ["default", "composer-2.5", "grok-4.5"];
|
|
13
|
+
export const CURSOR_UNPRICED_MODEL_IDS = ["default", "composer-2.5", "grok-4.5", "grok-4.6"];
|
|
14
14
|
const UNPRICED = new Set(CURSOR_UNPRICED_MODEL_IDS);
|
|
15
15
|
function isRecord(value) {
|
|
16
16
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
package/dist/usage.d.ts
CHANGED
|
@@ -19,6 +19,21 @@ export type CursorUsageEstimate = {
|
|
|
19
19
|
cacheWrite: number;
|
|
20
20
|
reasoningTokens: number;
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* A single-step trust path for TurnEnded counters is only safe when the
|
|
24
|
+
* totals are close to the request-local budget. Cursor reports cumulative
|
|
25
|
+
* conversation/Run totals even on Runs that never crossed an OpenCode tool
|
|
26
|
+
* boundary: single-step follow-up turns and parallel no-tools side calls
|
|
27
|
+
* carry the whole conversation's `cache_read`. Treating those totals as
|
|
28
|
+
* request-local inflates OpenCode's context usage and triggers premature
|
|
29
|
+
* compaction (issue #20).
|
|
30
|
+
*/
|
|
31
|
+
export declare const CUMULATIVE_TURN_ENDED_RATIO = 3;
|
|
32
|
+
export declare const MIN_TRUSTED_TURN_ENDED_TOTAL = 1024;
|
|
33
|
+
export declare function exceedsRequestLocalBudget(te: Record<string, unknown>, budget: {
|
|
34
|
+
inputTokens: number;
|
|
35
|
+
outputTokens: number;
|
|
36
|
+
}): boolean;
|
|
22
37
|
/**
|
|
23
38
|
* Request-local usage for a held Cursor Run.
|
|
24
39
|
*
|
package/dist/usage.js
CHANGED
|
@@ -35,6 +35,30 @@ export function buildLanguageModelV3UsageFromTurnEnded(te) {
|
|
|
35
35
|
},
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* A single-step trust path for TurnEnded counters is only safe when the
|
|
40
|
+
* totals are close to the request-local budget. Cursor reports cumulative
|
|
41
|
+
* conversation/Run totals even on Runs that never crossed an OpenCode tool
|
|
42
|
+
* boundary: single-step follow-up turns and parallel no-tools side calls
|
|
43
|
+
* carry the whole conversation's `cache_read`. Treating those totals as
|
|
44
|
+
* request-local inflates OpenCode's context usage and triggers premature
|
|
45
|
+
* compaction (issue #20).
|
|
46
|
+
*/
|
|
47
|
+
export const CUMULATIVE_TURN_ENDED_RATIO = 3;
|
|
48
|
+
export const MIN_TRUSTED_TURN_ENDED_TOTAL = 1024;
|
|
49
|
+
export function exceedsRequestLocalBudget(te, budget) {
|
|
50
|
+
const total = turnEndedCounter(te, "input_tokens") +
|
|
51
|
+
turnEndedCounter(te, "cache_read") +
|
|
52
|
+
turnEndedCounter(te, "cache_write");
|
|
53
|
+
const requestLocal = budget.inputTokens + budget.outputTokens;
|
|
54
|
+
// Skip when the request-local seed is unknown; otherwise compare against
|
|
55
|
+
// max(ratio × request-local, absolute floor) so tiny prompts still reject
|
|
56
|
+
// multi-million-token conversation cache totals.
|
|
57
|
+
if (requestLocal <= 0)
|
|
58
|
+
return false;
|
|
59
|
+
const trustCeiling = Math.max(requestLocal * CUMULATIVE_TURN_ENDED_RATIO, MIN_TRUSTED_TURN_ENDED_TOTAL);
|
|
60
|
+
return total > trustCeiling;
|
|
61
|
+
}
|
|
38
62
|
/**
|
|
39
63
|
* Request-local usage for a held Cursor Run.
|
|
40
64
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-opencode-provider",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Use Cursor subscription models from OpenCode via Cursor's Connect-RPC agent protocol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
"@opencode-ai/plugin": "^1.18.16",
|
|
93
93
|
"@opencode-ai/plugin-next": "npm:@opencode-ai/plugin@0.0.0-next-17155",
|
|
94
94
|
"@tsconfig/node22": "^22.0.5",
|
|
95
|
+
"@types/bun": "^1.3.0",
|
|
95
96
|
"@types/node": "^22.20.1",
|
|
96
97
|
"typescript": "^5.9.3"
|
|
97
98
|
}
|