opencode-qoder-bridge 0.1.8 → 0.1.9
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 +57 -1
- package/README.md +110 -37
- package/bin/statusline.mjs +30 -6
- package/bin/usage.mjs +6 -5
- package/dist/async-utils.d.ts +8 -0
- package/dist/async-utils.d.ts.map +1 -0
- package/dist/async-utils.js +27 -0
- package/dist/async-utils.js.map +1 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +26 -7
- package/dist/auth.js.map +1 -1
- package/dist/cost.d.ts +2 -1
- package/dist/cost.d.ts.map +1 -1
- package/dist/cost.js +163 -63
- package/dist/cost.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +5 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +152 -37
- package/dist/index.js.map +1 -1
- package/dist/language-model.d.ts +22 -0
- package/dist/language-model.d.ts.map +1 -1
- package/dist/language-model.js +931 -183
- package/dist/language-model.js.map +1 -1
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +35 -5
- package/dist/logger.js.map +1 -1
- package/dist/mcp-bridge.d.ts.map +1 -1
- package/dist/mcp-bridge.js +71 -7
- package/dist/mcp-bridge.js.map +1 -1
- package/dist/models.d.ts +8 -1
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +155 -44
- package/dist/models.js.map +1 -1
- package/dist/prompt-builder.d.ts.map +1 -1
- package/dist/prompt-builder.js +241 -43
- package/dist/prompt-builder.js.map +1 -1
- package/dist/sdk-auth.d.ts +3 -3
- package/dist/sdk-auth.d.ts.map +1 -1
- package/dist/sdk-auth.js +13 -9
- package/dist/sdk-auth.js.map +1 -1
- package/dist/sdk-session.d.ts.map +1 -1
- package/dist/sdk-session.js +26 -2
- package/dist/sdk-session.js.map +1 -1
- package/dist/session-store.d.ts +14 -3
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +451 -46
- package/dist/session-store.js.map +1 -1
- package/dist/state-dir.d.ts.map +1 -1
- package/dist/state-dir.js +2 -1
- package/dist/state-dir.js.map +1 -1
- package/dist/tool-normalizer.d.ts.map +1 -1
- package/dist/tool-normalizer.js +11 -2
- package/dist/tool-normalizer.js.map +1 -1
- package/dist/tui-register.d.ts.map +1 -1
- package/dist/tui-register.js +88 -37
- package/dist/tui-register.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +42 -17
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +14 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +54 -29
- package/dist/usage.js.map +1 -1
- package/package.json +12 -4
package/dist/language-model.js
CHANGED
|
@@ -1,35 +1,303 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
2
4
|
import { query } from "@qoder-ai/qoder-agent-sdk";
|
|
3
|
-
import { getModel, DEFAULT_MODEL_ID } from "./models.js";
|
|
5
|
+
import { getModel, DEFAULT_MODEL_ID, applyLiveModelUpdates } from "./models.js";
|
|
4
6
|
import { findQoderCLI } from "./auth.js";
|
|
5
7
|
import { buildPromptString, buildPromptIterable, latestPrompt, promptHasImage } from "./prompt-builder.js";
|
|
6
8
|
import { normalizeToolName, normalizeToolInputString } from "./tool-normalizer.js";
|
|
7
9
|
import { recordTurn } from "./cost.js";
|
|
8
|
-
import { ensureQoderSession,
|
|
10
|
+
import { deleteQoderSession, ensureQoderSession, getQoderSessionForCwd, getQoderSessionResetEpoch, withQoderSessionLease } from "./session-store.js";
|
|
9
11
|
import { hasQoderCredential, qoderAuth } from "./sdk-auth.js";
|
|
10
12
|
import { QoderAuthError, QoderSdkResultError } from "./errors.js";
|
|
11
|
-
import { debug, describeError } from "./logger.js";
|
|
13
|
+
import { debug, describeError, redactSensitiveText } from "./logger.js";
|
|
14
|
+
const UNSAFE_METADATA_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
15
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30 * 60 * 1000;
|
|
16
|
+
const MAX_REQUEST_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
const CLEANUP_GRACE_MS = 5_000;
|
|
18
|
+
const MAX_SEEN_MESSAGE_IDS = 100_000;
|
|
19
|
+
const MAX_TOOL_INPUT_CHARS = 4_000_000;
|
|
20
|
+
const MAX_OUTPUT_CHARS = 8_000_000;
|
|
21
|
+
const MAX_METADATA_NODES = 2_000;
|
|
22
|
+
const MAX_METADATA_STRING = 4_096;
|
|
23
|
+
const MAX_STOP_REASON_LENGTH = 256;
|
|
24
|
+
const QODER_BUILTIN_NAMES = {
|
|
25
|
+
read: "Read",
|
|
26
|
+
write: "Write",
|
|
27
|
+
edit: "Edit",
|
|
28
|
+
delete: "Delete",
|
|
29
|
+
view: "View",
|
|
30
|
+
bash: "Bash",
|
|
31
|
+
glob: "Glob",
|
|
32
|
+
grep: "Grep",
|
|
33
|
+
task: "Agent",
|
|
34
|
+
task_create: "TaskCreate",
|
|
35
|
+
taskcreate: "TaskCreate",
|
|
36
|
+
task_get: "TaskGet",
|
|
37
|
+
taskget: "TaskGet",
|
|
38
|
+
task_update: "TaskUpdate",
|
|
39
|
+
taskupdate: "TaskUpdate",
|
|
40
|
+
task_list: "TaskList",
|
|
41
|
+
tasklist: "TaskList",
|
|
42
|
+
question: "AskUserQuestion",
|
|
43
|
+
ask_user_question: "AskUserQuestion",
|
|
44
|
+
plan_exit: "ExitPlanMode",
|
|
45
|
+
exit_plan_mode: "ExitPlanMode",
|
|
46
|
+
skill: "Skill",
|
|
47
|
+
todo_write: "TodoWrite",
|
|
48
|
+
todowrite: "TodoWrite",
|
|
49
|
+
update_goal: "UpdateGoal",
|
|
50
|
+
updategoal: "UpdateGoal",
|
|
51
|
+
web_fetch: "WebFetch",
|
|
52
|
+
webfetch: "WebFetch",
|
|
53
|
+
web_search: "WebSearch",
|
|
54
|
+
websearch: "WebSearch",
|
|
55
|
+
image_gen: "ImageGen",
|
|
56
|
+
imagegen: "ImageGen",
|
|
57
|
+
image_search: "ImageSearch",
|
|
58
|
+
imagesearch: "ImageSearch",
|
|
59
|
+
notebook_edit: "NotebookEdit",
|
|
60
|
+
notebookedit: "NotebookEdit",
|
|
61
|
+
};
|
|
62
|
+
function toJsonValue(value, depth = 0, budget = { remaining: MAX_METADATA_NODES }) {
|
|
63
|
+
if (depth > 8)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (budget.remaining-- <= 0)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (value === null || typeof value === "boolean")
|
|
68
|
+
return value;
|
|
69
|
+
if (typeof value === "string")
|
|
70
|
+
return value.slice(0, MAX_METADATA_STRING);
|
|
71
|
+
if (typeof value === "number")
|
|
72
|
+
return Number.isFinite(value) ? value : undefined;
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const item of value) {
|
|
76
|
+
const normalized = toJsonValue(item, depth + 1, budget);
|
|
77
|
+
if (normalized !== undefined)
|
|
78
|
+
out.push(normalized);
|
|
79
|
+
if (budget.remaining <= 0)
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
if (!isRecord(value))
|
|
85
|
+
return undefined;
|
|
86
|
+
const out = {};
|
|
87
|
+
for (const [key, item] of Object.entries(value)) {
|
|
88
|
+
if (UNSAFE_METADATA_KEYS.has(key))
|
|
89
|
+
continue;
|
|
90
|
+
const normalized = toJsonValue(item, depth + 1, budget);
|
|
91
|
+
if (normalized !== undefined) {
|
|
92
|
+
Object.defineProperty(out, key.slice(0, 256), {
|
|
93
|
+
configurable: true,
|
|
94
|
+
enumerable: true,
|
|
95
|
+
value: normalized,
|
|
96
|
+
writable: true,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (budget.remaining <= 0)
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
function safeEnqueue(controller, part) {
|
|
105
|
+
try {
|
|
106
|
+
controller.enqueue(part);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function safeClose(controller) {
|
|
114
|
+
try {
|
|
115
|
+
controller.close();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Controller already closed or cancelled
|
|
119
|
+
}
|
|
120
|
+
}
|
|
12
121
|
function isRecord(v) {
|
|
13
122
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
14
123
|
}
|
|
15
124
|
function makeFinishReason(unified, raw) {
|
|
16
|
-
return {
|
|
125
|
+
return {
|
|
126
|
+
unified,
|
|
127
|
+
raw: raw === undefined ? undefined : redactSensitiveText(raw).slice(0, MAX_STOP_REASON_LENGTH),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function safeStopReason(value) {
|
|
131
|
+
return typeof value === "string"
|
|
132
|
+
? redactSensitiveText(value).slice(0, MAX_STOP_REASON_LENGTH)
|
|
133
|
+
: null;
|
|
17
134
|
}
|
|
18
135
|
function makeUsage(input, output, cacheRead, cacheWrite) {
|
|
136
|
+
const safeInput = finiteNonNegative(input);
|
|
137
|
+
const safeOutput = finiteNonNegative(output);
|
|
138
|
+
const safeCacheRead = Math.min(safeInput, finiteNonNegative(cacheRead));
|
|
139
|
+
const safeCacheWrite = Math.min(safeInput - safeCacheRead, finiteNonNegative(cacheWrite));
|
|
19
140
|
return {
|
|
20
141
|
inputTokens: {
|
|
21
|
-
total:
|
|
22
|
-
noCache: Math.max(0,
|
|
23
|
-
cacheRead,
|
|
24
|
-
cacheWrite,
|
|
142
|
+
total: safeInput,
|
|
143
|
+
noCache: Math.max(0, safeInput - safeCacheRead - safeCacheWrite),
|
|
144
|
+
cacheRead: safeCacheRead,
|
|
145
|
+
cacheWrite: safeCacheWrite,
|
|
25
146
|
},
|
|
26
147
|
outputTokens: {
|
|
27
|
-
total:
|
|
28
|
-
text:
|
|
148
|
+
total: safeOutput,
|
|
149
|
+
text: safeOutput,
|
|
29
150
|
reasoning: undefined,
|
|
30
151
|
},
|
|
31
152
|
};
|
|
32
153
|
}
|
|
154
|
+
function finiteNonNegative(value, fallback = 0) {
|
|
155
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
156
|
+
}
|
|
157
|
+
function requestTimeoutMs(value) {
|
|
158
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
|
|
159
|
+
return DEFAULT_REQUEST_TIMEOUT_MS;
|
|
160
|
+
return Math.min(MAX_REQUEST_TIMEOUT_MS, Math.max(1, Math.floor(value)));
|
|
161
|
+
}
|
|
162
|
+
function abortError() {
|
|
163
|
+
const error = new Error("Qoder request aborted");
|
|
164
|
+
error.name = "AbortError";
|
|
165
|
+
return error;
|
|
166
|
+
}
|
|
167
|
+
function safePublicError(error) {
|
|
168
|
+
if (error instanceof QoderAuthError || error instanceof QoderSdkResultError)
|
|
169
|
+
return error;
|
|
170
|
+
return new Error(describeError(error) || "Qoder request failed");
|
|
171
|
+
}
|
|
172
|
+
function tokenCount(value) {
|
|
173
|
+
return Math.floor(finiteNonNegative(value));
|
|
174
|
+
}
|
|
175
|
+
function ratio(value) {
|
|
176
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
177
|
+
? Math.min(1, value)
|
|
178
|
+
: undefined;
|
|
179
|
+
}
|
|
180
|
+
function safeJsonStringify(value) {
|
|
181
|
+
try {
|
|
182
|
+
return JSON.stringify(value);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function messageDedupeKey(message) {
|
|
189
|
+
const uuid = typeof message.uuid === "string" && message.uuid.trim() ? message.uuid : undefined;
|
|
190
|
+
if (!uuid)
|
|
191
|
+
return undefined;
|
|
192
|
+
// Some SDK versions reuse an outer UUID for several stream frames, while
|
|
193
|
+
// replayed frames repeat the complete payload. Hashing the payload keeps
|
|
194
|
+
// the latter idempotent without dropping legitimate same-UUID frames.
|
|
195
|
+
const payload = safeJsonStringify(message) ?? `${message.type ?? ""}:${message.event ?? ""}`;
|
|
196
|
+
return `${uuid}\u0000${createHash("sha256").update(payload).digest("hex")}`;
|
|
197
|
+
}
|
|
198
|
+
function rememberId(seen, id, maxSize) {
|
|
199
|
+
if (seen.has(id))
|
|
200
|
+
return false;
|
|
201
|
+
if (seen.size >= maxSize) {
|
|
202
|
+
const oldest = seen.values().next().value;
|
|
203
|
+
if (typeof oldest === "string")
|
|
204
|
+
seen.delete(oldest);
|
|
205
|
+
}
|
|
206
|
+
seen.add(id);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
function isAuthenticationError(value) {
|
|
210
|
+
if (typeof value !== "string")
|
|
211
|
+
return false;
|
|
212
|
+
return /auth|credential|token|unauthori|forbidden/i.test(value);
|
|
213
|
+
}
|
|
214
|
+
function resolveCwd(value) {
|
|
215
|
+
if (typeof value !== "string" || !value.trim())
|
|
216
|
+
return process.cwd();
|
|
217
|
+
try {
|
|
218
|
+
const resolved = resolve(value);
|
|
219
|
+
try {
|
|
220
|
+
return realpathSync(resolved);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return resolved;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return process.cwd();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function qoderEnvironment(environment) {
|
|
231
|
+
return environment ? { ...process.env, ...environment } : process.env;
|
|
232
|
+
}
|
|
233
|
+
const sessionTails = new Map();
|
|
234
|
+
function waitForTurn(previous, signal) {
|
|
235
|
+
if (!signal)
|
|
236
|
+
return previous.then(() => true, () => true);
|
|
237
|
+
if (signal.aborted)
|
|
238
|
+
return Promise.resolve(false);
|
|
239
|
+
return new Promise((resolveResult) => {
|
|
240
|
+
let settled = false;
|
|
241
|
+
const finish = (ready) => {
|
|
242
|
+
if (settled)
|
|
243
|
+
return;
|
|
244
|
+
settled = true;
|
|
245
|
+
signal.removeEventListener("abort", onAbort);
|
|
246
|
+
resolveResult(ready);
|
|
247
|
+
};
|
|
248
|
+
const onAbort = () => finish(false);
|
|
249
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
250
|
+
previous.then(() => finish(true), () => finish(true));
|
|
251
|
+
if (signal.aborted)
|
|
252
|
+
finish(false);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async function nextWithAbort(iterator, signal) {
|
|
256
|
+
if (signal.aborted)
|
|
257
|
+
return undefined;
|
|
258
|
+
let onAbort;
|
|
259
|
+
const aborted = new Promise((resolveAbort) => {
|
|
260
|
+
onAbort = () => resolveAbort(undefined);
|
|
261
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
262
|
+
});
|
|
263
|
+
try {
|
|
264
|
+
return await Promise.race([iterator.next(), aborted]);
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
signal.removeEventListener("abort", onAbort);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async function withSessionLock(key, signal, fn) {
|
|
271
|
+
if (!key) {
|
|
272
|
+
if (signal?.aborted)
|
|
273
|
+
return undefined;
|
|
274
|
+
return fn();
|
|
275
|
+
}
|
|
276
|
+
const previous = sessionTails.get(key) ?? Promise.resolve();
|
|
277
|
+
let release;
|
|
278
|
+
const current = new Promise((resolveRelease) => { release = resolveRelease; });
|
|
279
|
+
sessionTails.set(key, current);
|
|
280
|
+
const acquired = await waitForTurn(previous, signal);
|
|
281
|
+
const releaseAndClean = () => {
|
|
282
|
+
release();
|
|
283
|
+
if (sessionTails.get(key) === current)
|
|
284
|
+
sessionTails.delete(key);
|
|
285
|
+
};
|
|
286
|
+
if (!acquired) {
|
|
287
|
+
// Keep the queue closed behind the still-running request, even when this
|
|
288
|
+
// waiter is canceled before it reaches the SDK.
|
|
289
|
+
void previous.then(releaseAndClean, releaseAndClean);
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
if (signal?.aborted)
|
|
294
|
+
return undefined;
|
|
295
|
+
return await fn();
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
releaseAndClean();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
33
301
|
function mapStopReason(stopReason, hasToolCalls) {
|
|
34
302
|
if (hasToolCalls)
|
|
35
303
|
return makeFinishReason("tool-calls", stopReason ?? undefined);
|
|
@@ -42,9 +310,33 @@ function mapStopReason(stopReason, hasToolCalls) {
|
|
|
42
310
|
return makeFinishReason("stop", stopReason ?? undefined);
|
|
43
311
|
}
|
|
44
312
|
}
|
|
313
|
+
function trackOpenBlock(state, block) {
|
|
314
|
+
if (!state.openBlocks.some((item) => item.index === block.index))
|
|
315
|
+
state.openBlocks.push(block);
|
|
316
|
+
}
|
|
317
|
+
function untrackOpenBlock(state, index) {
|
|
318
|
+
const position = state.openBlocks.findIndex((item) => item.index === index);
|
|
319
|
+
if (position >= 0)
|
|
320
|
+
state.openBlocks.splice(position, 1);
|
|
321
|
+
}
|
|
45
322
|
export function isProviderExecutedTool(name, functionToolNames) {
|
|
46
323
|
return !functionToolNames.has(name);
|
|
47
324
|
}
|
|
325
|
+
function isProviderOwnedTool(rawName, normalizedName, functionToolNames) {
|
|
326
|
+
// Bridged MCP servers are executed by Qoder. Do not normalize an MCP name
|
|
327
|
+
// into an unrelated OpenCode function and execute the same call twice.
|
|
328
|
+
if (rawName.trim().toLowerCase().startsWith("mcp__"))
|
|
329
|
+
return true;
|
|
330
|
+
return isProviderExecutedTool(normalizedName, functionToolNames);
|
|
331
|
+
}
|
|
332
|
+
function qoderToolNameForHost(rawName) {
|
|
333
|
+
const trimmed = rawName.trim();
|
|
334
|
+
const normalized = normalizeToolName(trimmed);
|
|
335
|
+
return QODER_BUILTIN_NAMES[normalized] ?? trimmed;
|
|
336
|
+
}
|
|
337
|
+
function isProviderOwnedToolName(name) {
|
|
338
|
+
return name.trim().toLowerCase().startsWith("mcp__");
|
|
339
|
+
}
|
|
48
340
|
export class QoderLanguageModel {
|
|
49
341
|
specificationVersion = "v3";
|
|
50
342
|
provider = "qoder";
|
|
@@ -56,89 +348,150 @@ export class QoderLanguageModel {
|
|
|
56
348
|
this.bridgeOptions = bridgeOptions;
|
|
57
349
|
}
|
|
58
350
|
async doGenerate(options) {
|
|
351
|
+
if (options.abortSignal?.aborted)
|
|
352
|
+
throw abortError();
|
|
59
353
|
const { stream } = await this.doStream(options);
|
|
60
354
|
const reader = stream.getReader();
|
|
61
355
|
let text = "";
|
|
62
356
|
let reasoning = "";
|
|
63
357
|
let finishReason = makeFinishReason("stop");
|
|
64
358
|
let usage = makeUsage(0, 0, 0, 0);
|
|
359
|
+
let providerMetadata;
|
|
360
|
+
let sawFinish = false;
|
|
65
361
|
const toolCalls = [];
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
switch (value.type) {
|
|
71
|
-
case "text-delta":
|
|
72
|
-
text += value.delta;
|
|
362
|
+
try {
|
|
363
|
+
for (;;) {
|
|
364
|
+
const { value, done } = await reader.read();
|
|
365
|
+
if (done)
|
|
73
366
|
break;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
367
|
+
switch (value.type) {
|
|
368
|
+
case "text-delta":
|
|
369
|
+
text += value.delta;
|
|
370
|
+
break;
|
|
371
|
+
case "reasoning-delta":
|
|
372
|
+
reasoning += value.delta;
|
|
373
|
+
break;
|
|
374
|
+
case "tool-call":
|
|
375
|
+
toolCalls.push({
|
|
376
|
+
type: "tool-call",
|
|
377
|
+
toolCallId: value.toolCallId,
|
|
378
|
+
toolName: value.toolName,
|
|
379
|
+
input: value.input,
|
|
380
|
+
});
|
|
381
|
+
break;
|
|
382
|
+
case "finish":
|
|
383
|
+
sawFinish = true;
|
|
384
|
+
finishReason = value.finishReason;
|
|
385
|
+
usage = value.usage;
|
|
386
|
+
providerMetadata = value.providerMetadata;
|
|
387
|
+
break;
|
|
388
|
+
case "error":
|
|
389
|
+
throw safePublicError(value.error);
|
|
390
|
+
}
|
|
91
391
|
}
|
|
92
392
|
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
try {
|
|
395
|
+
await reader.cancel();
|
|
396
|
+
}
|
|
397
|
+
catch { /* best-effort stream cleanup */ }
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
try {
|
|
402
|
+
reader.releaseLock();
|
|
403
|
+
}
|
|
404
|
+
catch { /* reader may already be released */ }
|
|
405
|
+
}
|
|
406
|
+
if (!sawFinish) {
|
|
407
|
+
if (options.abortSignal?.aborted)
|
|
408
|
+
throw abortError();
|
|
409
|
+
throw new QoderSdkResultError("incomplete_stream", "Qoder stream closed before sending a finish message");
|
|
410
|
+
}
|
|
93
411
|
const content = [];
|
|
94
412
|
if (reasoning)
|
|
95
413
|
content.push({ type: "reasoning", text: reasoning });
|
|
96
414
|
if (text)
|
|
97
415
|
content.push({ type: "text", text });
|
|
98
416
|
content.push(...toolCalls);
|
|
99
|
-
return { content, finishReason, usage, warnings: [] };
|
|
417
|
+
return { content, finishReason, usage, providerMetadata, warnings: [] };
|
|
100
418
|
}
|
|
101
419
|
async doStream(options) {
|
|
102
420
|
const cli = findQoderCLI();
|
|
103
|
-
|
|
421
|
+
const childEnvironment = qoderEnvironment(this.bridgeOptions.env);
|
|
422
|
+
if (!hasQoderCredential(childEnvironment)) {
|
|
104
423
|
throw new QoderAuthError("No Qoder credentials found. Run `qoder login` or set QODER_PERSONAL_ACCESS_TOKEN.");
|
|
105
424
|
}
|
|
106
|
-
const resolved = getModel(this.modelId)
|
|
107
|
-
|
|
108
|
-
|
|
425
|
+
const resolved = getModel(this.modelId);
|
|
426
|
+
const model = resolved ?? getModel(DEFAULT_MODEL_ID);
|
|
427
|
+
if (!resolved) {
|
|
428
|
+
// Use the default catalog entry only for conservative prompt limits.
|
|
429
|
+
// Preserve the requested ID on the SDK call so an unknown model is not
|
|
430
|
+
// silently replaced by a different model.
|
|
431
|
+
debug(`Unknown model id "${this.modelId}"; forwarding it with default prompt limits`);
|
|
109
432
|
}
|
|
110
|
-
const model = resolved;
|
|
111
433
|
const sessionKey = this.bridgeOptions.sessionKey ?? this.bridgeOptions.sessionId;
|
|
112
|
-
const
|
|
113
|
-
? await getQoderSession(sessionKey)
|
|
114
|
-
: null;
|
|
115
|
-
const sessionId = this.bridgeOptions.sessionId ?? persisted?.qoderSessionId ?? randomUUID();
|
|
116
|
-
const shouldResume = Boolean(this.bridgeOptions.sessionId || persisted);
|
|
117
|
-
debug(`doStream model=${model.id} sessionId=${sessionId} resume=${shouldResume}`);
|
|
118
|
-
const promptMessages = options.prompt;
|
|
119
|
-
const promptInput = shouldResume
|
|
120
|
-
? latestPrompt(promptMessages)
|
|
121
|
-
: promptMessages;
|
|
122
|
-
const prompt = promptHasImage(promptInput)
|
|
123
|
-
? buildPromptIterable(promptInput, model.limit.context, sessionId)
|
|
124
|
-
: buildPromptString(promptInput, model.limit.context);
|
|
434
|
+
const cwd = resolveCwd(this.bridgeOptions.cwd);
|
|
125
435
|
const functionToolNames = new Set((options.tools ?? [])
|
|
126
436
|
.filter((t) => t.type === "function")
|
|
127
437
|
.map((t) => normalizeToolName(t.name)));
|
|
438
|
+
const hostToolNames = (options.tools ?? [])
|
|
439
|
+
.filter((tool) => tool.type === "function")
|
|
440
|
+
// MCP tools configured through mcpServers are executed by Qoder. They
|
|
441
|
+
// are provider-owned and must not be added to the native denylist.
|
|
442
|
+
.filter((tool) => !isProviderOwnedToolName(tool.name))
|
|
443
|
+
.map((tool) => qoderToolNameForHost(tool.name));
|
|
128
444
|
const abortController = new AbortController();
|
|
129
445
|
let qoderQuery = null;
|
|
130
|
-
let cleaned = false;
|
|
131
446
|
let externallyAborted = false;
|
|
447
|
+
let timedOut = false;
|
|
448
|
+
let requestTimer;
|
|
449
|
+
let cleanupPromise;
|
|
450
|
+
const timeoutMs = requestTimeoutMs(this.bridgeOptions.timeoutMs);
|
|
451
|
+
const timeoutError = () => new QoderSdkResultError("timeout", `Qoder request exceeded ${timeoutMs}ms`);
|
|
132
452
|
const cleanup = () => {
|
|
133
|
-
if (
|
|
134
|
-
return;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
453
|
+
if (cleanupPromise)
|
|
454
|
+
return cleanupPromise;
|
|
455
|
+
if (requestTimer)
|
|
456
|
+
clearTimeout(requestTimer);
|
|
457
|
+
if (options.abortSignal) {
|
|
458
|
+
try {
|
|
459
|
+
options.abortSignal.removeEventListener("abort", markExternal);
|
|
460
|
+
}
|
|
461
|
+
catch { /* ignore */ }
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
abortController.abort();
|
|
465
|
+
}
|
|
466
|
+
catch { /* best-effort cancellation */ }
|
|
467
|
+
const activeQuery = qoderQuery;
|
|
468
|
+
cleanupPromise = (async () => {
|
|
469
|
+
if (!activeQuery)
|
|
470
|
+
return;
|
|
471
|
+
let graceTimer;
|
|
472
|
+
try {
|
|
473
|
+
await Promise.race([
|
|
474
|
+
activeQuery.return(undefined).then(() => undefined, () => undefined),
|
|
475
|
+
new Promise((resolveCleanup) => {
|
|
476
|
+
graceTimer = setTimeout(resolveCleanup, CLEANUP_GRACE_MS);
|
|
477
|
+
if (typeof graceTimer.unref === "function")
|
|
478
|
+
graceTimer.unref();
|
|
479
|
+
}),
|
|
480
|
+
]);
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
// A transport may reject/throw while it is being torn down.
|
|
484
|
+
}
|
|
485
|
+
finally {
|
|
486
|
+
if (graceTimer)
|
|
487
|
+
clearTimeout(graceTimer);
|
|
488
|
+
}
|
|
489
|
+
})();
|
|
490
|
+
return cleanupPromise;
|
|
138
491
|
};
|
|
139
492
|
const markExternal = () => {
|
|
140
493
|
externallyAborted = true;
|
|
141
|
-
cleanup();
|
|
494
|
+
void cleanup();
|
|
142
495
|
};
|
|
143
496
|
if (options.abortSignal) {
|
|
144
497
|
if (options.abortSignal.aborted)
|
|
@@ -146,7 +499,6 @@ export class QoderLanguageModel {
|
|
|
146
499
|
else
|
|
147
500
|
options.abortSignal.addEventListener("abort", markExternal, { once: true });
|
|
148
501
|
}
|
|
149
|
-
const qoderOptions = this.buildQueryOptions(cli, sessionId, abortController, shouldResume);
|
|
150
502
|
const stream = new ReadableStream({
|
|
151
503
|
cancel: markExternal,
|
|
152
504
|
start: async (controller) => {
|
|
@@ -157,6 +509,7 @@ export class QoderLanguageModel {
|
|
|
157
509
|
activeText: new Set(),
|
|
158
510
|
activeReasoning: new Set(),
|
|
159
511
|
toolBlocks: new Map(),
|
|
512
|
+
openBlocks: [],
|
|
160
513
|
sawStreamText: false,
|
|
161
514
|
sawStreamTool: false,
|
|
162
515
|
sawStreamReasoning: false,
|
|
@@ -164,82 +517,235 @@ export class QoderLanguageModel {
|
|
|
164
517
|
pendingToolCalls: new Map(),
|
|
165
518
|
lastStopReason: null,
|
|
166
519
|
blockCounter: 0,
|
|
520
|
+
outputChars: 0,
|
|
167
521
|
finished: false,
|
|
522
|
+
resultReceived: false,
|
|
523
|
+
seenToolCallIds: new Set(),
|
|
524
|
+
seenMessageIds: new Set(),
|
|
525
|
+
artifacts: [],
|
|
526
|
+
planMode: undefined,
|
|
527
|
+
skillEvolution: undefined,
|
|
168
528
|
};
|
|
169
|
-
controller
|
|
529
|
+
safeEnqueue(controller, { type: "stream-start", warnings: [] });
|
|
170
530
|
try {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
531
|
+
if (externallyAborted) {
|
|
532
|
+
safeClose(controller);
|
|
533
|
+
return;
|
|
174
534
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
535
|
+
requestTimer = setTimeout(() => {
|
|
536
|
+
timedOut = true;
|
|
537
|
+
debug(`Qoder request timed out after ${timeoutMs}ms`);
|
|
538
|
+
void cleanup();
|
|
539
|
+
}, timeoutMs);
|
|
540
|
+
if (typeof requestTimer.unref === "function")
|
|
541
|
+
requestTimer.unref();
|
|
542
|
+
const lockKey = sessionKey ? `${cwd}\u0000${sessionKey}` : undefined;
|
|
543
|
+
const leaseKey = [this.bridgeOptions.sessionId, sessionKey]
|
|
544
|
+
.find((value) => typeof value === "string" && value.trim().length > 0);
|
|
545
|
+
const runTurn = () => withSessionLock(lockKey, abortController.signal, async () => {
|
|
546
|
+
let persisted = null;
|
|
547
|
+
let resetEpoch;
|
|
548
|
+
try {
|
|
549
|
+
if (externallyAborted)
|
|
550
|
+
return;
|
|
551
|
+
if (this.bridgeOptions.sessionPersistence && sessionKey) {
|
|
552
|
+
resetEpoch = await getQoderSessionResetEpoch();
|
|
553
|
+
try {
|
|
554
|
+
persisted = await getQoderSessionForCwd(sessionKey, cwd);
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
// Persistence contention or corruption must not make a fresh
|
|
558
|
+
// chat turn unavailable; the mapping can be repaired later.
|
|
559
|
+
debug("Could not read Qoder session mapping; starting fresh:", describeError(error));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const sessionId = this.bridgeOptions.sessionId ?? persisted?.qoderSessionId ?? randomUUID();
|
|
563
|
+
const shouldResume = Boolean(this.bridgeOptions.sessionId || persisted);
|
|
564
|
+
debug(`doStream model=${model.id} sessionId=${sessionId} cwd=${cwd} resume=${shouldResume}`);
|
|
565
|
+
const promptMessages = options.prompt;
|
|
566
|
+
const promptInput = shouldResume ? latestPrompt(promptMessages) : promptMessages;
|
|
567
|
+
const prompt = promptHasImage(promptInput)
|
|
568
|
+
? buildPromptIterable(promptInput, model.limit.context, sessionId)
|
|
569
|
+
: buildPromptString(promptInput, model.limit.context);
|
|
570
|
+
const qoderOptions = this.buildQueryOptions(cli, sessionId, abortController, shouldResume, this.modelId, cwd, hostToolNames, () => {
|
|
571
|
+
state.authExpired = true;
|
|
572
|
+
try {
|
|
573
|
+
abortController.abort();
|
|
574
|
+
}
|
|
575
|
+
catch { /* best-effort cancellation */ }
|
|
576
|
+
});
|
|
577
|
+
if (externallyAborted || abortController.signal.aborted)
|
|
578
|
+
return;
|
|
579
|
+
qoderQuery = query({ prompt, options: qoderOptions });
|
|
580
|
+
const activeQuery = qoderQuery;
|
|
581
|
+
if (externallyAborted || abortController.signal.aborted)
|
|
582
|
+
return;
|
|
583
|
+
const iterator = activeQuery[Symbol.asyncIterator]();
|
|
584
|
+
for (;;) {
|
|
585
|
+
const next = await nextWithAbort(iterator, abortController.signal);
|
|
586
|
+
if (!next || next.done)
|
|
587
|
+
break;
|
|
588
|
+
if (externallyAborted || timedOut)
|
|
589
|
+
break;
|
|
590
|
+
handleSdkMessage(next.value, state);
|
|
591
|
+
if (state.authExpired || state.finished)
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
if (state.authExpired && !state.finished) {
|
|
595
|
+
throw new QoderAuthError("Qoder authentication expired during the request. Re-authenticate with `qoder login` or refresh QODER_PERSONAL_ACCESS_TOKEN.");
|
|
596
|
+
}
|
|
597
|
+
if (timedOut)
|
|
598
|
+
throw timeoutError();
|
|
599
|
+
if (!externallyAborted && !state.resultReceived && !state.finished) {
|
|
600
|
+
throw new QoderSdkResultError("incomplete_stream", "Qoder ended the stream before sending a result message");
|
|
601
|
+
}
|
|
602
|
+
if (!externallyAborted && !state.failed && !abortController.signal.aborted && this.bridgeOptions.sessionPersistence && sessionKey) {
|
|
603
|
+
try {
|
|
604
|
+
await ensureQoderSession(sessionKey, sessionId, cwd, resetEpoch);
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
debug("Could not persist Qoder session mapping:", describeError(error));
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (state.invalidSession && persisted && sessionKey) {
|
|
611
|
+
try {
|
|
612
|
+
await deleteQoderSession(sessionKey, cwd);
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
debug("Could not clear invalid Qoder session mapping:", describeError(error));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
// A transport/query failure may arrive before Qoder can emit a
|
|
621
|
+
// structured result. Clear a persisted session when its error
|
|
622
|
+
// still identifies the resume target as invalid, so the next
|
|
623
|
+
// request can recover with a fresh Qoder session.
|
|
624
|
+
if (persisted && sessionKey && this.bridgeOptions.sessionPersistence && isLikelyInvalidSessionError(error)) {
|
|
625
|
+
try {
|
|
626
|
+
await deleteQoderSession(sessionKey, cwd);
|
|
627
|
+
}
|
|
628
|
+
catch (deleteError) {
|
|
629
|
+
debug("Could not clear invalid Qoder session mapping after transport failure:", describeError(deleteError));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
throw error;
|
|
633
|
+
}
|
|
634
|
+
finally {
|
|
635
|
+
await cleanup();
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
if (leaseKey) {
|
|
639
|
+
await withQoderSessionLease(leaseKey, cwd, abortController.signal, runTurn);
|
|
179
640
|
}
|
|
180
|
-
|
|
641
|
+
else {
|
|
642
|
+
await runTurn();
|
|
643
|
+
}
|
|
644
|
+
if (!state.finished && !externallyAborted) {
|
|
645
|
+
if (timedOut)
|
|
646
|
+
throw timeoutError();
|
|
647
|
+
if (!state.resultReceived) {
|
|
648
|
+
throw new QoderSdkResultError("incomplete_stream", "Qoder ended the stream before sending a result message");
|
|
649
|
+
}
|
|
181
650
|
emitFinish(state, makeUsage(0, 0, 0, 0), makeFinishReason("stop"), undefined);
|
|
182
651
|
}
|
|
183
|
-
cleanup();
|
|
184
|
-
controller
|
|
652
|
+
await cleanup();
|
|
653
|
+
safeClose(controller);
|
|
185
654
|
}
|
|
186
655
|
catch (err) {
|
|
187
|
-
cleanup();
|
|
188
|
-
if (externallyAborted
|
|
656
|
+
await cleanup();
|
|
657
|
+
if (externallyAborted) {
|
|
189
658
|
debug("Stream aborted by caller; closing without finish");
|
|
190
|
-
controller
|
|
659
|
+
safeClose(controller);
|
|
191
660
|
return;
|
|
192
661
|
}
|
|
193
662
|
debug("Stream failed:", describeError(err));
|
|
194
663
|
if (!state.finished) {
|
|
195
|
-
|
|
664
|
+
const streamError = state.authExpired
|
|
665
|
+
? new QoderAuthError("Qoder authentication expired during the request. Re-authenticate with `qoder login` or refresh QODER_PERSONAL_ACCESS_TOKEN.")
|
|
666
|
+
: timedOut ? timeoutError()
|
|
667
|
+
: safePublicError(err);
|
|
668
|
+
closeOpenBlocks(state);
|
|
669
|
+
safeEnqueue(controller, { type: "error", error: streamError });
|
|
196
670
|
emitFinish(state, makeUsage(0, 0, 0, 0), makeFinishReason("error"), undefined);
|
|
197
671
|
}
|
|
198
|
-
controller
|
|
672
|
+
safeClose(controller);
|
|
199
673
|
}
|
|
200
674
|
},
|
|
201
675
|
});
|
|
202
676
|
return { stream };
|
|
203
677
|
}
|
|
204
|
-
buildQueryOptions(cli, sessionId, abortController, shouldResume) {
|
|
678
|
+
buildQueryOptions(cli, sessionId, abortController, shouldResume, modelId = this.modelId, cwd = resolveCwd(this.bridgeOptions.cwd), hostToolNames = [], onAuthExpired) {
|
|
205
679
|
const sessionKey = this.bridgeOptions.sessionKey ?? this.bridgeOptions.sessionId;
|
|
206
680
|
const permissionMode = this.bridgeOptions.permissionMode ?? "default";
|
|
207
681
|
const opts = {
|
|
208
|
-
auth: qoderAuth(),
|
|
209
|
-
model:
|
|
682
|
+
auth: qoderAuth(qoderEnvironment(this.bridgeOptions.env)),
|
|
683
|
+
model: modelId,
|
|
210
684
|
allowDangerouslySkipPermissions: this.bridgeOptions.allowDangerouslySkipPermissions
|
|
211
685
|
?? (permissionMode === "bypassPermissions" ? true : undefined),
|
|
212
686
|
permissionMode,
|
|
213
687
|
includePartialMessages: true,
|
|
214
688
|
sessionId,
|
|
215
|
-
cwd
|
|
689
|
+
cwd,
|
|
216
690
|
abortController,
|
|
217
691
|
};
|
|
692
|
+
if (onAuthExpired)
|
|
693
|
+
opts.onAuthExpired = onAuthExpired;
|
|
218
694
|
if (cli)
|
|
219
695
|
opts.pathToQoderCLIExecutable = cli;
|
|
220
696
|
if (this.bridgeOptions.env)
|
|
221
|
-
opts.env = this.bridgeOptions.env;
|
|
222
|
-
if (
|
|
697
|
+
opts.env = qoderEnvironment(this.bridgeOptions.env);
|
|
698
|
+
if (this.bridgeOptions.planMode !== undefined)
|
|
699
|
+
opts.planMode = this.bridgeOptions.planMode;
|
|
700
|
+
const proxy = this.bridgeOptions.proxy ?? process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY;
|
|
701
|
+
if (proxy)
|
|
702
|
+
opts.proxy = proxy;
|
|
703
|
+
if (this.bridgeOptions.evolution)
|
|
704
|
+
opts.evolution = this.bridgeOptions.evolution;
|
|
705
|
+
const persistSession = Boolean(this.bridgeOptions.sessionId || (this.bridgeOptions.sessionPersistence && sessionKey));
|
|
706
|
+
opts.persistSession = persistSession;
|
|
707
|
+
if (persistSession && shouldResume)
|
|
223
708
|
opts.resume = sessionId;
|
|
224
|
-
opts.persistSession = true;
|
|
225
|
-
}
|
|
226
709
|
if (this.bridgeOptions.allowedTools)
|
|
227
710
|
opts.allowedTools = this.bridgeOptions.allowedTools;
|
|
228
|
-
|
|
229
|
-
|
|
711
|
+
const disallowedTools = [
|
|
712
|
+
...(this.bridgeOptions.disallowedTools ?? []),
|
|
713
|
+
...hostToolNames
|
|
714
|
+
.filter((name) => !isProviderOwnedToolName(name))
|
|
715
|
+
.map(qoderToolNameForHost),
|
|
716
|
+
]
|
|
717
|
+
.filter((name, index, all) => typeof name === "string" && name.trim() && all.indexOf(name) === index);
|
|
718
|
+
if (disallowedTools.length > 0)
|
|
719
|
+
opts.disallowedTools = disallowedTools;
|
|
230
720
|
const mcpServers = this.bridgeOptions.mcpServers;
|
|
231
721
|
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
232
722
|
opts.mcpServers = mcpServers;
|
|
233
723
|
}
|
|
234
724
|
if (this.bridgeOptions.extraArgs && Object.keys(this.bridgeOptions.extraArgs).length > 0) {
|
|
235
|
-
opts.extraArgs = this.bridgeOptions.extraArgs;
|
|
725
|
+
opts.extraArgs = Object.fromEntries(Object.entries(this.bridgeOptions.extraArgs).map(([key, value]) => [key.replace(/^--/, ""), value]));
|
|
236
726
|
}
|
|
237
727
|
return opts;
|
|
238
728
|
}
|
|
239
729
|
}
|
|
240
730
|
export function handleSdkMessage(m, state) {
|
|
241
|
-
|
|
731
|
+
if (state.finished || !isRecord(m))
|
|
732
|
+
return;
|
|
733
|
+
const messageId = messageDedupeKey(m);
|
|
734
|
+
if (messageId) {
|
|
735
|
+
const seenMessageIds = state.seenMessageIds ??= new Set();
|
|
736
|
+
if (!rememberId(seenMessageIds, messageId, MAX_SEEN_MESSAGE_IDS))
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const type = typeof m.type === "string" ? m.type : "";
|
|
740
|
+
if (!type.trim()) {
|
|
741
|
+
failStream(state, "malformed_stream", "Qoder sent a message without a type");
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
242
744
|
if (type === "stream_event") {
|
|
745
|
+
if (!isRecord(m.event)) {
|
|
746
|
+
failStream(state, "malformed_stream", "Qoder sent a stream message without an event");
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
243
749
|
handleStreamEvent(m.event, state);
|
|
244
750
|
}
|
|
245
751
|
else if (type === "assistant") {
|
|
@@ -248,151 +754,333 @@ export function handleSdkMessage(m, state) {
|
|
|
248
754
|
else if (type === "result") {
|
|
249
755
|
handleResult(m, state);
|
|
250
756
|
}
|
|
757
|
+
else if (type === "system") {
|
|
758
|
+
handleSystem(m, state);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
function handleSystem(m, state) {
|
|
762
|
+
const subtype = typeof m.subtype === "string" ? m.subtype : "";
|
|
763
|
+
if (subtype === "plan_mode_changed" && isRecord(m.plan_mode)) {
|
|
764
|
+
if (typeof m.plan_mode.active !== "boolean")
|
|
765
|
+
return;
|
|
766
|
+
state.planMode = m.plan_mode;
|
|
767
|
+
debug(`Plan mode changed: active=${state.planMode.active}`);
|
|
768
|
+
}
|
|
769
|
+
else if (subtype === "available_models_update" && Array.isArray(m.models)) {
|
|
770
|
+
debug(`Received live available_models_update with ${m.models.length} models`);
|
|
771
|
+
applyLiveModelUpdates(m.models);
|
|
772
|
+
}
|
|
773
|
+
else if (subtype === "artifacts_update" && Array.isArray(m.artifacts)) {
|
|
774
|
+
const incoming = m.artifacts.slice(0, 1_000);
|
|
775
|
+
for (const artifact of incoming) {
|
|
776
|
+
if (!isRecord(artifact))
|
|
777
|
+
continue;
|
|
778
|
+
const safeArtifact = toJsonValue(artifact);
|
|
779
|
+
if (!isRecord(safeArtifact))
|
|
780
|
+
continue;
|
|
781
|
+
const path = typeof safeArtifact.path === "string" ? safeArtifact.path : "";
|
|
782
|
+
const index = path ? state.artifacts.findIndex((item) => item.path === path) : -1;
|
|
783
|
+
if (index >= 0)
|
|
784
|
+
state.artifacts[index] = safeArtifact;
|
|
785
|
+
else if (state.artifacts.length < 1000)
|
|
786
|
+
state.artifacts.push(safeArtifact);
|
|
787
|
+
}
|
|
788
|
+
debug(`Artifacts updated: ${incoming.length} artifact(s)`);
|
|
789
|
+
}
|
|
790
|
+
else if (subtype === "skill_evolution" && isRecord(m.result)) {
|
|
791
|
+
state.skillEvolution = m.result;
|
|
792
|
+
debug(`Skill evolution result: status=${m.result.status}`);
|
|
793
|
+
}
|
|
251
794
|
}
|
|
252
795
|
function handleStreamEvent(ev, state) {
|
|
253
|
-
if (!ev)
|
|
796
|
+
if (!isRecord(ev)) {
|
|
797
|
+
failStream(state, "malformed_stream", "Qoder sent a stream event that was not an object");
|
|
254
798
|
return;
|
|
799
|
+
}
|
|
255
800
|
const { controller } = state;
|
|
256
|
-
const evType = ev.type;
|
|
257
|
-
|
|
801
|
+
const evType = typeof ev.type === "string" ? ev.type : "";
|
|
802
|
+
if (!evType.trim()) {
|
|
803
|
+
failStream(state, "malformed_stream", "Qoder sent a stream event without a type");
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const isContentBlockEvent = evType === "content_block_start"
|
|
807
|
+
|| evType === "content_block_delta"
|
|
808
|
+
|| evType === "content_block_stop";
|
|
809
|
+
const idx = typeof ev.index === "number" && Number.isInteger(ev.index) && ev.index >= 0 && ev.index < 100_000
|
|
810
|
+
? ev.index
|
|
811
|
+
: -1;
|
|
812
|
+
if (isContentBlockEvent && idx < 0) {
|
|
813
|
+
failStream(state, "malformed_stream", "Qoder sent a content block event without a valid index");
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (evType === "content_block_start" && !isRecord(ev.content_block)) {
|
|
817
|
+
failStream(state, "malformed_stream", "Qoder sent a content block start without a block");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
258
820
|
if (evType === "content_block_start" && isRecord(ev.content_block)) {
|
|
821
|
+
if (state.toolBlocks.has(idx)) {
|
|
822
|
+
const existing = state.toolBlocks.get(idx);
|
|
823
|
+
if (existing && ev.content_block.id === existing.id)
|
|
824
|
+
return;
|
|
825
|
+
failStream(state, "malformed_stream", "Qoder started a different tool block at an open index");
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (state.activeReasoning.has(idx) || state.activeText.has(idx)) {
|
|
829
|
+
failStream(state, "malformed_stream", "Qoder started a content block that was already open");
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
259
832
|
const block = ev.content_block;
|
|
260
|
-
const blockType = block.type;
|
|
261
|
-
if (blockType === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
|
|
833
|
+
const blockType = typeof block.type === "string" ? block.type : "";
|
|
834
|
+
if (blockType === "tool_use" && typeof block.id === "string" && block.id.trim() && typeof block.name === "string" && block.name.trim()) {
|
|
835
|
+
const seenToolCallIds = state.seenToolCallIds ??= new Set();
|
|
836
|
+
if (!rememberId(seenToolCallIds, block.id, MAX_SEEN_MESSAGE_IDS) || state.toolBlocks.has(idx))
|
|
837
|
+
return;
|
|
262
838
|
state.sawStreamTool = true;
|
|
263
839
|
const name = normalizeToolName(block.name);
|
|
264
|
-
const providerExecuted =
|
|
265
|
-
|
|
840
|
+
const providerExecuted = isProviderOwnedTool(block.name, name, state.functionToolNames);
|
|
841
|
+
const hasInput = Object.hasOwn(block, "input");
|
|
842
|
+
const initialInput = hasInput ? safeJsonStringify(block.input) : "";
|
|
843
|
+
if (hasInput && (initialInput === undefined || initialInput.length > MAX_TOOL_INPUT_CHARS)) {
|
|
844
|
+
failStream(state, "malformed_tool_input", `Qoder sent unserializable input for tool ${name}`);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
state.toolBlocks.set(idx, { id: block.id, name, input: initialInput ?? "", providerExecuted, hasInput });
|
|
848
|
+
trackOpenBlock(state, { kind: "tool", index: idx, id: block.id, providerExecuted });
|
|
266
849
|
if (!providerExecuted) {
|
|
267
|
-
controller
|
|
850
|
+
safeEnqueue(controller, { type: "tool-input-start", id: block.id, toolName: name });
|
|
268
851
|
}
|
|
269
852
|
}
|
|
853
|
+
else if (blockType === "tool_use") {
|
|
854
|
+
failStream(state, "malformed_tool_call", "Qoder sent a tool call without a valid id or name");
|
|
855
|
+
}
|
|
270
856
|
else if (blockType === "thinking") {
|
|
271
857
|
state.activeReasoning.add(idx);
|
|
272
|
-
|
|
858
|
+
trackOpenBlock(state, { kind: "reasoning", index: idx });
|
|
859
|
+
safeEnqueue(controller, { type: "reasoning-start", id: String(idx) });
|
|
273
860
|
}
|
|
274
861
|
else if (blockType === "text") {
|
|
275
862
|
state.activeText.add(idx);
|
|
276
|
-
|
|
863
|
+
trackOpenBlock(state, { kind: "text", index: idx });
|
|
864
|
+
safeEnqueue(controller, { type: "text-start", id: String(idx) });
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
failStream(state, "malformed_stream", `Qoder sent an unsupported content block type: ${blockType || "missing"}`);
|
|
277
868
|
}
|
|
278
869
|
return;
|
|
279
870
|
}
|
|
280
871
|
if (evType === "content_block_delta" && isRecord(ev.delta)) {
|
|
281
872
|
const delta = ev.delta;
|
|
282
873
|
const deltaType = delta.type;
|
|
283
|
-
if (deltaType === "thinking_delta" && typeof delta.thinking === "string"
|
|
874
|
+
if (deltaType === "thinking_delta" && typeof delta.thinking === "string") {
|
|
284
875
|
state.sawStreamReasoning = true;
|
|
285
876
|
if (!state.activeReasoning.has(idx)) {
|
|
286
877
|
state.activeReasoning.add(idx);
|
|
287
|
-
|
|
878
|
+
trackOpenBlock(state, { kind: "reasoning", index: idx });
|
|
879
|
+
safeEnqueue(controller, { type: "reasoning-start", id: String(idx) });
|
|
880
|
+
}
|
|
881
|
+
if (delta.thinking && appendOutput(state, delta.thinking)) {
|
|
882
|
+
safeEnqueue(controller, { type: "reasoning-delta", id: String(idx), delta: delta.thinking });
|
|
288
883
|
}
|
|
289
|
-
controller.enqueue({ type: "reasoning-delta", id: String(idx), delta: delta.thinking });
|
|
290
884
|
}
|
|
291
|
-
else if (deltaType === "text_delta" && typeof delta.text === "string"
|
|
885
|
+
else if (deltaType === "text_delta" && typeof delta.text === "string") {
|
|
292
886
|
state.sawStreamText = true;
|
|
293
887
|
if (!state.activeText.has(idx)) {
|
|
294
888
|
state.activeText.add(idx);
|
|
295
|
-
|
|
889
|
+
trackOpenBlock(state, { kind: "text", index: idx });
|
|
890
|
+
safeEnqueue(controller, { type: "text-start", id: String(idx) });
|
|
891
|
+
}
|
|
892
|
+
if (delta.text && appendOutput(state, delta.text)) {
|
|
893
|
+
safeEnqueue(controller, { type: "text-delta", id: String(idx), delta: delta.text });
|
|
296
894
|
}
|
|
297
|
-
controller.enqueue({ type: "text-delta", id: String(idx), delta: delta.text });
|
|
298
895
|
}
|
|
299
896
|
else if (deltaType === "input_json_delta" && typeof delta.partial_json === "string") {
|
|
300
897
|
const tb = state.toolBlocks.get(idx);
|
|
301
|
-
if (tb) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
controller.enqueue({ type: "tool-input-delta", id: tb.id, delta: delta.partial_json });
|
|
305
|
-
}
|
|
898
|
+
if (!tb) {
|
|
899
|
+
failStream(state, "malformed_tool_input", "Qoder sent tool input for a block that was not started");
|
|
900
|
+
return;
|
|
306
901
|
}
|
|
902
|
+
if (tb.input.length + delta.partial_json.length > MAX_TOOL_INPUT_CHARS) {
|
|
903
|
+
failStream(state, "tool_input_too_large", "Qoder sent a tool input larger than the bridge limit");
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
tb.hasInput = true;
|
|
907
|
+
tb.input += delta.partial_json;
|
|
908
|
+
if (!tb.providerExecuted) {
|
|
909
|
+
safeEnqueue(controller, { type: "tool-input-delta", id: tb.id, delta: delta.partial_json });
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
else if (deltaType === "input_json_delta") {
|
|
913
|
+
failStream(state, "malformed_tool_input", "Qoder sent a tool input delta without JSON text");
|
|
307
914
|
}
|
|
915
|
+
else {
|
|
916
|
+
failStream(state, "malformed_stream", "Qoder sent an unsupported content block delta");
|
|
917
|
+
}
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (evType === "content_block_delta") {
|
|
921
|
+
failStream(state, "malformed_stream", "Qoder sent a content block delta without a delta object");
|
|
308
922
|
return;
|
|
309
923
|
}
|
|
310
924
|
if (evType === "content_block_stop") {
|
|
311
925
|
const tb = state.toolBlocks.get(idx);
|
|
312
926
|
if (tb) {
|
|
313
|
-
if (!tb.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
927
|
+
if (!tb.hasInput) {
|
|
928
|
+
failStream(state, "malformed_tool_input", `Qoder closed tool ${tb.name} without input JSON`);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
if (!state.pendingToolCalls.has(tb.id) && !tb.providerExecuted) {
|
|
932
|
+
const input = normalizedToolInput(tb.name, tb.input);
|
|
933
|
+
if (input === null) {
|
|
934
|
+
failStream(state, "invalid_tool_input", `Qoder sent invalid JSON for tool ${tb.name}`);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
safeEnqueue(controller, { type: "tool-input-end", id: tb.id });
|
|
938
|
+
safeEnqueue(controller, { type: "tool-call", toolCallId: tb.id, toolName: tb.name, input });
|
|
317
939
|
state.emittedToolCall = true;
|
|
318
940
|
}
|
|
319
|
-
state.pendingToolCalls.
|
|
941
|
+
if (!state.pendingToolCalls.has(tb.id)) {
|
|
942
|
+
state.pendingToolCalls.set(tb.id, { name: tb.name, providerExecuted: tb.providerExecuted });
|
|
943
|
+
}
|
|
320
944
|
state.toolBlocks.delete(idx);
|
|
945
|
+
untrackOpenBlock(state, idx);
|
|
321
946
|
}
|
|
322
947
|
else if (state.activeReasoning.has(idx)) {
|
|
323
|
-
controller
|
|
948
|
+
safeEnqueue(controller, { type: "reasoning-end", id: String(idx) });
|
|
324
949
|
state.activeReasoning.delete(idx);
|
|
950
|
+
untrackOpenBlock(state, idx);
|
|
325
951
|
}
|
|
326
952
|
else if (state.activeText.has(idx)) {
|
|
327
|
-
controller
|
|
953
|
+
safeEnqueue(controller, { type: "text-end", id: String(idx) });
|
|
328
954
|
state.activeText.delete(idx);
|
|
955
|
+
untrackOpenBlock(state, idx);
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
failStream(state, "malformed_stream", "Qoder stopped a content block that was not started");
|
|
329
959
|
}
|
|
330
960
|
return;
|
|
331
961
|
}
|
|
332
962
|
if (evType === "message_delta" && isRecord(ev.delta) && typeof ev.delta.stop_reason === "string") {
|
|
333
|
-
state.lastStopReason = ev.delta.stop_reason;
|
|
963
|
+
state.lastStopReason = safeStopReason(ev.delta.stop_reason);
|
|
334
964
|
}
|
|
335
965
|
}
|
|
336
966
|
function handleAssistant(m, state) {
|
|
967
|
+
if (isAuthenticationError(m.error)) {
|
|
968
|
+
state.authExpired = true;
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
337
971
|
const message = m.message;
|
|
972
|
+
if (Object.hasOwn(message ?? {}, "stop_reason"))
|
|
973
|
+
state.lastStopReason = safeStopReason(message?.stop_reason);
|
|
338
974
|
const content = message?.content;
|
|
339
|
-
if (!Array.isArray(content))
|
|
975
|
+
if (!Array.isArray(content)) {
|
|
976
|
+
failStream(state, "malformed_stream", "Qoder sent an assistant message without content");
|
|
340
977
|
return;
|
|
978
|
+
}
|
|
341
979
|
const { controller } = state;
|
|
342
980
|
for (const raw of content) {
|
|
343
|
-
if (!isRecord(raw))
|
|
344
|
-
|
|
981
|
+
if (!isRecord(raw)) {
|
|
982
|
+
failStream(state, "malformed_stream", "Qoder sent an assistant content block that was not an object");
|
|
983
|
+
break;
|
|
984
|
+
}
|
|
345
985
|
const blockType = raw.type;
|
|
346
986
|
if (blockType === "text" && typeof raw.text === "string" && raw.text && !state.sawStreamText) {
|
|
987
|
+
if (!appendOutput(state, raw.text))
|
|
988
|
+
break;
|
|
347
989
|
const id = String(state.blockCounter++);
|
|
348
|
-
controller
|
|
349
|
-
controller
|
|
350
|
-
controller
|
|
990
|
+
safeEnqueue(controller, { type: "text-start", id });
|
|
991
|
+
safeEnqueue(controller, { type: "text-delta", id, delta: raw.text });
|
|
992
|
+
safeEnqueue(controller, { type: "text-end", id });
|
|
351
993
|
}
|
|
352
994
|
else if (blockType === "thinking" && typeof raw.thinking === "string" && raw.thinking && !state.sawStreamReasoning) {
|
|
995
|
+
if (!appendOutput(state, raw.thinking))
|
|
996
|
+
break;
|
|
353
997
|
const id = String(state.blockCounter++);
|
|
354
|
-
controller
|
|
355
|
-
controller
|
|
356
|
-
controller
|
|
998
|
+
safeEnqueue(controller, { type: "reasoning-start", id });
|
|
999
|
+
safeEnqueue(controller, { type: "reasoning-delta", id, delta: raw.thinking });
|
|
1000
|
+
safeEnqueue(controller, { type: "reasoning-end", id });
|
|
357
1001
|
}
|
|
358
|
-
else if (blockType === "tool_use" && typeof raw.id === "string" && typeof raw.name === "string" &&
|
|
1002
|
+
else if (blockType === "tool_use" && typeof raw.id === "string" && raw.id.trim() && typeof raw.name === "string" && raw.name.trim()) {
|
|
1003
|
+
const seenToolCallIds = state.seenToolCallIds ??= new Set();
|
|
1004
|
+
if (!rememberId(seenToolCallIds, raw.id, MAX_SEEN_MESSAGE_IDS))
|
|
1005
|
+
continue;
|
|
359
1006
|
const name = normalizeToolName(raw.name);
|
|
360
|
-
const providerExecuted =
|
|
1007
|
+
const providerExecuted = isProviderOwnedTool(raw.name, name, state.functionToolNames);
|
|
361
1008
|
state.pendingToolCalls.set(raw.id, { name, providerExecuted });
|
|
362
1009
|
if (!providerExecuted) {
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
controller
|
|
1010
|
+
const input = normalizedToolInput(name, raw.input);
|
|
1011
|
+
if (input === null) {
|
|
1012
|
+
failStream(state, "invalid_tool_input", `Qoder sent invalid JSON for tool ${name}`);
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
safeEnqueue(controller, { type: "tool-input-start", id: raw.id, toolName: name });
|
|
1016
|
+
safeEnqueue(controller, { type: "tool-input-delta", id: raw.id, delta: input });
|
|
1017
|
+
safeEnqueue(controller, { type: "tool-input-end", id: raw.id });
|
|
1018
|
+
safeEnqueue(controller, { type: "tool-call", toolCallId: raw.id, toolName: name, input });
|
|
369
1019
|
state.emittedToolCall = true;
|
|
370
1020
|
}
|
|
371
1021
|
}
|
|
1022
|
+
else if (blockType === "tool_use") {
|
|
1023
|
+
failStream(state, "malformed_tool_call", "Qoder sent a tool call without a valid id or name");
|
|
1024
|
+
break;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
function normalizedToolInput(toolName, raw) {
|
|
1029
|
+
if (raw === undefined)
|
|
1030
|
+
return null;
|
|
1031
|
+
const serialized = typeof raw === "string" ? raw : safeJsonStringify(raw);
|
|
1032
|
+
if (serialized === undefined || serialized.length > MAX_TOOL_INPUT_CHARS)
|
|
1033
|
+
return null;
|
|
1034
|
+
try {
|
|
1035
|
+
const parsed = JSON.parse(serialized.trim() || "{}");
|
|
1036
|
+
if (!isRecord(parsed))
|
|
1037
|
+
return null;
|
|
1038
|
+
return normalizeToolInputString(toolName, serialized);
|
|
372
1039
|
}
|
|
1040
|
+
catch {
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
function appendOutput(state, text) {
|
|
1045
|
+
const current = Number.isFinite(state.outputChars) ? state.outputChars : 0;
|
|
1046
|
+
if (current + text.length > MAX_OUTPUT_CHARS) {
|
|
1047
|
+
failStream(state, "output_too_large", "Qoder sent more output than the bridge limit");
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
state.outputChars = current + text.length;
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
1053
|
+
function failStream(state, subtype, detail) {
|
|
1054
|
+
if (state.finished)
|
|
1055
|
+
return;
|
|
1056
|
+
state.failed = true;
|
|
1057
|
+
closeOpenBlocks(state);
|
|
1058
|
+
safeEnqueue(state.controller, { type: "error", error: new QoderSdkResultError(subtype, detail) });
|
|
1059
|
+
emitFinish(state, makeUsage(0, 0, 0, 0), makeFinishReason("error", subtype), undefined);
|
|
373
1060
|
}
|
|
374
1061
|
function handleResult(m, state) {
|
|
1062
|
+
state.resultReceived = true;
|
|
375
1063
|
const { controller } = state;
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
emitFinish(state, makeUsage(0, 0, 0, 0), makeFinishReason("error", subtype), undefined);
|
|
1064
|
+
if (Object.hasOwn(m, "stop_reason")) {
|
|
1065
|
+
state.lastStopReason = safeStopReason(m.stop_reason);
|
|
1066
|
+
}
|
|
1067
|
+
if (state.activeReasoning.size > 0 || state.activeText.size > 0 || state.toolBlocks.size > 0) {
|
|
1068
|
+
state.failed = true;
|
|
1069
|
+
closeOpenBlocks(state);
|
|
1070
|
+
safeEnqueue(controller, {
|
|
1071
|
+
type: "error",
|
|
1072
|
+
error: new QoderSdkResultError("incomplete_stream", "Qoder sent a result before closing all content blocks"),
|
|
1073
|
+
});
|
|
1074
|
+
emitFinish(state, makeUsage(0, 0, 0, 0), makeFinishReason("error", "incomplete_stream"), undefined);
|
|
388
1075
|
return;
|
|
389
1076
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
let
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
const
|
|
1077
|
+
closeOpenBlocks(state);
|
|
1078
|
+
const usage = isRecord(m.usage) ? m.usage : {};
|
|
1079
|
+
let inputTokens = tokenCount(usage.input_tokens);
|
|
1080
|
+
let outputTokens = tokenCount(usage.output_tokens);
|
|
1081
|
+
const cachedInputTokens = tokenCount(usage.cache_read_input_tokens);
|
|
1082
|
+
const cacheWriteTokens = tokenCount(usage.cache_creation_input_tokens);
|
|
1083
|
+
const contextUsageRatio = ratio(usage.context_usage_ratio);
|
|
396
1084
|
let usageEstimated = false;
|
|
397
1085
|
// Qoder's first-party backend currently reports zero token counters, but it
|
|
398
1086
|
// does report the fraction of the context window used. Convert that ratio
|
|
@@ -400,10 +1088,9 @@ function handleResult(m, state) {
|
|
|
400
1088
|
// panel instead of permanently displaying 0 tokens / 0%.
|
|
401
1089
|
if (inputTokens === 0
|
|
402
1090
|
&& outputTokens === 0
|
|
403
|
-
&&
|
|
404
|
-
&& Number.isFinite(contextUsageRatio)
|
|
1091
|
+
&& contextUsageRatio !== undefined
|
|
405
1092
|
&& contextUsageRatio > 0) {
|
|
406
|
-
const totalTokens = Math.max(1, Math.round(
|
|
1093
|
+
const totalTokens = Math.max(1, Math.round(contextUsageRatio * state.contextWindow));
|
|
407
1094
|
const resultText = typeof m.result === "string" ? m.result : "";
|
|
408
1095
|
outputTokens = resultText ? Math.max(1, Math.ceil(Buffer.byteLength(resultText, "utf8") / 4)) : 0;
|
|
409
1096
|
outputTokens = Math.min(outputTokens, totalTokens);
|
|
@@ -411,45 +1098,106 @@ function handleResult(m, state) {
|
|
|
411
1098
|
usageEstimated = true;
|
|
412
1099
|
debug(`Token counters absent; estimated ${inputTokens} in / ${outputTokens} out from context ratio`);
|
|
413
1100
|
}
|
|
414
|
-
const costUsd =
|
|
1101
|
+
const costUsd = finiteNonNegative(m.total_cost_usd);
|
|
1102
|
+
const model = typeof m.model === "string" && m.model.trim() ? m.model : "unknown";
|
|
1103
|
+
const durationMs = finiteNonNegative(m.duration_ms);
|
|
1104
|
+
const turns = finiteNonNegative(m.num_turns, 1);
|
|
1105
|
+
const record = () => {
|
|
1106
|
+
try {
|
|
1107
|
+
recordTurn({
|
|
1108
|
+
model,
|
|
1109
|
+
usage: {
|
|
1110
|
+
input_tokens: inputTokens,
|
|
1111
|
+
output_tokens: outputTokens,
|
|
1112
|
+
cache_read_input_tokens: cachedInputTokens,
|
|
1113
|
+
cache_creation_input_tokens: cacheWriteTokens,
|
|
1114
|
+
},
|
|
1115
|
+
costUsd,
|
|
1116
|
+
durationMs,
|
|
1117
|
+
turns,
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
catch (err) {
|
|
1121
|
+
debug("Cost ledger write skipped:", describeError(err));
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
const isAuthError = state.authExpired;
|
|
1125
|
+
const subtype = isAuthError
|
|
1126
|
+
? "authentication_failed"
|
|
1127
|
+
: typeof m.subtype === "string" ? m.subtype : "error_during_execution";
|
|
1128
|
+
const isError = isAuthError || m.is_error === true || m.subtype !== "success";
|
|
1129
|
+
if (isError) {
|
|
1130
|
+
state.failed = true;
|
|
1131
|
+
const detail = Array.isArray(m.errors)
|
|
1132
|
+
? redactSensitiveText((safeJsonStringify(m.errors) ?? "").slice(0, 4096))
|
|
1133
|
+
: "";
|
|
1134
|
+
state.invalidSession = isInvalidSessionError(subtype, detail);
|
|
1135
|
+
const error = state.authExpired
|
|
1136
|
+
? new QoderAuthError("Qoder authentication expired during the request. Re-authenticate with `qoder login` or refresh QODER_PERSONAL_ACCESS_TOKEN.")
|
|
1137
|
+
: new QoderSdkResultError(subtype, detail);
|
|
1138
|
+
record();
|
|
1139
|
+
safeEnqueue(controller, { type: "error", error });
|
|
1140
|
+
emitFinish(state, makeUsage(inputTokens, outputTokens, cachedInputTokens, cacheWriteTokens), makeFinishReason("error", subtype), undefined);
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
415
1143
|
const hasToolCalls = state.emittedToolCall && state.pendingToolCalls.size > 0;
|
|
416
1144
|
const finishReason = mapStopReason(state.lastStopReason, hasToolCalls);
|
|
417
|
-
|
|
418
|
-
recordTurn({
|
|
419
|
-
model: m.model ?? "unknown",
|
|
420
|
-
usage: {
|
|
421
|
-
input_tokens: inputTokens,
|
|
422
|
-
output_tokens: outputTokens,
|
|
423
|
-
cache_read_input_tokens: cachedInputTokens,
|
|
424
|
-
cache_creation_input_tokens: cacheWriteTokens,
|
|
425
|
-
},
|
|
426
|
-
costUsd,
|
|
427
|
-
durationMs: typeof m.duration_ms === "number" ? m.duration_ms : 0,
|
|
428
|
-
turns: typeof m.num_turns === "number" ? m.num_turns : 1,
|
|
429
|
-
modelUsage: m.modelUsage,
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
catch (err) {
|
|
433
|
-
debug("Cost ledger write skipped:", describeError(err));
|
|
434
|
-
}
|
|
1145
|
+
record();
|
|
435
1146
|
const qoderMeta = {};
|
|
436
|
-
if (
|
|
437
|
-
qoderMeta.totalCostUSD =
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
1147
|
+
if (Number.isFinite(costUsd))
|
|
1148
|
+
qoderMeta.totalCostUSD = costUsd;
|
|
1149
|
+
const modelUsage = toJsonValue(m.modelUsage);
|
|
1150
|
+
if (modelUsage !== undefined)
|
|
1151
|
+
qoderMeta.modelUsage = modelUsage;
|
|
1152
|
+
if (contextUsageRatio !== undefined)
|
|
441
1153
|
qoderMeta.contextUsageRatio = contextUsageRatio;
|
|
442
1154
|
if (usageEstimated)
|
|
443
1155
|
qoderMeta.usageEstimated = true;
|
|
1156
|
+
const planMode = toJsonValue(state.planMode);
|
|
1157
|
+
if (planMode !== undefined)
|
|
1158
|
+
qoderMeta.planMode = planMode;
|
|
1159
|
+
const artifacts = toJsonValue(state.artifacts);
|
|
1160
|
+
if (artifacts !== undefined && state.artifacts.length > 0)
|
|
1161
|
+
qoderMeta.artifacts = artifacts;
|
|
1162
|
+
const skillEvolution = toJsonValue(state.skillEvolution);
|
|
1163
|
+
if (skillEvolution !== undefined)
|
|
1164
|
+
qoderMeta.skillEvolution = skillEvolution;
|
|
444
1165
|
emitFinish(state, makeUsage(inputTokens, outputTokens, cachedInputTokens, cacheWriteTokens), finishReason, Object.keys(qoderMeta).length > 0 ? qoderMeta : undefined);
|
|
445
1166
|
}
|
|
1167
|
+
function isInvalidSessionError(subtype, detail) {
|
|
1168
|
+
return /(?:session.*(?:invalid|not[_ ]found|missing|expired)|(?:invalid|unknown|missing).*session)/i.test(`${subtype} ${detail}`);
|
|
1169
|
+
}
|
|
1170
|
+
function isLikelyInvalidSessionError(error) {
|
|
1171
|
+
const detail = error instanceof Error ? `${error.name} ${error.message}` : String(error);
|
|
1172
|
+
return isInvalidSessionError("", detail);
|
|
1173
|
+
}
|
|
446
1174
|
function emitFinish(state, usage, finishReason, qoderMeta) {
|
|
447
|
-
state.
|
|
1175
|
+
if (state.finished)
|
|
1176
|
+
return;
|
|
1177
|
+
state.finished = true;
|
|
1178
|
+
closeOpenBlocks(state);
|
|
1179
|
+
safeEnqueue(state.controller, {
|
|
448
1180
|
type: "finish",
|
|
449
1181
|
finishReason,
|
|
450
1182
|
usage,
|
|
451
1183
|
...(qoderMeta ? { providerMetadata: { qoder: qoderMeta } } : {}),
|
|
452
1184
|
});
|
|
453
|
-
|
|
1185
|
+
}
|
|
1186
|
+
function closeOpenBlocks(state) {
|
|
1187
|
+
for (const block of state.openBlocks ?? []) {
|
|
1188
|
+
if (block.kind === "reasoning") {
|
|
1189
|
+
safeEnqueue(state.controller, { type: "reasoning-end", id: String(block.index) });
|
|
1190
|
+
}
|
|
1191
|
+
else if (block.kind === "text") {
|
|
1192
|
+
safeEnqueue(state.controller, { type: "text-end", id: String(block.index) });
|
|
1193
|
+
}
|
|
1194
|
+
else if (block.kind === "tool" && !block.providerExecuted) {
|
|
1195
|
+
safeEnqueue(state.controller, { type: "tool-input-end", id: block.id });
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
state.openBlocks.length = 0;
|
|
1199
|
+
state.activeReasoning.clear();
|
|
1200
|
+
state.activeText.clear();
|
|
1201
|
+
state.toolBlocks.clear();
|
|
454
1202
|
}
|
|
455
1203
|
//# sourceMappingURL=language-model.js.map
|