cursor-opencode-provider 0.3.0 → 0.3.2

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 CHANGED
@@ -47,7 +47,7 @@ Add the package to OpenCode config. OpenCode installs npm plugins with Bun at st
47
47
  }
48
48
  ```
49
49
 
50
- Pin a version if you want: `"cursor-opencode-provider@0.2.9"`.
50
+ Pin a version if you want: `"cursor-opencode-provider@0.3.2"`.
51
51
 
52
52
  You can also install it yourself first:
53
53
 
@@ -117,7 +117,7 @@ browser login or an API key. `CURSOR_API_KEY` is also picked up automatically.
117
117
  }
118
118
  ```
119
119
 
120
- Pin a version if you want: `"cursor-opencode-provider@0.2.9/plugin/opencode2"`.
120
+ Pin a version if you want: `"cursor-opencode-provider@0.3.2/plugin/opencode2"`.
121
121
 
122
122
  OpenCode 2.0 installs the published package into its host cache and loads the AI SDK
123
123
  entry from there (`aisdk:cursor-opencode-provider`). No extra env vars are required.
@@ -337,6 +337,7 @@ OpenCode
337
337
  | `src/opencode2/` | 2.0-only catalog mapping, integration/auth, and local API types |
338
338
  | `src/plugin-core.ts` | Host-neutral SDK factory, package matching, API base/telemetry resolution |
339
339
  | `src/model-config.ts` | Cursor model → OpenCode model mapping shared by every plugin surface |
340
+ | `src/pricing.ts` / `src/pricing-data.ts` | Cursor docs token rates → classic `cost` and OpenCode 2.0 cost tiers |
340
341
  | `src/index.ts` | `createCursor` factory; default export is `CursorPlugin` |
341
342
  | `src/language-model.ts` | AI SDK `LanguageModelV3` adapter (`doStream`, `doGenerate`) |
342
343
  | `src/session.ts` | Held-open agent Run session and pending exec correlation |
@@ -350,10 +351,18 @@ OpenCode
350
351
 
351
352
  ### Injected system guidance
352
353
 
353
- The provider adds OpenCode-specific system guidance to normal tool-capable conversations, including tool availability, canonical workspace-path grounding, and preferring `edit` / `write` over shell-based file mutation when those tools are available. Compaction keeps its dedicated prompt unchanged.
354
+ The provider adds OpenCode-specific system guidance to normal tool-capable conversations, including tool availability, canonical workspace-path grounding, and preferring `edit` / `write` (or `apply_patch`, when the host advertises that instead) over shell-based file mutation when those tools are available. Compaction keeps its dedicated prompt unchanged.
354
355
 
355
356
  If this guidance causes issues, update `buildOpenCodeInteractionGuidance` in [`src/language-model.ts`](src/language-model.ts) and its focused coverage in [`test/prompt-history.test.ts`](test/prompt-history.test.ts).
356
357
 
358
+ ### `apply_patch` models (GPT-5 and friends)
359
+
360
+ OpenCode 1.x does not simply add `apply_patch` for GPT-series models — it **removes** `edit` and `write` from the tool catalog and advertises `apply_patch` in their place (`ToolRegistry.tools`; the model id must contain `gpt-` and neither `oss` nor `gpt-4`). Cursor keeps sending its native write/edit requests regardless, so without translation every file change on a `gpt-5*` model is refused as an unavailable tool.
361
+
362
+ The provider translates transparently: a native write becomes an `*** Add File:` patch, and a Pi edit becomes a minimal `*** Update File:` chunk widened to whole lines. This is keyed purely off what the host advertises — it is inert whenever `edit`/`write` are offered normally, and inert again when the host offers neither those nor `apply_patch`. An edit that cannot be expressed faithfully (target unreadable, or the text to replace is absent or ambiguous) is refused with a specific message rather than applied to the wrong region.
363
+
364
+ OpenCode 2.0 does not perform this substitution, so the path is 1.x-only in practice. See [`src/protocol/apply-patch.ts`](src/protocol/apply-patch.ts) for the upstream references.
365
+
357
366
  ### Native subagent routing
358
367
 
359
368
  The provider reads the permission-filtered subagent catalog from the current host `task` or `actor` definition and advertises those recipients to Cursor as custom subagents. Exact configured names are preserved; unknown future Cursor subtype strings fall back to `general`. If a complete catalog omits every compatible recipient, the request fails on Cursor's typed subagent channel instead of selecting an arbitrary specialist.
@@ -412,6 +421,7 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
412
421
  - **Compaction resets Cursor conversation state** — the classic plugin marks OpenCode's `compaction` agent explicitly. On those turns the provider mints an isolated Cursor `conversation_id`, drops the prior checkpoint + KV blobs, preserves real tool outputs as OpenCode-host observations in the seed history, and re-advertises the session's last tool catalog while refusing execution during the summary itself. The first normal turn then rebases once more onto a fresh conversation seeded with OpenCode's compacted prompt and normal system instructions, so the summary-agent checkpoint cannot suppress later tool calls. Ordinary no-tool / `toolChoice:none` calls do not reset conversation state.
413
422
  - **Conversation bindings and compaction catalogs are bounded** — process-global per-session bindings, prior tool catalogs, and pending post-compaction rebases use a 256-session LRU bound. Evicting a conversation binding also drops its checkpoint and KV blobs.
414
423
  - **Interrupted Runs resume from checkpoints** — a remote EOF, Connect end-stream, or trailer error is never emitted as a successful `stop`. When the failed Run produced an eligible checkpoint, the provider opens a new RPC for the same conversation and sends that state with `ResumeAction`, so completed text and tool work are not replayed. Before any stateful output, an interruption without a checkpoint can still rebase from OpenCode history. Stateful interruptions without a checkpoint are surfaced because replay would be ambiguous; retry exhaustion remains explicit. A transport closure after `turn_ended` is treated as successful completion.
424
+ - **Large reads are capped at 50 KB by OpenCode, and the cap is reported** — OpenCode's `read` tool stops at `MAX_BYTES = 50 * 1024`, cutting on a whole-line boundary, and appends `(Output capped at 50 KB. Showing lines X-Y. Use offset=N to continue.)`. The provider strips that envelope before returning content to Cursor so the model cannot echo wrappers into a later write, but it re-states the cap: a capped native read appends a `[Partial read: …]` marker after the content, a capped MCP read gets a separate notice content item, and a Pi read carries structured `PiReadExecSuccess.truncation`. The structured `truncated` flag alone is not enough — verified against live `gpt-5.4-mini` and `grok-4.5` sessions, both of which asserted "highly confident" that partial content was the whole file until the textual marker was added. Deliberately paged reads (explicit `offset`/`limit`) are not marked. Without that signal the model treats a capped read as the whole file and rewrites it truncated, discarding everything past the cap. The cap is hardcoded in OpenCode's read tool — it is not the configurable `tool_output.max_bytes`, which governs `Truncate.Service` and not read's line accumulation — so the only lever is paging with `offset`. Prefer `edit` over whole-file `write` on files above 50 KB.
415
425
  - **No fallback models** — if Cursor’s `AvailableModels` API is unreachable and there is no local cache, the provider exposes no models.
416
426
 
417
427
  ## License
package/dist/agent-url.js CHANGED
@@ -21,6 +21,8 @@ function normalizeApiBaseURL(baseURL) {
21
21
  return new URL(baseURL).origin;
22
22
  }
23
23
  function resolveCacheKey(token, options) {
24
+ // codeql[js/insufficient-password-hash] -- The token is a high-entropy JWT/API key,
25
+ // and this digest is only an ephemeral memoization key, never a password verifier.
24
26
  const tokenHash = createHash("sha256").update(token).digest("hex").slice(0, 16);
25
27
  return `${tokenHash}|${normalizeApiBaseURL(options.apiBaseURL ?? options.baseURL)}|telem:${options.telemetryEnabled === true}`;
26
28
  }
@@ -145,8 +145,9 @@ export function slugifyWorkspacePath(workspaceRoot) {
145
145
  const resolved = path.resolve(workspaceRoot);
146
146
  return resolved
147
147
  .replace(/[^a-zA-Z0-9]/g, "-")
148
- .replace(/-+/g, "-")
149
- .replace(/^-+|-+$/g, "");
148
+ .split("-")
149
+ .filter(Boolean)
150
+ .join("-");
150
151
  }
151
152
  /**
152
153
  * Cursor-style project metadata root for a workspace.
@@ -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, 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, 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";
@@ -954,7 +954,9 @@ export async function pump(session, controller, ids, abortSignal) {
954
954
  if (!displayCallId ||
955
955
  parsed.resultField !== "read_result" ||
956
956
  parsed.toolName !== "read" ||
957
- !advertisedToolNameSet.has("write"))
957
+ // `apply_patch` is the substitute this host offers when it withholds
958
+ // `write` — the follow-up write_args is remapped onto it either way.
959
+ !(advertisedToolNameSet.has("write") || advertisedToolNameSet.has("apply_patch")))
958
960
  return false;
959
961
  const stored = session.displayToolCalls.get(displayCallId);
960
962
  const display = parseDisplayToolCall(displayCallId, stored);
@@ -1377,6 +1379,10 @@ export async function pump(session, controller, ids, abortSignal) {
1377
1379
  parsed.toolName = executableToolName;
1378
1380
  }
1379
1381
  remapNativeSubagentForCatalog(parsed, advertisedToolNameSet, session.subagentCatalog);
1382
+ // Hosts that advertise `apply_patch` in place of `edit`/`write` (see
1383
+ // protocol/apply-patch.ts) still receive Cursor's native write/edit
1384
+ // exec requests. Translate before the unavailable-tool check below.
1385
+ remapEditToolsForCatalog(parsed, advertisedToolNameSet, workspaceRootFromRequestContext(session.requestContext));
1380
1386
  }
1381
1387
  const displayCallId = extractExecDisplayCallId(esm);
1382
1388
  trace(`exec: id=${parsed?.id} variant=${parsed ? Object.keys(parsed).join(",") : "none"} toolName=${parsed?.toolName} resultField=${parsed?.resultField}`);
@@ -1663,6 +1669,12 @@ export function buildOpenCodeInteractionGuidance(tools, isCompaction, workspaceR
1663
1669
  ? "- For file changes, use OpenCode `edit` for targeted changes to existing files and `write` to create files or intentionally replace complete contents; do not use shell, Python, or heredocs to change file content while these tools are available."
1664
1670
  : "- Use OpenCode `write` for file-content changes; do not use shell, Python, or heredocs to change file content while it is available.");
1665
1671
  }
1672
+ else if (names.has("apply_patch")) {
1673
+ // This host withheld `edit`/`write` and offers `apply_patch` instead. Native
1674
+ // Cursor write/edit requests are translated into it, so say so rather than
1675
+ // leaving the turn with no file-editing guidance at all.
1676
+ 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.");
1677
+ }
1666
1678
  return [
1667
1679
  `OpenCode exposes exactly these executable tools for this turn: ${[...names].map((name) => `\`${name}\``).join(", ")}.`,
1668
1680
  `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.`,
@@ -1,4 +1,5 @@
1
1
  import { CURSOR_VARIANT_PARAMETERS_KEY, CURSOR_WIRE_MODEL_ID_KEY, parseCursorContextLimit, } from "./models.js";
2
+ import { applyCursorModelCost } from "./pricing.js";
2
3
  /**
3
4
  * Cursor `ModelInfo` → OpenCode model-config mapping.
4
5
  *
@@ -19,9 +20,27 @@ import { CURSOR_VARIANT_PARAMETERS_KEY, CURSOR_WIRE_MODEL_ID_KEY, parseCursorCon
19
20
  * Dots and unicode letters are preserved so names stay readable.
20
21
  * Fixes https://github.com/oakimov/cursor-opencode-provider/issues/2.
21
22
  */
23
+ function stripMarkupTags(value) {
24
+ const chunks = [];
25
+ let cursor = 0;
26
+ while (cursor < value.length) {
27
+ const start = value.indexOf("<", cursor);
28
+ if (start === -1) {
29
+ chunks.push(value.slice(cursor));
30
+ break;
31
+ }
32
+ chunks.push(value.slice(cursor, start));
33
+ const end = value.indexOf(">", start + 1);
34
+ if (end === -1) {
35
+ chunks.push(value.slice(start));
36
+ break;
37
+ }
38
+ cursor = end + 1;
39
+ }
40
+ return chunks.join("");
41
+ }
22
42
  function safeLabel(value) {
23
- return (value
24
- .replace(/<[^>]+>/g, "")
43
+ return (stripMarkupTags(value)
25
44
  .replace(/[()<>&"'`]/g, "")
26
45
  .replace(/\s+/g, " ")
27
46
  .trim() || "default");
@@ -169,12 +188,13 @@ export function modelsToConfig(models) {
169
188
  const baseVariants = variantsForTier(m, "base");
170
189
  const longVariants = variantsForTier(m, "long");
171
190
  if (baseVariants.length > 0 || longVariants.length === 0) {
172
- out[m.id] = modelInfoToConfig(m, { thinkingSuffix, contextTier: "base" });
191
+ out[m.id] = applyCursorModelCost(m.id, modelInfoToConfig(m, { thinkingSuffix, contextTier: "base" }));
173
192
  }
174
193
  if (longVariants.length === 0)
175
194
  continue;
176
195
  if (baseVariants.length === 0) {
177
- out[m.id] = modelInfoToConfig(m, { thinkingSuffix, contextTier: "long" });
196
+ // No separate base tier — the wire id itself is the long-context entry.
197
+ out[m.id] = applyCursorModelCost(`${m.id}-1m`, modelInfoToConfig(m, { thinkingSuffix, contextTier: "long" }));
178
198
  continue;
179
199
  }
180
200
  let longId = `${m.id}-1m`;
@@ -182,7 +202,7 @@ export function modelsToConfig(models) {
182
202
  while (usedIds.has(longId))
183
203
  longId = `${m.id}-1m-${suffix++}`;
184
204
  usedIds.add(longId);
185
- out[longId] = modelInfoToConfig(m, { thinkingSuffix, contextTier: "long" });
205
+ out[longId] = applyCursorModelCost(longId, modelInfoToConfig(m, { thinkingSuffix, contextTier: "long" }));
186
206
  }
187
207
  return out;
188
208
  }
@@ -2,6 +2,7 @@ import { pathToFileURL } from "node:url";
2
2
  import { CURSOR_PROVIDER_ID } from "../shared.js";
3
3
  import { CURSOR_WIRE_MODEL_ID_KEY } from "../models.js";
4
4
  import { modelsToConfig } from "../model-config.js";
5
+ import { toOpenCode2Costs } from "../pricing.js";
5
6
  /**
6
7
  * Catalog registration for the OpenCode 2.0 plugin — the replacement for the
7
8
  * classic plugin's `config` hook.
@@ -69,7 +70,7 @@ function applyModelEntry(draft, id, entry) {
69
70
  model.status = "active";
70
71
  model.enabled = true;
71
72
  model.time = { released: 0 };
72
- model.cost = [];
73
+ model.cost = toOpenCode2Costs(entry.cost);
73
74
  // Carries the default variant parameters (and the wire id) through to
74
75
  // doStream as provider options, matching the classic plugin.
75
76
  if (options)
@@ -53,7 +53,19 @@ export type ModelInfo2 = {
53
53
  time: {
54
54
  released: number;
55
55
  };
56
- cost: readonly unknown[];
56
+ /** OpenCode 2.0 tiered $/1M rates; empty when Cursor does not publish a price. */
57
+ cost: readonly {
58
+ tier?: {
59
+ type: "context";
60
+ size: number;
61
+ };
62
+ input: number;
63
+ output: number;
64
+ cache: {
65
+ read: number;
66
+ write: number;
67
+ };
68
+ }[];
57
69
  status: "alpha" | "beta" | "deprecated" | "active";
58
70
  enabled: boolean;
59
71
  limit: {
@@ -0,0 +1,194 @@
1
+ /**
2
+ * AUTO-GENERATED by scripts/generate-cursor-pricing.ts — do not edit by hand.
3
+ * Source: https://cursor.com/docs/models-and-pricing.md
4
+ * Regenerate: bun run generate:pricing
5
+ */
6
+ export declare const CURSOR_PRICING_SOURCE: "https://cursor.com/docs/models-and-pricing.md";
7
+ export declare const CURSOR_MODEL_COSTS: {
8
+ readonly "claude-fable-5": {
9
+ readonly input: 10;
10
+ readonly output: 50;
11
+ readonly cache_read: 1;
12
+ readonly cache_write: 12.5;
13
+ };
14
+ readonly "claude-haiku-4-5": {
15
+ readonly input: 1;
16
+ readonly output: 5;
17
+ readonly cache_read: 0.1;
18
+ readonly cache_write: 1.25;
19
+ };
20
+ readonly "claude-opus-4-5": {
21
+ readonly input: 5;
22
+ readonly output: 25;
23
+ readonly cache_read: 0.5;
24
+ readonly cache_write: 6.25;
25
+ };
26
+ readonly "claude-opus-4-6": {
27
+ readonly input: 5;
28
+ readonly output: 25;
29
+ readonly cache_read: 0.5;
30
+ readonly cache_write: 6.25;
31
+ };
32
+ readonly "claude-opus-4-7": {
33
+ readonly input: 5;
34
+ readonly output: 25;
35
+ readonly cache_read: 0.5;
36
+ readonly cache_write: 6.25;
37
+ };
38
+ readonly "claude-opus-4-8": {
39
+ readonly input: 5;
40
+ readonly output: 25;
41
+ readonly cache_read: 0.5;
42
+ readonly cache_write: 6.25;
43
+ };
44
+ readonly "claude-opus-5": {
45
+ readonly input: 5;
46
+ readonly output: 25;
47
+ readonly cache_read: 0.5;
48
+ readonly cache_write: 6.25;
49
+ };
50
+ readonly "claude-sonnet-4": {
51
+ readonly input: 3;
52
+ readonly output: 15;
53
+ readonly cache_read: 0.3;
54
+ readonly cache_write: 3.75;
55
+ readonly context_over_200k: {
56
+ readonly input: 6;
57
+ readonly output: 22.5;
58
+ readonly cache_read: 0.6;
59
+ readonly cache_write: 7.5;
60
+ };
61
+ };
62
+ readonly "claude-sonnet-4-5": {
63
+ readonly input: 3;
64
+ readonly output: 15;
65
+ readonly cache_read: 0.3;
66
+ readonly cache_write: 3.75;
67
+ };
68
+ readonly "claude-sonnet-4-6": {
69
+ readonly input: 3;
70
+ readonly output: 15;
71
+ readonly cache_read: 0.3;
72
+ readonly cache_write: 3.75;
73
+ };
74
+ readonly "claude-sonnet-5": {
75
+ readonly input: 3;
76
+ readonly output: 15;
77
+ readonly cache_read: 0.3;
78
+ readonly cache_write: 3.75;
79
+ };
80
+ readonly "gemini-2.5-flash": {
81
+ readonly input: 0.3;
82
+ readonly output: 2.5;
83
+ readonly cache_read: 0.03;
84
+ };
85
+ readonly "gemini-3-flash": {
86
+ readonly input: 0.5;
87
+ readonly output: 3;
88
+ readonly cache_read: 0.05;
89
+ };
90
+ readonly "gemini-3.1-pro": {
91
+ readonly input: 2;
92
+ readonly output: 12;
93
+ readonly cache_read: 0.2;
94
+ };
95
+ readonly "gemini-3.5-flash": {
96
+ readonly input: 1.5;
97
+ readonly output: 9;
98
+ readonly cache_read: 0.15;
99
+ };
100
+ readonly "gemini-3.6-flash": {
101
+ readonly input: 1.5;
102
+ readonly output: 7.5;
103
+ readonly cache_read: 0.15;
104
+ };
105
+ readonly "glm-5.2": {
106
+ readonly input: 1.4;
107
+ readonly output: 4.4;
108
+ readonly cache_read: 0.26;
109
+ };
110
+ readonly "gpt-5-mini": {
111
+ readonly input: 0.25;
112
+ readonly output: 2;
113
+ readonly cache_read: 0.025;
114
+ };
115
+ readonly "gpt-5.1": {
116
+ readonly input: 1.25;
117
+ readonly output: 10;
118
+ readonly cache_read: 0.125;
119
+ };
120
+ readonly "gpt-5.2": {
121
+ readonly input: 1.75;
122
+ readonly output: 14;
123
+ readonly cache_read: 0.175;
124
+ };
125
+ readonly "gpt-5.3-codex": {
126
+ readonly input: 1.75;
127
+ readonly output: 14;
128
+ readonly cache_read: 0.175;
129
+ };
130
+ readonly "gpt-5.4": {
131
+ readonly input: 2.5;
132
+ readonly output: 15;
133
+ readonly cache_read: 0.25;
134
+ readonly context_over_200k: {
135
+ readonly input: 5;
136
+ readonly output: 15;
137
+ readonly cache_read: 0.5;
138
+ };
139
+ };
140
+ readonly "gpt-5.4-mini": {
141
+ readonly input: 0.75;
142
+ readonly output: 4.5;
143
+ readonly cache_read: 0.075;
144
+ };
145
+ readonly "gpt-5.4-nano": {
146
+ readonly input: 0.2;
147
+ readonly output: 1.25;
148
+ readonly cache_read: 0.02;
149
+ };
150
+ readonly "gpt-5.5": {
151
+ readonly input: 5;
152
+ readonly output: 30;
153
+ readonly cache_read: 0.5;
154
+ readonly context_over_200k: {
155
+ readonly input: 10;
156
+ readonly output: 30;
157
+ readonly cache_read: 1;
158
+ };
159
+ };
160
+ readonly "gpt-5.6-luna": {
161
+ readonly input: 0.2;
162
+ readonly output: 1.2;
163
+ readonly cache_read: 0.02;
164
+ readonly cache_write: 0.25;
165
+ };
166
+ readonly "gpt-5.6-sol": {
167
+ readonly input: 5;
168
+ readonly output: 30;
169
+ readonly cache_read: 0.5;
170
+ readonly cache_write: 6.25;
171
+ readonly context_over_200k: {
172
+ readonly input: 10;
173
+ readonly output: 30;
174
+ readonly cache_read: 1;
175
+ readonly cache_write: 12.5;
176
+ };
177
+ };
178
+ readonly "gpt-5.6-terra": {
179
+ readonly input: 2;
180
+ readonly output: 12;
181
+ readonly cache_read: 0.2;
182
+ readonly cache_write: 2.5;
183
+ };
184
+ readonly "kimi-k2.7-code": {
185
+ readonly input: 0.95;
186
+ readonly output: 4;
187
+ readonly cache_read: 0.19;
188
+ };
189
+ readonly "kimi-k3": {
190
+ readonly input: 3;
191
+ readonly output: 15;
192
+ readonly cache_read: 0.3;
193
+ };
194
+ };
@@ -0,0 +1,194 @@
1
+ /**
2
+ * AUTO-GENERATED by scripts/generate-cursor-pricing.ts — do not edit by hand.
3
+ * Source: https://cursor.com/docs/models-and-pricing.md
4
+ * Regenerate: bun run generate:pricing
5
+ */
6
+ export const CURSOR_PRICING_SOURCE = "https://cursor.com/docs/models-and-pricing.md";
7
+ export const CURSOR_MODEL_COSTS = {
8
+ "claude-fable-5": {
9
+ "input": 10,
10
+ "output": 50,
11
+ "cache_read": 1,
12
+ "cache_write": 12.5
13
+ },
14
+ "claude-haiku-4-5": {
15
+ "input": 1,
16
+ "output": 5,
17
+ "cache_read": 0.1,
18
+ "cache_write": 1.25
19
+ },
20
+ "claude-opus-4-5": {
21
+ "input": 5,
22
+ "output": 25,
23
+ "cache_read": 0.5,
24
+ "cache_write": 6.25
25
+ },
26
+ "claude-opus-4-6": {
27
+ "input": 5,
28
+ "output": 25,
29
+ "cache_read": 0.5,
30
+ "cache_write": 6.25
31
+ },
32
+ "claude-opus-4-7": {
33
+ "input": 5,
34
+ "output": 25,
35
+ "cache_read": 0.5,
36
+ "cache_write": 6.25
37
+ },
38
+ "claude-opus-4-8": {
39
+ "input": 5,
40
+ "output": 25,
41
+ "cache_read": 0.5,
42
+ "cache_write": 6.25
43
+ },
44
+ "claude-opus-5": {
45
+ "input": 5,
46
+ "output": 25,
47
+ "cache_read": 0.5,
48
+ "cache_write": 6.25
49
+ },
50
+ "claude-sonnet-4": {
51
+ "input": 3,
52
+ "output": 15,
53
+ "cache_read": 0.3,
54
+ "cache_write": 3.75,
55
+ "context_over_200k": {
56
+ "input": 6,
57
+ "output": 22.5,
58
+ "cache_read": 0.6,
59
+ "cache_write": 7.5
60
+ }
61
+ },
62
+ "claude-sonnet-4-5": {
63
+ "input": 3,
64
+ "output": 15,
65
+ "cache_read": 0.3,
66
+ "cache_write": 3.75
67
+ },
68
+ "claude-sonnet-4-6": {
69
+ "input": 3,
70
+ "output": 15,
71
+ "cache_read": 0.3,
72
+ "cache_write": 3.75
73
+ },
74
+ "claude-sonnet-5": {
75
+ "input": 3,
76
+ "output": 15,
77
+ "cache_read": 0.3,
78
+ "cache_write": 3.75
79
+ },
80
+ "gemini-2.5-flash": {
81
+ "input": 0.3,
82
+ "output": 2.5,
83
+ "cache_read": 0.03
84
+ },
85
+ "gemini-3-flash": {
86
+ "input": 0.5,
87
+ "output": 3,
88
+ "cache_read": 0.05
89
+ },
90
+ "gemini-3.1-pro": {
91
+ "input": 2,
92
+ "output": 12,
93
+ "cache_read": 0.2
94
+ },
95
+ "gemini-3.5-flash": {
96
+ "input": 1.5,
97
+ "output": 9,
98
+ "cache_read": 0.15
99
+ },
100
+ "gemini-3.6-flash": {
101
+ "input": 1.5,
102
+ "output": 7.5,
103
+ "cache_read": 0.15
104
+ },
105
+ "glm-5.2": {
106
+ "input": 1.4,
107
+ "output": 4.4,
108
+ "cache_read": 0.26
109
+ },
110
+ "gpt-5-mini": {
111
+ "input": 0.25,
112
+ "output": 2,
113
+ "cache_read": 0.025
114
+ },
115
+ "gpt-5.1": {
116
+ "input": 1.25,
117
+ "output": 10,
118
+ "cache_read": 0.125
119
+ },
120
+ "gpt-5.2": {
121
+ "input": 1.75,
122
+ "output": 14,
123
+ "cache_read": 0.175
124
+ },
125
+ "gpt-5.3-codex": {
126
+ "input": 1.75,
127
+ "output": 14,
128
+ "cache_read": 0.175
129
+ },
130
+ "gpt-5.4": {
131
+ "input": 2.5,
132
+ "output": 15,
133
+ "cache_read": 0.25,
134
+ "context_over_200k": {
135
+ "input": 5,
136
+ "output": 15,
137
+ "cache_read": 0.5
138
+ }
139
+ },
140
+ "gpt-5.4-mini": {
141
+ "input": 0.75,
142
+ "output": 4.5,
143
+ "cache_read": 0.075
144
+ },
145
+ "gpt-5.4-nano": {
146
+ "input": 0.2,
147
+ "output": 1.25,
148
+ "cache_read": 0.02
149
+ },
150
+ "gpt-5.5": {
151
+ "input": 5,
152
+ "output": 30,
153
+ "cache_read": 0.5,
154
+ "context_over_200k": {
155
+ "input": 10,
156
+ "output": 30,
157
+ "cache_read": 1
158
+ }
159
+ },
160
+ "gpt-5.6-luna": {
161
+ "input": 0.2,
162
+ "output": 1.2,
163
+ "cache_read": 0.02,
164
+ "cache_write": 0.25
165
+ },
166
+ "gpt-5.6-sol": {
167
+ "input": 5,
168
+ "output": 30,
169
+ "cache_read": 0.5,
170
+ "cache_write": 6.25,
171
+ "context_over_200k": {
172
+ "input": 10,
173
+ "output": 30,
174
+ "cache_read": 1,
175
+ "cache_write": 12.5
176
+ }
177
+ },
178
+ "gpt-5.6-terra": {
179
+ "input": 2,
180
+ "output": 12,
181
+ "cache_read": 0.2,
182
+ "cache_write": 2.5
183
+ },
184
+ "kimi-k2.7-code": {
185
+ "input": 0.95,
186
+ "output": 4,
187
+ "cache_read": 0.19
188
+ },
189
+ "kimi-k3": {
190
+ "input": 3,
191
+ "output": 15,
192
+ "cache_read": 0.3
193
+ }
194
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Cursor model token pricing for OpenCode cost reporting.
3
+ *
4
+ * Rates come from Cursor's public docs (Other Models pool = provider API price).
5
+ * Generated data lives in `pricing-data.ts`; regenerate with
6
+ * `bun run generate:pricing`. Unknown / unpublished models get no `cost`.
7
+ */
8
+ /** Classic OpenCode / models.dev cost shape used in provider config. */
9
+ export type OpenCodeModelCost = {
10
+ input: number;
11
+ output: number;
12
+ cache_read?: number;
13
+ cache_write?: number;
14
+ context_over_200k?: OpenCodeModelCost;
15
+ };
16
+ /** OpenCode 2.0 `Model.Info.cost` entry (array of tiered rates). */
17
+ export type OpenCode2ModelCost = {
18
+ tier?: {
19
+ type: "context";
20
+ size: number;
21
+ };
22
+ input: number;
23
+ output: number;
24
+ cache: {
25
+ read: number;
26
+ write: number;
27
+ };
28
+ };
29
+ export type CursorPricingCoverage = {
30
+ priced: string[];
31
+ missing: string[];
32
+ };
33
+ /**
34
+ * Models we expose that Cursor does not currently publish numeric rates for
35
+ * on the Other Models table (Auto / first-party Cursor Models pool).
36
+ */
37
+ export declare const CURSOR_UNPRICED_MODEL_IDS: readonly ["default", "composer-2.5", "grok-4.5"];
38
+ export declare function validateOpenCodeModelCost(value: unknown, path?: string): {
39
+ valid: boolean;
40
+ errors: string[];
41
+ };
42
+ export declare function isOpenCodeModelCost(value: unknown): value is OpenCodeModelCost;
43
+ /** Strip synthetic OpenCode long-context suffix (`…-1m`, `…-1m-2`, …). */
44
+ export declare function wireModelIdForPricing(modelId: string): {
45
+ baseId: string;
46
+ longContextEntry: boolean;
47
+ };
48
+ /**
49
+ * Look up classic OpenCode cost for a catalog / wire model id.
50
+ * Synthetic `-1m` entries retain both base and documented long-context rates.
51
+ */
52
+ export declare function getCursorModelCost(modelId: string): OpenCodeModelCost | undefined;
53
+ export declare function applyCursorModelCost<T extends Record<string, unknown>>(modelId: string, entry: T): T & {
54
+ cost?: OpenCodeModelCost;
55
+ };
56
+ /** Convert classic cost → OpenCode 2.0 `Model.Info.cost` array. */
57
+ export declare function toOpenCode2Costs(cost: OpenCodeModelCost | undefined): OpenCode2ModelCost[];
58
+ /**
59
+ * Coverage helper for CI / maintainer checks.
60
+ * Intentionally unpriced first-party ids are counted as priced (covered).
61
+ */
62
+ export declare function checkCursorPricingCoverage(modelIds: string[]): CursorPricingCoverage;