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