@vidit19sharma/opencode-cursor-auth 0.1.0 → 0.3.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/CHANGELOG.md +61 -0
- package/README.md +83 -8
- package/dist/bridge.d.ts +14 -1
- package/dist/bridge.js +375 -112
- package/dist/index.d.ts +1 -3
- package/dist/index.js +3 -128
- package/dist/models.d.ts +20 -0
- package/dist/models.js +134 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.js +139 -0
- package/installation.md +139 -0
- package/package.json +3 -1
package/dist/bridge.js
CHANGED
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
* next HTTP request (role:"tool" messages) resolves the parked promises and
|
|
9
9
|
* the SAME run continues — one metered Cursor run per opencode tool loop.
|
|
10
10
|
*/
|
|
11
|
-
import { Agent } from "@cursor/sdk";
|
|
11
|
+
import { Agent, AuthenticationError, ConfigurationError, NetworkError, RateLimitError } from "@cursor/sdk";
|
|
12
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
15
15
|
import { homedir, tmpdir } from "node:os";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
+
import { markBlocked, resolveVariant } from "./models.js";
|
|
17
18
|
// @cursor/sdk's internal logger writes "HH:MM:SS.mmm INFO ..." lines via
|
|
18
19
|
// console.log unconditionally (no config/env to silence it, verified against
|
|
19
20
|
// the esm bundle). Filter exactly those lines, pass everything else through.
|
|
@@ -31,6 +32,111 @@ function textOf(content) {
|
|
|
31
32
|
return "";
|
|
32
33
|
return content.map((p) => p.text ?? "").join("");
|
|
33
34
|
}
|
|
35
|
+
function statusFor(err) {
|
|
36
|
+
const e = (typeof err === "object" && err !== null ? err : {});
|
|
37
|
+
const msg = String(e.message ?? err ?? "");
|
|
38
|
+
const code = typeof e.code === "string" ? e.code : "";
|
|
39
|
+
if (err instanceof AuthenticationError || e.status === 401 || code === "unauthenticated" || /invalid user api key/i.test(msg))
|
|
40
|
+
return 401;
|
|
41
|
+
if (err instanceof RateLimitError || e.status === 429 || code === "resource_exhausted" || /rate limit/i.test(msg))
|
|
42
|
+
return 429;
|
|
43
|
+
if (code === "permission_denied" || /model blocked|permission denied|forbidden/i.test(msg))
|
|
44
|
+
return 403;
|
|
45
|
+
if (err instanceof ConfigurationError || code === "invalid_argument" || code === "not_found" || /cannot use this model|unknown model/i.test(msg))
|
|
46
|
+
return 400;
|
|
47
|
+
return 502;
|
|
48
|
+
}
|
|
49
|
+
const ERROR_TYPE = {
|
|
50
|
+
400: "invalid_request_error",
|
|
51
|
+
401: "authentication_error",
|
|
52
|
+
403: "permission_error",
|
|
53
|
+
429: "rate_limit_error",
|
|
54
|
+
};
|
|
55
|
+
function errorBody(err, status = statusFor(err)) {
|
|
56
|
+
const e = (typeof err === "object" && err !== null ? err : {});
|
|
57
|
+
return {
|
|
58
|
+
error: {
|
|
59
|
+
message: String(e.message ?? err ?? "unknown error"),
|
|
60
|
+
type: ERROR_TYPE[status] ?? "api_error",
|
|
61
|
+
...(typeof e.code === "string" ? { code: e.code } : {}),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const abortError = () => new DOMException("aborted", "AbortError");
|
|
66
|
+
export function isTransient(err) {
|
|
67
|
+
const e = (typeof err === "object" && err !== null ? err : {});
|
|
68
|
+
if (e.name === "AbortError")
|
|
69
|
+
return false;
|
|
70
|
+
const status = statusFor(err);
|
|
71
|
+
if (status === 429)
|
|
72
|
+
return true;
|
|
73
|
+
if (status === 400 || status === 401 || status === 403 || e.status === 404)
|
|
74
|
+
return false;
|
|
75
|
+
if (err instanceof NetworkError || (typeof e.status === "number" && e.status >= 500))
|
|
76
|
+
return true;
|
|
77
|
+
return /unavailable|deadline_exceeded|ECONNRESET|ETIMEDOUT|fetch failed/i.test(`${e.code ?? ""} ${e.message ?? ""}`);
|
|
78
|
+
}
|
|
79
|
+
export async function withRetry(fn, signal, baseDelayMs = 1000, retryable = isTransient) {
|
|
80
|
+
for (let attempt = 0;; attempt++) {
|
|
81
|
+
if (signal?.aborted)
|
|
82
|
+
throw abortError();
|
|
83
|
+
try {
|
|
84
|
+
return await fn();
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
if (attempt >= 2 || signal?.aborted || !retryable(err))
|
|
88
|
+
throw err;
|
|
89
|
+
const delay = Math.min(baseDelayMs * 2 ** attempt * (0.75 + Math.random() * 0.5), 30_000);
|
|
90
|
+
await new Promise((r) => {
|
|
91
|
+
const t = setTimeout(r, delay);
|
|
92
|
+
signal?.addEventListener("abort", () => (clearTimeout(t), r(undefined)), { once: true });
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// ponytail: global semaphore, slot held across parked tool loops; per-session fairness if it ever matters
|
|
98
|
+
let maxActive = Number(process.env.OPENCODE_CURSOR_MAX_ACTIVE) || 4;
|
|
99
|
+
let active = 0;
|
|
100
|
+
const waiters = [];
|
|
101
|
+
function pump() {
|
|
102
|
+
while (active < maxActive && waiters.length) {
|
|
103
|
+
active++;
|
|
104
|
+
waiters.shift()();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function acquireSlot(signal) {
|
|
108
|
+
if (signal?.aborted)
|
|
109
|
+
return Promise.reject(abortError());
|
|
110
|
+
if (active < maxActive && !waiters.length) {
|
|
111
|
+
active++;
|
|
112
|
+
return Promise.resolve();
|
|
113
|
+
}
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const onAbort = () => {
|
|
116
|
+
const i = waiters.indexOf(go);
|
|
117
|
+
if (i >= 0)
|
|
118
|
+
waiters.splice(i, 1);
|
|
119
|
+
reject(abortError());
|
|
120
|
+
};
|
|
121
|
+
const go = () => {
|
|
122
|
+
signal?.removeEventListener("abort", onAbort);
|
|
123
|
+
resolve();
|
|
124
|
+
};
|
|
125
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
126
|
+
waiters.push(go);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function releaseSlot() {
|
|
130
|
+
active--;
|
|
131
|
+
pump();
|
|
132
|
+
}
|
|
133
|
+
export function _setMaxActive(n) {
|
|
134
|
+
maxActive = n;
|
|
135
|
+
pump();
|
|
136
|
+
}
|
|
137
|
+
export function errorResponse(err, status = statusFor(err)) {
|
|
138
|
+
return Response.json(errorBody(err, status), { status });
|
|
139
|
+
}
|
|
34
140
|
function chunk(id, model, delta, finish, usage) {
|
|
35
141
|
return {
|
|
36
142
|
id,
|
|
@@ -41,7 +147,7 @@ function chunk(id, model, delta, finish, usage) {
|
|
|
41
147
|
...(usage ? { usage } : {}),
|
|
42
148
|
};
|
|
43
149
|
}
|
|
44
|
-
function sseResponse(model) {
|
|
150
|
+
function sseResponse(model, onCancel) {
|
|
45
151
|
const id = `chatcmpl-${randomUUID()}`;
|
|
46
152
|
const enc = new TextEncoder();
|
|
47
153
|
let controller;
|
|
@@ -53,19 +159,31 @@ function sseResponse(model) {
|
|
|
53
159
|
},
|
|
54
160
|
cancel() {
|
|
55
161
|
closed = true;
|
|
162
|
+
onCancel();
|
|
56
163
|
},
|
|
57
164
|
});
|
|
165
|
+
function frame(payload) {
|
|
166
|
+
if (closed)
|
|
167
|
+
return;
|
|
168
|
+
controller.enqueue(enc.encode(`data: ${JSON.stringify(payload)}\n\n`));
|
|
169
|
+
}
|
|
58
170
|
function send(delta, finish = null, usage) {
|
|
171
|
+
frame(chunk(id, model, delta, finish, usage));
|
|
172
|
+
}
|
|
173
|
+
function close() {
|
|
59
174
|
if (closed)
|
|
60
175
|
return;
|
|
61
|
-
|
|
176
|
+
closed = true;
|
|
177
|
+
try {
|
|
178
|
+
controller.close();
|
|
179
|
+
}
|
|
180
|
+
catch { }
|
|
62
181
|
}
|
|
63
182
|
function end() {
|
|
64
183
|
if (closed)
|
|
65
184
|
return;
|
|
66
185
|
controller.enqueue(enc.encode("data: [DONE]\n\n"));
|
|
67
|
-
|
|
68
|
-
closed = true;
|
|
186
|
+
close();
|
|
69
187
|
}
|
|
70
188
|
const sink = {
|
|
71
189
|
text: (t) => send({ content: t }),
|
|
@@ -83,10 +201,10 @@ function sseResponse(model) {
|
|
|
83
201
|
end();
|
|
84
202
|
},
|
|
85
203
|
fail: (err) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
end();
|
|
204
|
+
frame(errorBody(err));
|
|
205
|
+
close();
|
|
89
206
|
},
|
|
207
|
+
close,
|
|
90
208
|
};
|
|
91
209
|
return {
|
|
92
210
|
response: new Response(stream, {
|
|
@@ -96,13 +214,11 @@ function sseResponse(model) {
|
|
|
96
214
|
sink,
|
|
97
215
|
};
|
|
98
216
|
}
|
|
99
|
-
function jsonCollector(model) {
|
|
217
|
+
function jsonCollector(model, resolve) {
|
|
100
218
|
const id = `chatcmpl-${randomUUID()}`;
|
|
101
219
|
let content = "";
|
|
102
220
|
let calls = [];
|
|
103
|
-
|
|
104
|
-
const promise = new Promise((r) => (resolve = r));
|
|
105
|
-
const sink = {
|
|
221
|
+
return {
|
|
106
222
|
text: (t) => (content += t),
|
|
107
223
|
reasoning: () => { },
|
|
108
224
|
toolCalls: (c) => (calls = calls.concat(c)),
|
|
@@ -132,9 +248,18 @@ function jsonCollector(model) {
|
|
|
132
248
|
],
|
|
133
249
|
usage: usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
134
250
|
})),
|
|
135
|
-
fail: (err) => resolve(
|
|
251
|
+
fail: (err) => resolve(errorResponse(err)),
|
|
252
|
+
close: () => { },
|
|
136
253
|
};
|
|
137
|
-
|
|
254
|
+
}
|
|
255
|
+
function deferred() {
|
|
256
|
+
let resolve;
|
|
257
|
+
let reject;
|
|
258
|
+
const promise = new Promise((res, rej) => {
|
|
259
|
+
resolve = res;
|
|
260
|
+
reject = rej;
|
|
261
|
+
});
|
|
262
|
+
return { promise, resolve, reject };
|
|
138
263
|
}
|
|
139
264
|
const BATCH_SETTLE_MS = 300;
|
|
140
265
|
const TOOL_RESULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -144,13 +269,33 @@ const DISK_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
144
269
|
const MAX_SESSIONS = 8;
|
|
145
270
|
const SESSION_STORE_DIR = join(homedir(), ".cache", "opencode-cursor-auth");
|
|
146
271
|
const SESSION_STORE_PATH = join(SESSION_STORE_DIR, "sessions.json");
|
|
272
|
+
const TMP_CWD_PREFIX = join(tmpdir(), "opencode-cursor-");
|
|
147
273
|
const sessions = new Map();
|
|
148
274
|
const heldByAgent = new Map();
|
|
149
275
|
const agentByToolCallId = new Map();
|
|
150
276
|
let diskSessions = new Map();
|
|
151
277
|
let sessionsLoaded = false;
|
|
152
|
-
|
|
278
|
+
// ponytail: no orphan sweep, other opencode instances may own those dirs
|
|
279
|
+
function releaseCwd(cwd) {
|
|
280
|
+
if (!cwd.startsWith(TMP_CWD_PREFIX))
|
|
281
|
+
return;
|
|
282
|
+
for (const v of sessions.values())
|
|
283
|
+
if (v.cwd === cwd)
|
|
284
|
+
return;
|
|
285
|
+
for (const v of diskSessions.values())
|
|
286
|
+
if (v.cwd === cwd)
|
|
287
|
+
return;
|
|
288
|
+
for (const h of heldByAgent.values())
|
|
289
|
+
if (h.cwd === cwd)
|
|
290
|
+
return;
|
|
291
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
292
|
+
}
|
|
293
|
+
// scope (opencode session id) is folded in so identical histories from
|
|
294
|
+
// different sessions (e.g. parallel sub-agents) never share a Cursor agent
|
|
295
|
+
function hashMessages(messages, scope) {
|
|
153
296
|
const norm = messages.map((m) => [m.role, textOf(m.content), m.tool_call_id ?? "", JSON.stringify(m.tool_calls ?? null)]);
|
|
297
|
+
if (scope)
|
|
298
|
+
norm.unshift(["scope", scope]);
|
|
154
299
|
return createHash("sha256").update(JSON.stringify(norm)).digest("hex");
|
|
155
300
|
}
|
|
156
301
|
function collectDiskEntries() {
|
|
@@ -176,6 +321,9 @@ async function loadSessions() {
|
|
|
176
321
|
if (now - v.lastUsedAt <= DISK_SESSION_TTL_MS)
|
|
177
322
|
diskSessions.set(k, v);
|
|
178
323
|
}
|
|
324
|
+
for (const v of Object.values(raw.entries))
|
|
325
|
+
if (now - v.lastUsedAt > DISK_SESSION_TTL_MS)
|
|
326
|
+
releaseCwd(v.cwd);
|
|
179
327
|
}
|
|
180
328
|
catch { }
|
|
181
329
|
}
|
|
@@ -185,9 +333,17 @@ function saveSessions() {
|
|
|
185
333
|
}
|
|
186
334
|
function sweepSessions(save = true) {
|
|
187
335
|
const now = Date.now();
|
|
336
|
+
const evicted = [];
|
|
337
|
+
for (const [k, v] of diskSessions)
|
|
338
|
+
if (now - v.lastUsedAt > DISK_SESSION_TTL_MS) {
|
|
339
|
+
diskSessions.delete(k);
|
|
340
|
+
evicted.push(v.cwd);
|
|
341
|
+
}
|
|
188
342
|
for (const [k, v] of sessions)
|
|
189
|
-
if (now - v.lastUsedAt > SESSION_TTL_MS)
|
|
343
|
+
if (now - v.lastUsedAt > SESSION_TTL_MS) {
|
|
190
344
|
sessions.delete(k);
|
|
345
|
+
evicted.push(v.cwd);
|
|
346
|
+
}
|
|
191
347
|
while (sessions.size > MAX_SESSIONS) {
|
|
192
348
|
let oldestKey = "";
|
|
193
349
|
let oldest = Infinity;
|
|
@@ -196,8 +352,11 @@ function sweepSessions(save = true) {
|
|
|
196
352
|
oldest = v.lastUsedAt;
|
|
197
353
|
oldestKey = k;
|
|
198
354
|
}
|
|
355
|
+
evicted.push(sessions.get(oldestKey).cwd);
|
|
199
356
|
sessions.delete(oldestKey);
|
|
200
357
|
}
|
|
358
|
+
for (const cwd of evicted)
|
|
359
|
+
releaseCwd(cwd);
|
|
201
360
|
if (save)
|
|
202
361
|
saveSessions();
|
|
203
362
|
}
|
|
@@ -214,15 +373,18 @@ function rememberSession(held, toolCalls) {
|
|
|
214
373
|
? { tool_calls: toolCalls.map((c) => ({ id: c.id, function: { name: c.name, arguments: c.argumentsJson } })) }
|
|
215
374
|
: {}),
|
|
216
375
|
};
|
|
217
|
-
const key = hashMessages(held.messages.concat(assistant));
|
|
376
|
+
const key = hashMessages(held.messages.concat(assistant), held.scope);
|
|
218
377
|
const entry = {
|
|
219
378
|
agentId: held.agent.agentId,
|
|
220
379
|
cwd: held.cwd,
|
|
221
380
|
lastUsedAt: Date.now(),
|
|
222
381
|
};
|
|
382
|
+
const displaced = sessions.get(key) ?? diskSessions.get(key);
|
|
223
383
|
sessions.set(key, entry);
|
|
224
384
|
diskSessions.set(key, entry);
|
|
225
385
|
sweepSessions();
|
|
386
|
+
if (displaced && displaced.cwd !== entry.cwd)
|
|
387
|
+
releaseCwd(displaced.cwd);
|
|
226
388
|
}
|
|
227
389
|
async function syncAgentsMd(cwd, messages) {
|
|
228
390
|
mkdirSync(cwd, { recursive: true });
|
|
@@ -242,7 +404,12 @@ function mapUsage(u) {
|
|
|
242
404
|
return undefined;
|
|
243
405
|
const prompt = u.inputTokens ?? 0;
|
|
244
406
|
const completion = u.outputTokens ?? 0;
|
|
245
|
-
|
|
407
|
+
const usage = { prompt_tokens: prompt, completion_tokens: completion, total_tokens: prompt + completion };
|
|
408
|
+
if (u.cacheReadTokens)
|
|
409
|
+
usage.prompt_tokens_details = { cached_tokens: u.cacheReadTokens };
|
|
410
|
+
if (u.reasoningTokens)
|
|
411
|
+
usage.completion_tokens_details = { reasoning_tokens: u.reasoningTokens };
|
|
412
|
+
return usage;
|
|
246
413
|
}
|
|
247
414
|
// deltas arriving between tool flush and the continuation request get buffered
|
|
248
415
|
function emit(held, kind, t) {
|
|
@@ -251,73 +418,124 @@ function emit(held, kind, t) {
|
|
|
251
418
|
else
|
|
252
419
|
held.buffer.push({ kind, t });
|
|
253
420
|
}
|
|
421
|
+
function openReply(held) {
|
|
422
|
+
if (held.opened)
|
|
423
|
+
return;
|
|
424
|
+
held.opened = true;
|
|
425
|
+
if (held.response)
|
|
426
|
+
held.reply.resolve(held.response);
|
|
427
|
+
}
|
|
254
428
|
function attachSink(held, sink) {
|
|
255
429
|
held.sink = sink;
|
|
430
|
+
if (held.buffer.length)
|
|
431
|
+
openReply(held);
|
|
256
432
|
for (const b of held.buffer)
|
|
257
433
|
(b.kind === "text" ? sink.text : sink.reasoning)(b.t);
|
|
258
434
|
held.buffer = [];
|
|
259
435
|
}
|
|
260
|
-
function
|
|
436
|
+
function attachSignal(held, signal) {
|
|
437
|
+
held.detachSignal?.();
|
|
438
|
+
held.detachSignal = null;
|
|
439
|
+
if (!signal)
|
|
440
|
+
return;
|
|
441
|
+
const onAbort = () => abortHeld(held);
|
|
442
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
443
|
+
held.detachSignal = () => signal.removeEventListener("abort", onAbort);
|
|
444
|
+
}
|
|
445
|
+
function closeAgent(held) {
|
|
446
|
+
try {
|
|
447
|
+
held.agent.close();
|
|
448
|
+
}
|
|
449
|
+
catch { }
|
|
450
|
+
}
|
|
451
|
+
function teardownHeld(held) {
|
|
261
452
|
held.done = true;
|
|
453
|
+
if (held.slot) {
|
|
454
|
+
held.slot = false;
|
|
455
|
+
releaseSlot();
|
|
456
|
+
}
|
|
457
|
+
held.detachSignal?.();
|
|
458
|
+
held.detachSignal = null;
|
|
262
459
|
if (held.flushTimer)
|
|
263
460
|
clearTimeout(held.flushTimer);
|
|
264
461
|
if (held.idleTimer)
|
|
265
462
|
clearTimeout(held.idleTimer);
|
|
266
463
|
for (const id of held.parked.keys())
|
|
267
464
|
agentByToolCallId.delete(id);
|
|
268
|
-
heldByAgent.delete(agentId);
|
|
465
|
+
heldByAgent.delete(held.agent.agentId);
|
|
466
|
+
}
|
|
467
|
+
function failHeld(held, err) {
|
|
468
|
+
teardownHeld(held);
|
|
469
|
+
if (statusFor(err) === 403)
|
|
470
|
+
markBlocked(held.model);
|
|
471
|
+
if (held.response && !held.opened) {
|
|
472
|
+
held.opened = true;
|
|
473
|
+
held.reply.resolve(errorResponse(err));
|
|
474
|
+
held.sink?.close();
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
held.sink?.fail(err);
|
|
478
|
+
}
|
|
479
|
+
closeAgent(held);
|
|
480
|
+
releaseCwd(held.cwd);
|
|
481
|
+
}
|
|
482
|
+
function abortHeld(held) {
|
|
483
|
+
if (held.done)
|
|
484
|
+
return;
|
|
485
|
+
teardownHeld(held);
|
|
486
|
+
held.run?.cancel().catch(() => { });
|
|
487
|
+
closeAgent(held);
|
|
488
|
+
held.sink?.close();
|
|
489
|
+
held.reply.reject(new DOMException("aborted", "AbortError"));
|
|
490
|
+
releaseCwd(held.cwd);
|
|
269
491
|
}
|
|
270
492
|
/** Model requested tools: emit tool_calls, end this HTTP segment, park the run. */
|
|
271
|
-
function flushToolCalls(
|
|
493
|
+
function flushToolCalls(held) {
|
|
272
494
|
held.flushTimer = null;
|
|
273
495
|
if (held.done || !held.pendingCalls.length)
|
|
274
496
|
return;
|
|
275
497
|
const calls = held.pendingCalls;
|
|
276
498
|
held.pendingCalls = [];
|
|
277
499
|
for (const c of calls)
|
|
278
|
-
agentByToolCallId.set(c.id, agentId);
|
|
500
|
+
agentByToolCallId.set(c.id, held.agent.agentId);
|
|
279
501
|
const sink = held.sink;
|
|
280
502
|
held.sink = null;
|
|
503
|
+
// the parked run outlives this HTTP segment, so its abort signal must not
|
|
504
|
+
// reach it; the continuation request attaches its own
|
|
505
|
+
held.detachSignal?.();
|
|
506
|
+
held.detachSignal = null;
|
|
507
|
+
openReply(held);
|
|
281
508
|
sink?.toolCalls(calls);
|
|
282
509
|
sink?.finish("tool_calls");
|
|
283
510
|
rememberSession(held, calls);
|
|
284
|
-
held.idleTimer = setTimeout(() =>
|
|
285
|
-
teardownHeld(agentId, held);
|
|
286
|
-
held.run.cancel().catch(() => { });
|
|
287
|
-
try {
|
|
288
|
-
held.agent.close();
|
|
289
|
-
}
|
|
290
|
-
catch { }
|
|
291
|
-
}, TOOL_RESULT_TIMEOUT_MS);
|
|
511
|
+
held.idleTimer = setTimeout(() => abortHeld(held), TOOL_RESULT_TIMEOUT_MS);
|
|
292
512
|
held.idleTimer.unref?.();
|
|
293
513
|
}
|
|
294
|
-
async function wireRunCompletion(
|
|
295
|
-
if (held.wired)
|
|
514
|
+
async function wireRunCompletion(held) {
|
|
515
|
+
if (held.wired || !held.run)
|
|
296
516
|
return;
|
|
297
517
|
held.wired = true;
|
|
518
|
+
let result;
|
|
298
519
|
try {
|
|
299
|
-
|
|
300
|
-
if (held.done)
|
|
301
|
-
return;
|
|
302
|
-
const usage = mapUsage(result.usage) ?? held.usage;
|
|
303
|
-
teardownHeld(agentId, held);
|
|
304
|
-
rememberSession(held, []);
|
|
305
|
-
held.sink?.finish("stop", usage);
|
|
306
|
-
try {
|
|
307
|
-
held.agent.close();
|
|
308
|
-
}
|
|
309
|
-
catch { }
|
|
520
|
+
result = await held.run.wait();
|
|
310
521
|
}
|
|
311
522
|
catch (err) {
|
|
312
|
-
if (held.done)
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
held.sink?.fail(err);
|
|
316
|
-
try {
|
|
317
|
-
held.agent.close();
|
|
318
|
-
}
|
|
319
|
-
catch { }
|
|
523
|
+
if (!held.done)
|
|
524
|
+
failHeld(held, err);
|
|
525
|
+
return;
|
|
320
526
|
}
|
|
527
|
+
if (held.done)
|
|
528
|
+
return;
|
|
529
|
+
if (result.status === "cancelled")
|
|
530
|
+
return abortHeld(held);
|
|
531
|
+
if (result.status === "error")
|
|
532
|
+
return failHeld(held, result.error ?? new Error("cursor run failed"));
|
|
533
|
+
const usage = mapUsage(result.usage) ?? held.usage;
|
|
534
|
+
teardownHeld(held);
|
|
535
|
+
rememberSession(held, []);
|
|
536
|
+
openReply(held);
|
|
537
|
+
held.sink?.finish("stop", usage);
|
|
538
|
+
closeAgent(held);
|
|
321
539
|
}
|
|
322
540
|
export function _resetForTest() {
|
|
323
541
|
sessions.clear();
|
|
@@ -326,79 +544,110 @@ export function _resetForTest() {
|
|
|
326
544
|
agentByToolCallId.clear();
|
|
327
545
|
sessionsLoaded = false;
|
|
328
546
|
}
|
|
329
|
-
export
|
|
547
|
+
export function _stats() {
|
|
548
|
+
return {
|
|
549
|
+
held: heldByAgent.size,
|
|
550
|
+
sessions: sessions.size,
|
|
551
|
+
disk: diskSessions.size,
|
|
552
|
+
agentIds: [...sessions.values()].map((v) => v.agentId).sort(),
|
|
553
|
+
active,
|
|
554
|
+
waiting: waiters.length,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
export async function handleChatCompletion(body, apiKey, signal, scope) {
|
|
558
|
+
if (signal?.aborted)
|
|
559
|
+
throw new DOMException("aborted", "AbortError");
|
|
330
560
|
await loadSessions();
|
|
331
561
|
const streaming = body.stream !== false;
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
const
|
|
562
|
+
const reply = deferred();
|
|
563
|
+
let held;
|
|
564
|
+
const sse = streaming ? sseResponse(body.model, () => held && abortHeld(held)) : undefined;
|
|
565
|
+
const sink = sse?.sink ?? jsonCollector(body.model, reply.resolve);
|
|
336
566
|
// continuation: trailing tool results for a parked run
|
|
337
567
|
const lastMsg = body.messages[body.messages.length - 1];
|
|
338
|
-
if (lastMsg?.role === "tool" && lastMsg.tool_call_id
|
|
568
|
+
if (lastMsg?.role === "tool" && lastMsg.tool_call_id) {
|
|
339
569
|
const agentId = agentByToolCallId.get(lastMsg.tool_call_id);
|
|
340
|
-
|
|
341
|
-
if (held
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
held.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
570
|
+
held = agentId ? heldByAgent.get(agentId) : undefined;
|
|
571
|
+
if (!held || held.done) {
|
|
572
|
+
return Response.json({ error: { message: "cursor run for this tool call expired — please retry", type: "invalid_request_error", code: "tool_call_expired" } }, { status: 400 });
|
|
573
|
+
}
|
|
574
|
+
if (held.idleTimer)
|
|
575
|
+
clearTimeout(held.idleTimer);
|
|
576
|
+
held.messages = body.messages;
|
|
577
|
+
held.textAcc = "";
|
|
578
|
+
held.reply = reply;
|
|
579
|
+
held.response = sse?.response ?? null;
|
|
580
|
+
held.opened = false;
|
|
581
|
+
attachSignal(held, signal);
|
|
582
|
+
attachSink(held, sink);
|
|
583
|
+
for (const m of body.messages) {
|
|
584
|
+
if (m.role !== "tool" || !m.tool_call_id)
|
|
585
|
+
continue;
|
|
586
|
+
const resolve = held.parked.get(m.tool_call_id);
|
|
587
|
+
if (resolve) {
|
|
588
|
+
held.parked.delete(m.tool_call_id);
|
|
589
|
+
agentByToolCallId.delete(m.tool_call_id);
|
|
590
|
+
resolve(textOf(m.content));
|
|
356
591
|
}
|
|
357
|
-
return reply();
|
|
358
592
|
}
|
|
359
|
-
|
|
360
|
-
return reply();
|
|
593
|
+
return reply.promise;
|
|
361
594
|
}
|
|
595
|
+
// opencode wraps variant options as providerOptions.cursor; @ai-sdk/openai-compatible (name "cursor") spreads them into the body top level
|
|
596
|
+
const model = resolveVariant(body.model, body.cursor_params) ?? { id: body.model };
|
|
597
|
+
if (typeof model === "string")
|
|
598
|
+
return errorResponse(model, 400);
|
|
362
599
|
// new turn: reuse the agent whose last-seen conversation matches our prefix
|
|
363
600
|
const prefix = body.messages.slice(0, -1);
|
|
364
|
-
const key = hashMessages(prefix);
|
|
601
|
+
const key = hashMessages(prefix, scope);
|
|
602
|
+
await acquireSlot(signal);
|
|
365
603
|
const entry = sessions.get(key) ?? diskSessions.get(key);
|
|
366
604
|
let agent;
|
|
367
605
|
let cwd;
|
|
368
606
|
let newSuffix = body.messages;
|
|
607
|
+
const agentOptions = (cwd) => ({
|
|
608
|
+
model,
|
|
609
|
+
tools: ["mcp"],
|
|
610
|
+
...(apiKey ? { apiKey } : {}),
|
|
611
|
+
local: { cwd, settingSources: ["project"], enableAgentRetries: true },
|
|
612
|
+
});
|
|
369
613
|
if (entry) {
|
|
370
614
|
sessions.delete(key);
|
|
615
|
+
diskSessions.delete(key);
|
|
371
616
|
cwd = entry.cwd;
|
|
372
|
-
agent = await Agent.resume(entry.agentId, {
|
|
373
|
-
model: { id: body.model },
|
|
374
|
-
tools: ["mcp"],
|
|
375
|
-
...(apiKey ? { apiKey } : {}),
|
|
376
|
-
local: { cwd, settingSources: ["project"], enableAgentRetries: true },
|
|
377
|
-
}).catch(() => {
|
|
378
|
-
sessions.delete(key);
|
|
379
|
-
diskSessions.delete(key);
|
|
617
|
+
agent = await withRetry(() => Agent.resume(entry.agentId, agentOptions(cwd)), signal).catch(() => {
|
|
380
618
|
saveSessions();
|
|
619
|
+
releaseCwd(entry.cwd);
|
|
381
620
|
return undefined;
|
|
382
621
|
});
|
|
383
622
|
if (agent)
|
|
384
623
|
newSuffix = body.messages.slice(-1);
|
|
385
624
|
}
|
|
386
625
|
if (!agent) {
|
|
387
|
-
cwd = await mkdtemp(
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
626
|
+
cwd = await mkdtemp(TMP_CWD_PREFIX);
|
|
627
|
+
try {
|
|
628
|
+
agent = await withRetry(() => Agent.create(agentOptions(cwd)), signal);
|
|
629
|
+
}
|
|
630
|
+
catch (err) {
|
|
631
|
+
releaseSlot();
|
|
632
|
+
releaseCwd(cwd);
|
|
633
|
+
if (signal?.aborted)
|
|
634
|
+
throw abortError();
|
|
635
|
+
return errorResponse(err);
|
|
636
|
+
}
|
|
394
637
|
}
|
|
395
|
-
|
|
638
|
+
held = {
|
|
396
639
|
agent,
|
|
397
640
|
cwd: cwd,
|
|
398
|
-
|
|
641
|
+
model: body.model,
|
|
642
|
+
run: null,
|
|
399
643
|
messages: body.messages,
|
|
644
|
+
scope,
|
|
400
645
|
textAcc: "",
|
|
401
646
|
sink: null,
|
|
647
|
+
reply,
|
|
648
|
+
response: sse?.response ?? null,
|
|
649
|
+
opened: false,
|
|
650
|
+
detachSignal: null,
|
|
402
651
|
buffer: [],
|
|
403
652
|
pendingCalls: [],
|
|
404
653
|
parked: new Map(),
|
|
@@ -406,9 +655,12 @@ export async function handleChatCompletion(body, apiKey) {
|
|
|
406
655
|
idleTimer: null,
|
|
407
656
|
wired: false,
|
|
408
657
|
done: false,
|
|
658
|
+
slot: true,
|
|
409
659
|
};
|
|
660
|
+
const self = held;
|
|
661
|
+
heldByAgent.set(agent.agentId, held);
|
|
662
|
+
attachSignal(held, signal);
|
|
410
663
|
attachSink(held, sink);
|
|
411
|
-
const agentIdRef = { id: "" };
|
|
412
664
|
const customTools = {};
|
|
413
665
|
for (const t of body.tools ?? []) {
|
|
414
666
|
customTools[t.function.name] = {
|
|
@@ -416,11 +668,11 @@ export async function handleChatCompletion(body, apiKey) {
|
|
|
416
668
|
inputSchema: t.function.parameters ?? { type: "object", properties: {} },
|
|
417
669
|
execute: (args, ctx) => {
|
|
418
670
|
const id = (ctx.toolCallId ?? `call_${randomUUID()}`).replace(/\s+/g, "_");
|
|
419
|
-
|
|
420
|
-
if (
|
|
421
|
-
clearTimeout(
|
|
422
|
-
|
|
423
|
-
return new Promise((resolve) =>
|
|
671
|
+
self.pendingCalls.push({ id, name: t.function.name, argumentsJson: JSON.stringify(args ?? {}) });
|
|
672
|
+
if (self.flushTimer)
|
|
673
|
+
clearTimeout(self.flushTimer);
|
|
674
|
+
self.flushTimer = setTimeout(() => flushToolCalls(self), BATCH_SETTLE_MS);
|
|
675
|
+
return new Promise((resolve) => self.parked.set(id, resolve));
|
|
424
676
|
},
|
|
425
677
|
};
|
|
426
678
|
}
|
|
@@ -430,29 +682,40 @@ export async function handleChatCompletion(body, apiKey) {
|
|
|
430
682
|
.map((m) => (m.role === "user" ? textOf(m.content) : `${m.role.toUpperCase()}: ${textOf(m.content)}`))
|
|
431
683
|
.filter(Boolean)
|
|
432
684
|
.join("\n\n") || "(empty message)";
|
|
685
|
+
let run;
|
|
433
686
|
try {
|
|
434
|
-
|
|
687
|
+
run = await withRetry(() => agent.send(prompt, {
|
|
435
688
|
onDelta: ({ update }) => {
|
|
689
|
+
if (self.done)
|
|
690
|
+
return;
|
|
436
691
|
if (update.type === "text-delta" && update.text) {
|
|
437
|
-
|
|
438
|
-
|
|
692
|
+
openReply(self);
|
|
693
|
+
self.textAcc += update.text;
|
|
694
|
+
emit(self, "text", update.text);
|
|
439
695
|
}
|
|
440
696
|
else if (update.type === "thinking-delta" && update.text) {
|
|
441
|
-
|
|
697
|
+
openReply(self);
|
|
698
|
+
emit(self, "reasoning", update.text);
|
|
442
699
|
}
|
|
443
700
|
else if (update.type === "turn-ended") {
|
|
444
|
-
|
|
701
|
+
self.usage = mapUsage(update.usage);
|
|
445
702
|
}
|
|
446
703
|
},
|
|
447
704
|
local: { customTools: customTools },
|
|
448
|
-
});
|
|
705
|
+
}), signal, 1000, (err) => !self.opened && !self.done && isTransient(err));
|
|
449
706
|
}
|
|
450
707
|
catch (err) {
|
|
451
|
-
|
|
452
|
-
|
|
708
|
+
if (!self.done)
|
|
709
|
+
failHeld(self, err);
|
|
710
|
+
return reply.promise;
|
|
711
|
+
}
|
|
712
|
+
if (self.done) {
|
|
713
|
+
run.cancel().catch(() => { });
|
|
714
|
+
return reply.promise;
|
|
453
715
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
716
|
+
if (run.agentId !== agent.agentId)
|
|
717
|
+
console.error(`[opencode-cursor-auth] run.agentId ${run.agentId} != agent.agentId ${agent.agentId}`);
|
|
718
|
+
self.run = run;
|
|
719
|
+
void wireRunCompletion(self);
|
|
720
|
+
return reply.promise;
|
|
458
721
|
}
|