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/auth-renewal.js
CHANGED
|
@@ -4,6 +4,7 @@ import { AuthExchangeError, decodeJwtPayload, exchangeApiKey, isExchangeableApiK
|
|
|
4
4
|
// OpenCode's SessionRetry keys off such words in provider error messages.
|
|
5
5
|
import { CursorAuthError, CursorServerError, sanitizeHostTerminalMessage } from "./errors.js";
|
|
6
6
|
import { errorMessage, trace } from "./debug.js";
|
|
7
|
+
import { processShared } from "./process-shared.js";
|
|
7
8
|
// Credential renewal policy. The two Cursor credential kinds are renewed by
|
|
8
9
|
// independent paths and never stand in for each other:
|
|
9
10
|
//
|
|
@@ -76,6 +77,12 @@ function isoTime(ms) {
|
|
|
76
77
|
function backoffMs(failures) {
|
|
77
78
|
return Math.min(BACKOFF_MAX_MS, BACKOFF_INITIAL_MS * 2 ** Math.max(0, failures - 1));
|
|
78
79
|
}
|
|
80
|
+
function terminalError(failure) {
|
|
81
|
+
return new CursorAuthError(failure.message, {
|
|
82
|
+
code: failure.code,
|
|
83
|
+
...(failure.statusCode !== undefined ? { statusCode: failure.statusCode } : {}),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
79
86
|
function stateFor(map, key) {
|
|
80
87
|
let state = map.get(key);
|
|
81
88
|
if (!state) {
|
|
@@ -94,27 +101,34 @@ function recordSuccess(state) {
|
|
|
94
101
|
state.retryAt = undefined;
|
|
95
102
|
state.lastFailure = undefined;
|
|
96
103
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
104
|
+
// Per credential, not per module copy: every location OpenCode 2 serves loads
|
|
105
|
+
// its own copy, and each would otherwise renew the same login separately.
|
|
106
|
+
const sessionStates = processShared("auth-renewal.session-states.v1", () => new Map());
|
|
107
|
+
/** API base + stored refresh token → newer session token obtained there. */
|
|
108
|
+
const sessionSuccessors = processShared("auth-renewal.session-successors.v2", () => new Map());
|
|
100
109
|
/**
|
|
101
110
|
* Follow renewals this process already made, so a caller still holding an
|
|
102
111
|
* older stored credential (persisting failed, or a host that persists only
|
|
103
112
|
* later) gets the newest token instead of renewing again.
|
|
104
113
|
*/
|
|
105
|
-
export function latestSessionTokens(tokens) {
|
|
114
|
+
export function latestSessionTokens(tokens, baseUrl = API_BASE) {
|
|
106
115
|
let current = tokens;
|
|
107
116
|
for (let hops = 0; hops < MAX_SESSION_SUCCESSORS; hops++) {
|
|
108
|
-
const next = sessionSuccessors.get(current.refreshToken);
|
|
117
|
+
const next = sessionSuccessors.get(`${baseUrl}\0${current.refreshToken}`);
|
|
109
118
|
if (!next || next === current.refreshToken)
|
|
110
119
|
break;
|
|
111
120
|
current = { accessToken: next, refreshToken: next };
|
|
112
121
|
}
|
|
122
|
+
// A verified successor replaces the token it renewed even when issued in
|
|
123
|
+
// the same expiry second. A separately stored newer access token still wins.
|
|
124
|
+
if (tokens.accessToken === tokens.refreshToken)
|
|
125
|
+
return current;
|
|
113
126
|
return longerLived(tokens.accessToken, current.accessToken) === current.accessToken ? current : tokens;
|
|
114
127
|
}
|
|
115
|
-
function rememberSuccessor(refreshToken, accessToken) {
|
|
116
|
-
|
|
117
|
-
sessionSuccessors.
|
|
128
|
+
function rememberSuccessor(baseUrl, refreshToken, accessToken) {
|
|
129
|
+
const key = `${baseUrl}\0${refreshToken}`;
|
|
130
|
+
sessionSuccessors.delete(key);
|
|
131
|
+
sessionSuccessors.set(key, accessToken);
|
|
118
132
|
while (sessionSuccessors.size > MAX_SESSION_SUCCESSORS) {
|
|
119
133
|
const oldest = sessionSuccessors.keys().next().value;
|
|
120
134
|
if (oldest === undefined)
|
|
@@ -134,22 +148,24 @@ export function isSessionRenewalDue(token, now = Date.now()) {
|
|
|
134
148
|
*/
|
|
135
149
|
export async function renewSessionIfDue(tokens, options = {}) {
|
|
136
150
|
const baseUrl = options.baseUrl ?? API_BASE;
|
|
137
|
-
const base = latestSessionTokens(tokens);
|
|
151
|
+
const base = latestSessionTokens(tokens, baseUrl);
|
|
138
152
|
const fromMemory = base.accessToken !== tokens.accessToken;
|
|
139
|
-
|
|
153
|
+
const state = stateFor(sessionStates, `${baseUrl}\0${base.refreshToken}`);
|
|
154
|
+
if (state.terminal)
|
|
155
|
+
throw terminalError(state.terminal);
|
|
156
|
+
if (!options.force && !isSessionRenewalDue(base.accessToken) && !state.inflight) {
|
|
140
157
|
return { accessToken: base.accessToken, renewed: fromMemory };
|
|
141
158
|
}
|
|
142
|
-
|
|
143
|
-
if (!state.terminal && (options.force || state.retryAt === undefined || Date.now() >= state.retryAt)) {
|
|
159
|
+
if (state.inflight || options.force || state.retryAt === undefined || Date.now() >= state.retryAt) {
|
|
144
160
|
state.inflight ??= (async () => {
|
|
145
161
|
const result = await refreshCursorSession(base.refreshToken, baseUrl);
|
|
146
162
|
if (result.ok) {
|
|
147
163
|
const { expiresAtMs } = tokenTimes(result.accessToken);
|
|
148
|
-
if (expiresAtMs === undefined) {
|
|
149
|
-
recordTransientFailure(state, "session refresh returned
|
|
164
|
+
if (expiresAtMs === undefined || !isUsable(result.accessToken, Date.now())) {
|
|
165
|
+
recordTransientFailure(state, "session refresh returned an unusable token");
|
|
150
166
|
}
|
|
151
167
|
else {
|
|
152
|
-
rememberSuccessor(base.refreshToken, result.accessToken);
|
|
168
|
+
rememberSuccessor(baseUrl, base.refreshToken, result.accessToken);
|
|
153
169
|
recordSuccess(state);
|
|
154
170
|
trace(`auth: session renewed exp=${isoTime(expiresAtMs)}`);
|
|
155
171
|
return;
|
|
@@ -159,9 +175,13 @@ export async function renewSessionIfDue(tokens, options = {}) {
|
|
|
159
175
|
recordTransientFailure(state, result.message);
|
|
160
176
|
}
|
|
161
177
|
else {
|
|
162
|
-
state.terminal =
|
|
163
|
-
|
|
164
|
-
|
|
178
|
+
state.terminal = {
|
|
179
|
+
message: result.kind === "policy"
|
|
180
|
+
? `Cursor sign-in policy blocks this login (sign_in_policy_violation); ${SIGN_IN_AGAIN} with an allowed account`
|
|
181
|
+
: `Cursor ended this login session; ${SIGN_IN_AGAIN}`,
|
|
182
|
+
code: result.kind === "policy" ? "sign_in_policy_violation" : "session_logout",
|
|
183
|
+
...(result.status !== undefined ? { statusCode: result.status } : {}),
|
|
184
|
+
};
|
|
165
185
|
trace(`auth: session renewal final failure kind=${result.kind}`);
|
|
166
186
|
return;
|
|
167
187
|
}
|
|
@@ -172,12 +192,12 @@ export async function renewSessionIfDue(tokens, options = {}) {
|
|
|
172
192
|
});
|
|
173
193
|
await state.inflight;
|
|
174
194
|
}
|
|
175
|
-
const renewed = latestSessionTokens(base);
|
|
195
|
+
const renewed = latestSessionTokens(base, baseUrl);
|
|
176
196
|
if (renewed.accessToken !== base.accessToken) {
|
|
177
197
|
return { accessToken: renewed.accessToken, renewed: true };
|
|
178
198
|
}
|
|
179
199
|
if (state.terminal)
|
|
180
|
-
throw state.terminal;
|
|
200
|
+
throw terminalError(state.terminal);
|
|
181
201
|
if (!isUsable(base.accessToken, Date.now())) {
|
|
182
202
|
throw new CursorAuthError(`Cursor login expired and could not be renewed (${sanitizeHostTerminalMessage(state.lastFailure ?? "no renewal attempt allowed yet")}); ${SIGN_IN_AGAIN}`, { code: "session_expired" });
|
|
183
203
|
}
|
|
@@ -188,7 +208,7 @@ export async function renewSessionIfDue(tokens, options = {}) {
|
|
|
188
208
|
};
|
|
189
209
|
}
|
|
190
210
|
// ── API key ──
|
|
191
|
-
const apiKeyStates = new Map();
|
|
211
|
+
const apiKeyStates = processShared("auth-renewal.api-key-states.v1", () => new Map());
|
|
192
212
|
/**
|
|
193
213
|
* JWT for a raw `crsr_` API key: the cached or `seed` JWT while it is not
|
|
194
214
|
* within five minutes of expiry, otherwise a fresh exchange. Cached per API
|
|
@@ -213,7 +233,15 @@ export async function resolveApiKeyToken(apiKey, options = {}) {
|
|
|
213
233
|
state.inflight ??= (async () => {
|
|
214
234
|
try {
|
|
215
235
|
const pair = await exchangeApiKey(apiKey, baseUrl);
|
|
216
|
-
|
|
236
|
+
if (isExchangeableApiKey(pair.accessToken) || tokenTimes(pair.accessToken).expiresAtMs === undefined ||
|
|
237
|
+
!isUsable(pair.accessToken, Date.now())) {
|
|
238
|
+
throw new AuthExchangeError("API key exchange returned an unusable token");
|
|
239
|
+
}
|
|
240
|
+
// A forced exchange follows a server rejection: keeping the rejected
|
|
241
|
+
// JWT on an expiry tie would retry the request with that same token.
|
|
242
|
+
state.token = options.force || !state.token
|
|
243
|
+
? pair.accessToken
|
|
244
|
+
: longerLived(state.token, pair.accessToken);
|
|
217
245
|
recordSuccess(state);
|
|
218
246
|
trace(`auth: API key exchanged exp=${isoTime(tokenTimes(pair.accessToken).expiresAtMs)}`);
|
|
219
247
|
}
|
|
@@ -225,9 +253,13 @@ export async function resolveApiKeyToken(apiKey, options = {}) {
|
|
|
225
253
|
}
|
|
226
254
|
else {
|
|
227
255
|
const status = error instanceof AuthExchangeError ? error.status : undefined;
|
|
228
|
-
state.terminal =
|
|
229
|
-
|
|
230
|
-
|
|
256
|
+
state.terminal = {
|
|
257
|
+
message: kind === "policy"
|
|
258
|
+
? "Cursor sign-in policy blocks this API key (sign_in_policy_violation); use an allowed account"
|
|
259
|
+
: `Cursor rejected the API key${status ? ` (HTTP ${status})` : ""}; create a new key and sign in with it`,
|
|
260
|
+
code: kind === "policy" ? "sign_in_policy_violation" : "api_key_rejected",
|
|
261
|
+
...(status ? { statusCode: status } : {}),
|
|
262
|
+
};
|
|
231
263
|
trace(`auth: API key exchange final failure kind=${kind} status=${status ?? "none"}`);
|
|
232
264
|
}
|
|
233
265
|
}
|
|
@@ -240,7 +272,7 @@ export async function resolveApiKeyToken(apiKey, options = {}) {
|
|
|
240
272
|
if (state.token && isUsable(state.token, Date.now()))
|
|
241
273
|
return result(state.token);
|
|
242
274
|
if (state.terminal)
|
|
243
|
-
throw state.terminal;
|
|
275
|
+
throw terminalError(state.terminal);
|
|
244
276
|
throw new CursorServerError(`Cursor API key exchange failed (${sanitizeHostTerminalMessage(state.lastFailure ?? "waiting before the next attempt")}); it is retried automatically`, { transient: true, replaySafe: true });
|
|
245
277
|
}
|
|
246
278
|
/**
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What OpenCode is told a Cursor turn costs at catalog rates.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode prices every step finish and adds it to the session: OpenCode 1.x
|
|
5
|
+
* from `providerMetadata.copilot.totalNanoAiu` when present
|
|
6
|
+
* (`session/session.ts` getUsage), OpenCode 2 always from the step's tokens at
|
|
7
|
+
* the model's catalog rates. Each step's token total must also stay equal to
|
|
8
|
+
* Cursor's context occupancy, because both hosts show context use and decide
|
|
9
|
+
* compaction from the latest step's tokens. Cursor reports token counters only
|
|
10
|
+
* once, in `TurnEnded`, aggregated over every model call of its Run.
|
|
11
|
+
*
|
|
12
|
+
* So each tool step is billed an estimate shaped like one model call (the
|
|
13
|
+
* context carried over from the previous step read from cache, the rest new
|
|
14
|
+
* input), and the turn's last step settles the difference to its catalog-priced
|
|
15
|
+
* cost: its input/cache split is chosen so the token-priced cost approaches the
|
|
16
|
+
* outstanding amount, and the same amount goes into `totalNanoAiu`. Both hosts
|
|
17
|
+
* then bill identical amounts at the unmodified catalog rates. What a final
|
|
18
|
+
* step cannot carry stays outstanding for a later priced turn-end. Settlement
|
|
19
|
+
* is bounded by minimum and maximum token prices and whole-token rounding;
|
|
20
|
+
* negative balances cannot refund prior host charges.
|
|
21
|
+
*/
|
|
22
|
+
import type { LanguageModelV3Usage } from "@ai-sdk/provider";
|
|
23
|
+
import { type OpenCodeModelCost } from "./pricing.js";
|
|
24
|
+
import { type CursorUsageCounters } from "./usage.js";
|
|
25
|
+
/** USD per token for each category a host prices. */
|
|
26
|
+
export type StepPrices = {
|
|
27
|
+
input: number;
|
|
28
|
+
output: number;
|
|
29
|
+
cacheRead: number;
|
|
30
|
+
cacheWrite: number;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Price entry the catalog gave this model: `<wire>-fast` for a Fast variant
|
|
34
|
+
* with its own published rate, else the wire id (a long-context entry is
|
|
35
|
+
* priced from the base entry and its over-200k tier), as `model-config.ts` does.
|
|
36
|
+
*/
|
|
37
|
+
export declare function cursorPricingId(wireModelId: string, parameterValues: ReadonlyArray<{
|
|
38
|
+
id: string;
|
|
39
|
+
value: string;
|
|
40
|
+
}>): string;
|
|
41
|
+
/** The rates a host applies to a step with `inputSideTokens` of prompt (its context tier). */
|
|
42
|
+
export declare function hostStepPrices(cost: OpenCodeModelCost | undefined, inputSideTokens: number): StepPrices | undefined;
|
|
43
|
+
/** What a host charges for `usage` at `prices`. */
|
|
44
|
+
export declare function hostUsageCostUsd(prices: StepPrices, usage: LanguageModelV3Usage): number;
|
|
45
|
+
/**
|
|
46
|
+
* Catalog-priced Run cost from aggregate `TurnEnded` counters. The final
|
|
47
|
+
* context approximates the prompt tier, bounded by total input so output alone
|
|
48
|
+
* cannot select a higher tier. Per-call tiers are unavailable: a Run crossing
|
|
49
|
+
* tiers or compacting internally cannot be priced exactly from these totals.
|
|
50
|
+
* A cache category without its own published rate is priced as input.
|
|
51
|
+
*/
|
|
52
|
+
export declare function turnEndedCostUsd(cost: OpenCodeModelCost | undefined, counters: CursorUsageCounters, contextTokens: number): number | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Occupancy-shaped usage for one step: `usedTokens` in total (`output = 1`, so
|
|
55
|
+
* hosts accept it as the context snapshot), `prefixTokens` of it read from
|
|
56
|
+
* cache and the rest new input. With `targetUsd`, the split moves between
|
|
57
|
+
* input and cache read (and cache write only when those two cannot reach it)
|
|
58
|
+
* so the host-priced cost meets the target as closely as the total allows.
|
|
59
|
+
*/
|
|
60
|
+
export declare function shapeStepUsage(input: {
|
|
61
|
+
usedTokens: number;
|
|
62
|
+
prefixTokens: number;
|
|
63
|
+
prices: StepPrices | undefined;
|
|
64
|
+
targetUsd?: number;
|
|
65
|
+
}): {
|
|
66
|
+
usage: LanguageModelV3Usage;
|
|
67
|
+
costUsd: number;
|
|
68
|
+
};
|
|
69
|
+
/** `providerMetadata.copilot.totalNanoAiu` for a step that costs `usd`. */
|
|
70
|
+
export declare function nanoAiuForUsd(usd: number): number;
|
|
71
|
+
/** Estimates belong to one Run and are replaced only by that Run's counters. */
|
|
72
|
+
export type RunCostEstimate = {
|
|
73
|
+
estimatedUsd?: number;
|
|
74
|
+
ledgerEpoch?: object;
|
|
75
|
+
};
|
|
76
|
+
type LedgerTotals = {
|
|
77
|
+
realUsd: number;
|
|
78
|
+
billedUsd: number;
|
|
79
|
+
};
|
|
80
|
+
type LedgerEntry = LedgerTotals & {
|
|
81
|
+
estimatedUsd: number;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Per OpenCode session: catalog-priced cost (from `TurnEnded`) and what
|
|
85
|
+
* this provider has reported to the host. Priced turn-ends reconcile the balance.
|
|
86
|
+
*/
|
|
87
|
+
export declare class BillingLedger {
|
|
88
|
+
private readonly entries;
|
|
89
|
+
constructor(entries?: Map<string, LedgerEntry>);
|
|
90
|
+
private entry;
|
|
91
|
+
recordReal(key: string, usd: number, run?: RunCostEstimate): void;
|
|
92
|
+
recordBilled(key: string, usd: number, run?: RunCostEstimate): void;
|
|
93
|
+
/** Confirmed plus unconfirmed cost minus the amount reported to the host. */
|
|
94
|
+
outstanding(key: string): number;
|
|
95
|
+
estimated(key: string): number;
|
|
96
|
+
totals(key: string): LedgerTotals;
|
|
97
|
+
clear(): void;
|
|
98
|
+
}
|
|
99
|
+
export declare const billingLedger: BillingLedger;
|
|
100
|
+
/** The catalog cost entry for this Run's model, if Cursor publishes one. */
|
|
101
|
+
export declare function cursorRunCost(wireModelId: string, parameterValues: ReadonlyArray<{
|
|
102
|
+
id: string;
|
|
103
|
+
value: string;
|
|
104
|
+
}>): OpenCodeModelCost | undefined;
|
|
105
|
+
export {};
|
package/dist/billing.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { getCursorModelCost, hasCursorFastPricing } from "./pricing.js";
|
|
2
|
+
import { buildLanguageModelV3UsageFromCounters } from "./usage.js";
|
|
3
|
+
const PER_MILLION = 1_000_000;
|
|
4
|
+
const CONTEXT_TIER_TOKENS = 200_000;
|
|
5
|
+
/** OpenCode 1.x divides `totalNanoAiu` by 1e11 to get USD. */
|
|
6
|
+
const NANO_AIU_PER_USD = 100_000_000_000;
|
|
7
|
+
/**
|
|
8
|
+
* Price entry the catalog gave this model: `<wire>-fast` for a Fast variant
|
|
9
|
+
* with its own published rate, else the wire id (a long-context entry is
|
|
10
|
+
* priced from the base entry and its over-200k tier), as `model-config.ts` does.
|
|
11
|
+
*/
|
|
12
|
+
export function cursorPricingId(wireModelId, parameterValues) {
|
|
13
|
+
const fast = parameterValues.some((parameter) => parameter.id === "fast" && parameter.value === "true");
|
|
14
|
+
return fast && hasCursorFastPricing(wireModelId) ? `${wireModelId}-fast` : wireModelId;
|
|
15
|
+
}
|
|
16
|
+
/** The rates a host applies to a step with `inputSideTokens` of prompt (its context tier). */
|
|
17
|
+
export function hostStepPrices(cost, inputSideTokens) {
|
|
18
|
+
if (!cost)
|
|
19
|
+
return undefined;
|
|
20
|
+
const tier = cost.context_over_200k && inputSideTokens > CONTEXT_TIER_TOKENS ? cost.context_over_200k : cost;
|
|
21
|
+
return {
|
|
22
|
+
input: tier.input / PER_MILLION,
|
|
23
|
+
output: tier.output / PER_MILLION,
|
|
24
|
+
// Both hosts charge an unpriced cache category nothing.
|
|
25
|
+
cacheRead: (tier.cache_read ?? 0) / PER_MILLION,
|
|
26
|
+
cacheWrite: (tier.cache_write ?? 0) / PER_MILLION,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** What a host charges for `usage` at `prices`. */
|
|
30
|
+
export function hostUsageCostUsd(prices, usage) {
|
|
31
|
+
const input = usage.inputTokens;
|
|
32
|
+
return (input.noCache ?? 0) * prices.input
|
|
33
|
+
+ (input.cacheRead ?? 0) * prices.cacheRead
|
|
34
|
+
+ (input.cacheWrite ?? 0) * prices.cacheWrite
|
|
35
|
+
+ (usage.outputTokens.total ?? 0) * prices.output;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Catalog-priced Run cost from aggregate `TurnEnded` counters. The final
|
|
39
|
+
* context approximates the prompt tier, bounded by total input so output alone
|
|
40
|
+
* cannot select a higher tier. Per-call tiers are unavailable: a Run crossing
|
|
41
|
+
* tiers or compacting internally cannot be priced exactly from these totals.
|
|
42
|
+
* A cache category without its own published rate is priced as input.
|
|
43
|
+
*/
|
|
44
|
+
export function turnEndedCostUsd(cost, counters, contextTokens) {
|
|
45
|
+
if (!cost)
|
|
46
|
+
return undefined;
|
|
47
|
+
// Reuse the canonical partition guard; inconsistent cache counters must not
|
|
48
|
+
// create billable input beyond input_tokens. Keep the raw fields diagnostic.
|
|
49
|
+
const usage = buildLanguageModelV3UsageFromCounters(counters);
|
|
50
|
+
const input = usage.inputTokens;
|
|
51
|
+
const promptTokens = Math.min(input.total ?? 0, Math.max(0, contextTokens));
|
|
52
|
+
const tier = cost.context_over_200k && promptTokens > CONTEXT_TIER_TOKENS ? cost.context_over_200k : cost;
|
|
53
|
+
return ((input.noCache ?? 0) * tier.input
|
|
54
|
+
+ (input.cacheRead ?? 0) * (tier.cache_read ?? tier.input)
|
|
55
|
+
+ (input.cacheWrite ?? 0) * (tier.cache_write ?? tier.input)
|
|
56
|
+
+ (usage.outputTokens.total ?? 0) * tier.output) / PER_MILLION;
|
|
57
|
+
}
|
|
58
|
+
function usageOf(split) {
|
|
59
|
+
return {
|
|
60
|
+
inputTokens: {
|
|
61
|
+
total: split.noCache + split.cacheRead + split.cacheWrite,
|
|
62
|
+
noCache: split.noCache,
|
|
63
|
+
cacheRead: split.cacheRead,
|
|
64
|
+
cacheWrite: split.cacheWrite,
|
|
65
|
+
},
|
|
66
|
+
outputTokens: { total: 1, text: 1, reasoning: 0 },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Split `inputTokens` between two categories so their cost is as close to
|
|
71
|
+
* `budget` as whole tokens allow, clamped to what the pair can carry.
|
|
72
|
+
*/
|
|
73
|
+
function mix(inputTokens, budget, low, high) {
|
|
74
|
+
const split = { noCache: 0, cacheRead: 0, cacheWrite: 0 };
|
|
75
|
+
const span = high.price - low.price;
|
|
76
|
+
const atHigh = span > 0
|
|
77
|
+
? Math.min(inputTokens, Math.max(0, Math.round((budget - inputTokens * low.price) / span)))
|
|
78
|
+
: 0;
|
|
79
|
+
split[high.category] += atHigh;
|
|
80
|
+
split[low.category] += inputTokens - atHigh;
|
|
81
|
+
return split;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Occupancy-shaped usage for one step: `usedTokens` in total (`output = 1`, so
|
|
85
|
+
* hosts accept it as the context snapshot), `prefixTokens` of it read from
|
|
86
|
+
* cache and the rest new input. With `targetUsd`, the split moves between
|
|
87
|
+
* input and cache read (and cache write only when those two cannot reach it)
|
|
88
|
+
* so the host-priced cost meets the target as closely as the total allows.
|
|
89
|
+
*/
|
|
90
|
+
export function shapeStepUsage(input) {
|
|
91
|
+
const used = Math.max(0, Math.trunc(input.usedTokens));
|
|
92
|
+
if (used <= 0) {
|
|
93
|
+
return {
|
|
94
|
+
usage: {
|
|
95
|
+
inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
|
|
96
|
+
outputTokens: { total: 0, text: 0, reasoning: 0 },
|
|
97
|
+
},
|
|
98
|
+
costUsd: 0,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const inputTokens = used - 1;
|
|
102
|
+
const cacheRead = Math.min(inputTokens, Math.max(0, Math.trunc(input.prefixTokens)));
|
|
103
|
+
let split = { noCache: inputTokens - cacheRead, cacheRead, cacheWrite: 0 };
|
|
104
|
+
const prices = input.prices;
|
|
105
|
+
if (prices && input.targetUsd !== undefined) {
|
|
106
|
+
const budget = input.targetUsd - prices.output;
|
|
107
|
+
const options = [
|
|
108
|
+
{ category: "noCache", price: prices.input },
|
|
109
|
+
{ category: "cacheRead", price: prices.cacheRead },
|
|
110
|
+
];
|
|
111
|
+
const [low, high] = [...options].sort((a, b) => a.price - b.price);
|
|
112
|
+
if (budget >= inputTokens * low.price && budget <= inputTokens * high.price) {
|
|
113
|
+
split = mix(inputTokens, budget, low, high);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const write = { category: "cacheWrite", price: prices.cacheWrite };
|
|
117
|
+
const all = [...options, write].sort((a, b) => a.price - b.price);
|
|
118
|
+
const cheapest = all[0];
|
|
119
|
+
const dearest = all[all.length - 1];
|
|
120
|
+
split = budget < inputTokens * low.price
|
|
121
|
+
? mix(inputTokens, budget, cheapest, low)
|
|
122
|
+
: mix(inputTokens, budget, high, dearest);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const usage = usageOf(split);
|
|
126
|
+
return { usage, costUsd: prices ? hostUsageCostUsd(prices, usage) : 0 };
|
|
127
|
+
}
|
|
128
|
+
/** `providerMetadata.copilot.totalNanoAiu` for a step that costs `usd`. */
|
|
129
|
+
export function nanoAiuForUsd(usd) {
|
|
130
|
+
return Math.max(0, Math.round(usd * NANO_AIU_PER_USD));
|
|
131
|
+
}
|
|
132
|
+
const MAX_LEDGERS = 1_024;
|
|
133
|
+
/**
|
|
134
|
+
* Per OpenCode session: catalog-priced cost (from `TurnEnded`) and what
|
|
135
|
+
* this provider has reported to the host. Priced turn-ends reconcile the balance.
|
|
136
|
+
*/
|
|
137
|
+
export class BillingLedger {
|
|
138
|
+
entries;
|
|
139
|
+
constructor(entries = new Map()) {
|
|
140
|
+
this.entries = entries;
|
|
141
|
+
}
|
|
142
|
+
entry(key) {
|
|
143
|
+
let entry = this.entries.get(key);
|
|
144
|
+
if (entry) {
|
|
145
|
+
this.entries.delete(key);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
entry = { realUsd: 0, billedUsd: 0, estimatedUsd: 0 };
|
|
149
|
+
while (this.entries.size >= MAX_LEDGERS) {
|
|
150
|
+
const oldest = this.entries.keys().next().value;
|
|
151
|
+
if (oldest === undefined)
|
|
152
|
+
break;
|
|
153
|
+
this.entries.delete(oldest);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
this.entries.set(key, entry);
|
|
157
|
+
return entry;
|
|
158
|
+
}
|
|
159
|
+
recordReal(key, usd, run) {
|
|
160
|
+
if (!Number.isFinite(usd) || usd < 0)
|
|
161
|
+
return;
|
|
162
|
+
const entry = this.entry(key);
|
|
163
|
+
entry.realUsd += usd;
|
|
164
|
+
// Missing TurnEnded is unknown cost, not free work. Other Runs' estimates
|
|
165
|
+
// stay provisionally covered, including Runs already closed by the host.
|
|
166
|
+
const priorEstimate = run?.ledgerEpoch === entry ? run.estimatedUsd ?? 0 : 0;
|
|
167
|
+
entry.estimatedUsd = Math.max(0, entry.estimatedUsd - priorEstimate);
|
|
168
|
+
if (run)
|
|
169
|
+
run.estimatedUsd = 0;
|
|
170
|
+
}
|
|
171
|
+
recordBilled(key, usd, run) {
|
|
172
|
+
if (!Number.isFinite(usd) || usd <= 0)
|
|
173
|
+
return;
|
|
174
|
+
const entry = this.entry(key);
|
|
175
|
+
entry.billedUsd += usd;
|
|
176
|
+
if (run) {
|
|
177
|
+
// An evicted session starts a new ledger; its old Run estimate must not
|
|
178
|
+
// replace another Run's estimates in the new entry.
|
|
179
|
+
if (run.ledgerEpoch !== entry)
|
|
180
|
+
run.estimatedUsd = 0;
|
|
181
|
+
run.ledgerEpoch = entry;
|
|
182
|
+
entry.estimatedUsd += usd;
|
|
183
|
+
run.estimatedUsd = (run.estimatedUsd ?? 0) + usd;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Confirmed plus unconfirmed cost minus the amount reported to the host. */
|
|
187
|
+
outstanding(key) {
|
|
188
|
+
const entry = this.entries.get(key);
|
|
189
|
+
return entry ? entry.realUsd + entry.estimatedUsd - entry.billedUsd : 0;
|
|
190
|
+
}
|
|
191
|
+
estimated(key) {
|
|
192
|
+
return this.entries.get(key)?.estimatedUsd ?? 0;
|
|
193
|
+
}
|
|
194
|
+
totals(key) {
|
|
195
|
+
const entry = this.entries.get(key);
|
|
196
|
+
return entry ? { realUsd: entry.realUsd, billedUsd: entry.billedUsd } : { realUsd: 0, billedUsd: 0 };
|
|
197
|
+
}
|
|
198
|
+
clear() {
|
|
199
|
+
this.entries.clear();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Plugin and SDK loaders can evaluate separate copies of this package in one
|
|
203
|
+
// process. Share plain ledger data, rather than a class instance owned by one
|
|
204
|
+
// module graph, so session balances survive those loader transitions.
|
|
205
|
+
const BILLING_ENTRIES = Symbol.for("cursor-opencode-provider.billing-ledger.v2");
|
|
206
|
+
const processState = globalThis;
|
|
207
|
+
const sharedEntries = processState[BILLING_ENTRIES] ??= new Map();
|
|
208
|
+
export const billingLedger = new BillingLedger(sharedEntries);
|
|
209
|
+
/** The catalog cost entry for this Run's model, if Cursor publishes one. */
|
|
210
|
+
export function cursorRunCost(wireModelId, parameterValues) {
|
|
211
|
+
return getCursorModelCost(cursorPricingId(wireModelId, parameterValues));
|
|
212
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function normalizeClassicInterruptedResults(messages: readonly unknown[]): any[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { restoreOpenCodeTimeout } from "./shell-timeout.js";
|
|
2
|
+
/**
|
|
3
|
+
* OpenCode 1 exposes interrupted partial output as successful SDK output:
|
|
4
|
+
* https://github.com/anomalyco/opencode/blob/v1.18.31/packages/opencode/src/session/message-v2.ts#L325-L346
|
|
5
|
+
* Its shell also completes with exit:null after cancellation and appends its
|
|
6
|
+
* own metadata block (tool/shell.ts L542-L589). Preserve failure in the model
|
|
7
|
+
* projection while retaining progress text. A timed-out shell is stored with
|
|
8
|
+
* a display notice in place of that block and only `state.output` reaches the
|
|
9
|
+
* model, so the projection restores the block. Stored messages are not mutated.
|
|
10
|
+
*/
|
|
11
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
12
|
+
export function normalizeClassicInterruptedResults(messages) {
|
|
13
|
+
return messages.map(message => {
|
|
14
|
+
const msg = record(message);
|
|
15
|
+
if (!msg || !Array.isArray(msg.parts))
|
|
16
|
+
return message;
|
|
17
|
+
return { ...msg, parts: msg.parts.map(item => {
|
|
18
|
+
const part = record(item);
|
|
19
|
+
const state = record(part?.state);
|
|
20
|
+
if (part?.type !== "tool" || typeof part.callID !== "string" || !part.callID.startsWith("cursor_") || !state)
|
|
21
|
+
return item;
|
|
22
|
+
const metadata = record(state.metadata);
|
|
23
|
+
const timeoutMs = metadata?.cursorShellTimeoutMs;
|
|
24
|
+
if (state.status === "completed" && part.tool === "bash" && typeof state.output === "string"
|
|
25
|
+
&& typeof timeoutMs === "number" && Number.isSafeInteger(timeoutMs) && timeoutMs >= 0) {
|
|
26
|
+
return { ...part, state: { ...state, output: restoreOpenCodeTimeout(state.output, timeoutMs) } };
|
|
27
|
+
}
|
|
28
|
+
const partialError = state.status === "error" && metadata?.interrupted === true;
|
|
29
|
+
// Null exit and the native trailing block must agree. Ordinary successful
|
|
30
|
+
// stdout can legitimately contain the same words or markup.
|
|
31
|
+
const shellAbort = state.status === "completed" && part.tool === "bash" && metadata?.exit === null
|
|
32
|
+
&& typeof state.output === "string" && /<shell_metadata>\r?\nUser aborted the command\r?\n<\/shell_metadata>\s*$/.test(state.output);
|
|
33
|
+
if (!partialError && !shellAbort)
|
|
34
|
+
return item;
|
|
35
|
+
const message = typeof state.error === "string" && state.error ? state.error : "Tool execution interrupted";
|
|
36
|
+
const progress = shellAbort ? state.output : typeof metadata?.output === "string" ? metadata.output : "";
|
|
37
|
+
const { output: _output, title: _title, attachments: _attachments, ...base } = state;
|
|
38
|
+
return { ...part, state: { ...base, status: "error", error: progress ? `${message}\n\n${progress}` : message,
|
|
39
|
+
metadata: { ...metadata, interrupted: false } } };
|
|
40
|
+
}) };
|
|
41
|
+
});
|
|
42
|
+
}
|
package/dist/context/build.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type OpencodeToolDef } from "../protocol/tools.js";
|
|
2
2
|
import { type OpencodeJson } from "./rules.js";
|
|
3
|
+
import { hostSkillFiles, loadBridgeSkills } from "./skills.js";
|
|
3
4
|
export type BuildRequestContextInput = {
|
|
4
5
|
workspaceRoot: string;
|
|
5
6
|
tools?: OpencodeToolDef[];
|
|
@@ -13,6 +14,8 @@ export type BuildRequestContextInput = {
|
|
|
13
14
|
mergedConfig?: OpencodeJson;
|
|
14
15
|
/** Host system context to deliver as the frozen system-instructions rule. */
|
|
15
16
|
systemInstructions?: SystemInstructions;
|
|
17
|
+
/** Optional host session id for `opencode.host.skills` list(). */
|
|
18
|
+
sessionID?: string;
|
|
16
19
|
};
|
|
17
20
|
/**
|
|
18
21
|
* Host system context (OpenCode's system prompt plus this provider's
|
|
@@ -48,10 +51,11 @@ export declare function withSystemInstructions(context: Record<string, unknown>,
|
|
|
48
51
|
export declare const DYNAMIC_REQUEST_CONTEXT_KEYS: readonly ["tools", "custom_subagents", "mcp_file_system_options", "mcp_meta_tool_options", "web_search_enabled", "web_fetch_enabled", "custom_subagents_info_complete", "mcp_file_system_info_complete", "mcp_info_complete", "hooks_additional_context"];
|
|
49
52
|
export type DynamicRequestContextKey = typeof DYNAMIC_REQUEST_CONTEXT_KEYS[number];
|
|
50
53
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
54
|
+
* Derived from the system-instructions rule on every materialization (see
|
|
55
|
+
* `skills.ts`); never kept on a frozen base.
|
|
53
56
|
*/
|
|
54
57
|
export declare const HOST_DUPLICATED_REQUEST_CONTEXT_KEYS: readonly ["agent_skills", "agent_skills_info_complete"];
|
|
58
|
+
export declare function resetWorkspaceFactsForTests(): void;
|
|
55
59
|
/**
|
|
56
60
|
* Full RequestContext payload for live UMA + exec #10 reply.
|
|
57
61
|
* Workspace env/git/layout, the host system-instructions rule, and
|
|
@@ -59,9 +63,24 @@ export declare const HOST_DUPLICATED_REQUEST_CONTEXT_KEYS: readonly ["agent_skil
|
|
|
59
63
|
* own directories.
|
|
60
64
|
*/
|
|
61
65
|
export declare function buildRequestContext(input: BuildRequestContextInput): Promise<Record<string, unknown>>;
|
|
66
|
+
/** Locations for path-desc materialization (bridge, then remembered OC2 files). */
|
|
67
|
+
export declare function resolveSkillLocations(input: Pick<BuildRequestContextInput, "tools" | "sessionID">, workspaceRoot: string): Promise<{
|
|
68
|
+
bridgeSkills?: Awaited<ReturnType<typeof loadBridgeSkills>>;
|
|
69
|
+
skillFiles?: ReturnType<typeof hostSkillFiles>;
|
|
70
|
+
}>;
|
|
62
71
|
/** Rediscover only capability/plugin sections that may change during a chat. */
|
|
63
72
|
export declare function buildDynamicRequestContext(input: BuildRequestContextInput): Promise<Record<string, unknown>>;
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
export type MaterializeRequestContextOptions = {
|
|
74
|
+
tools?: OpencodeToolDef[];
|
|
75
|
+
skillFiles?: ReadonlyMap<string, string>;
|
|
76
|
+
bridgeSkills?: Awaited<ReturnType<typeof loadBridgeSkills>>;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Keep expensive workspace state frozen while replacing every live capability
|
|
80
|
+
* field. Skill locations (bridge / OpenCode 2 files / OpenCode 1 catalog
|
|
81
|
+
* paths) turn the skill catalog in the system-instructions rule into Cursor's
|
|
82
|
+
* path-desc `agent_skills`.
|
|
83
|
+
*/
|
|
84
|
+
export declare function materializeRequestContext(base: Record<string, unknown>, dynamic: Record<string, unknown>, options?: MaterializeRequestContextOptions): Record<string, unknown>;
|
|
66
85
|
/** Strip live capability fields before retaining/persisting a conversation base. */
|
|
67
86
|
export declare function requestContextBase(context: Record<string, unknown>): Record<string, unknown>;
|