@bitkyc08/opencodex 2.10.2 → 2.11.0

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.
Files changed (121) hide show
  1. package/README.md +31 -0
  2. package/bin/ocx.mjs +10 -0
  3. package/gui/dist/assets/index-Bk-PN-70.css +1 -0
  4. package/gui/dist/assets/index-BynIEIV-.js +70 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +4 -2
  7. package/src/adapters/cursor/effort-map.ts +11 -0
  8. package/src/adapters/cursor/live-transport.ts +11 -0
  9. package/src/adapters/cursor/native-exec-fs.ts +9 -6
  10. package/src/adapters/cursor/native-exec.ts +4 -2
  11. package/src/adapters/cursor/protobuf-events.ts +176 -4
  12. package/src/adapters/cursor/request-builder.ts +15 -4
  13. package/src/adapters/cursor/tool-definitions.ts +118 -2
  14. package/src/adapters/google.ts +15 -5
  15. package/src/adapters/openai-chat.ts +24 -2
  16. package/src/adapters/openai-responses.ts +2 -1
  17. package/src/bridge.ts +9 -5
  18. package/src/chat/outbound.ts +4 -3
  19. package/src/claude/desktop-3p.ts +222 -2
  20. package/src/claude/outbound.ts +15 -6
  21. package/src/cli/account-api.ts +4 -0
  22. package/src/cli/account-extended.ts +112 -0
  23. package/src/cli/account.ts +23 -6
  24. package/src/cli/claude-desktop.ts +26 -3
  25. package/src/cli/config-command.ts +9 -0
  26. package/src/cli/help.ts +18 -2
  27. package/src/cli/index.ts +277 -55
  28. package/src/cli/models.ts +5 -1
  29. package/src/cli/provider.ts +8 -2
  30. package/src/cli/ready.ts +301 -0
  31. package/src/cli/system-restart-client.ts +146 -0
  32. package/src/cli/tray-proxy.ts +153 -6
  33. package/src/clients/config-export.ts +12 -19
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-namespaces.ts +49 -3
  36. package/src/codex/account-priority.ts +83 -0
  37. package/src/codex/auth-api.ts +83 -0
  38. package/src/codex/auth-context.ts +5 -2
  39. package/src/codex/catalog/provider-fetch.ts +11 -0
  40. package/src/codex/catalog/sync.ts +23 -1
  41. package/src/codex/codex-write-lock.ts +16 -4
  42. package/src/codex/desired-state.ts +37 -4
  43. package/src/codex/history-job.ts +15 -5
  44. package/src/codex/history-provider.ts +31 -14
  45. package/src/codex/history-worker.ts +28 -4
  46. package/src/codex/inject-coordination.ts +13 -1
  47. package/src/codex/inject.ts +360 -66
  48. package/src/codex/internal/history-writer.ts +1 -1
  49. package/src/codex/native-main-lock-file.ts +5 -1
  50. package/src/codex/native-main-owner.ts +17 -3
  51. package/src/codex/native-profile-manager.ts +19 -0
  52. package/src/codex/native-profile-startup.ts +8 -0
  53. package/src/codex/native-residue.ts +140 -27
  54. package/src/codex/pool-rotation.ts +74 -4
  55. package/src/codex/refresh.ts +7 -0
  56. package/src/codex/routing.ts +177 -36
  57. package/src/codex/subagent-model-fallback.ts +34 -4
  58. package/src/codex/sync.ts +61 -0
  59. package/src/codex/upstream-host-health.ts +329 -31
  60. package/src/combos/request.ts +2 -0
  61. package/src/config.ts +221 -2
  62. package/src/images/loop.ts +1 -1
  63. package/src/integrations/native/ownership-preflight.ts +39 -2
  64. package/src/lib/bun-stream-caps.ts +3 -3
  65. package/src/lib/sse-decoder.ts +41 -0
  66. package/src/lib/system-restart-contract.ts +73 -0
  67. package/src/lib/windows-secret-acl.ts +141 -39
  68. package/src/lib/windows-user-principal.ts +283 -0
  69. package/src/lib/winsw.ts +18 -2
  70. package/src/oauth/key-providers.ts +12 -0
  71. package/src/providers/derive.ts +54 -2
  72. package/src/providers/free-directory.ts +6 -5
  73. package/src/providers/model-discovery.ts +9 -3
  74. package/src/providers/quota.ts +592 -0
  75. package/src/providers/registry.ts +316 -13
  76. package/src/responses/parser.ts +26 -10
  77. package/src/responses/reasoning-replay-cache.ts +1 -0
  78. package/src/routing/profile-namespace.ts +15 -0
  79. package/src/routing/profile.ts +2 -1
  80. package/src/server/auth-cors.ts +44 -13
  81. package/src/server/chat-completions.ts +0 -4
  82. package/src/server/claude-messages.ts +73 -15
  83. package/src/server/github-copilot-responses-repair.ts +338 -0
  84. package/src/server/index.ts +328 -111
  85. package/src/server/lifecycle.ts +36 -0
  86. package/src/server/management/agent-settings-routes.ts +147 -56
  87. package/src/server/management/config-routes.ts +7 -2
  88. package/src/server/management/context.ts +4 -0
  89. package/src/server/management/native-integration-routes.ts +199 -20
  90. package/src/server/management/provider-routes.ts +41 -0
  91. package/src/server/management/routing-profile-routes.ts +234 -5
  92. package/src/server/management/system-restart.ts +12 -10
  93. package/src/server/management/system-routes.ts +20 -0
  94. package/src/server/management-auth.ts +51 -3
  95. package/src/server/ports.ts +41 -1
  96. package/src/server/proxy-liveness.ts +129 -4
  97. package/src/server/readiness.ts +99 -0
  98. package/src/server/relay.ts +113 -97
  99. package/src/server/request-log.ts +10 -4
  100. package/src/server/responses/compact.ts +107 -12
  101. package/src/server/responses/core.ts +220 -39
  102. package/src/server/responses-item-id-repair.ts +22 -3
  103. package/src/server/responses-model-rewrite.ts +29 -0
  104. package/src/server/sse-frame-buffer.ts +292 -0
  105. package/src/server/sse-payload-rewrite.ts +25 -14
  106. package/src/server/ws-bridge.ts +27 -22
  107. package/src/service-manager-probe.ts +520 -10
  108. package/src/service.ts +134 -2
  109. package/src/storage/worker-lifecycle.ts +14 -14
  110. package/src/tray/windows-tray.ps1 +74 -9
  111. package/src/types.ts +68 -2
  112. package/src/update/index.ts +12 -0
  113. package/src/update/job.ts +392 -18
  114. package/src/update/npm-cache-preflight.d.mts +47 -0
  115. package/src/update/npm-cache-preflight.mjs +201 -0
  116. package/src/usage/log.ts +1 -1
  117. package/src/vision/index.ts +77 -2
  118. package/src/web-search/loop.ts +1 -1
  119. package/src/web-search/parse.ts +4 -1
  120. package/gui/dist/assets/index-BKVqyYqT.js +0 -70
  121. package/gui/dist/assets/index-Ca_3269W.css +0 -1
@@ -0,0 +1,201 @@
1
+ import { lstatSync, readdirSync, realpathSync } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
3
+ import { isAbsolute, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { npmInvocation } from "./npm-invocation.mjs";
6
+
7
+ const WORKER_ARG = "--ocx-npm-cache-preflight-worker";
8
+ const PROTOCOL_VERSION = 1;
9
+ const WORKER_TIMEOUT_MS = 10_000;
10
+ const NPM_CONFIG_TIMEOUT_MS = 5_000;
11
+ const INSPECTION_TIMEOUT_MS = 7_500;
12
+ const MAX_ENTRIES = 100_000;
13
+ const MAX_DEPTH = 64;
14
+
15
+ const RESULT_REASONS = new Set([
16
+ "cache_accessible",
17
+ "cache_entry_foreign_owner",
18
+ "cache_entry_inaccessible",
19
+ "cache_path_malformed",
20
+ "inspection_incomplete",
21
+ "npm_config_failed",
22
+ "npm_unavailable",
23
+ ]);
24
+
25
+ function inaccessibleByMode(stat) {
26
+ if (stat.isSymbolicLink()) return false;
27
+ const ownerBits = stat.mode & 0o700;
28
+ if (stat.isDirectory()) return (ownerBits & 0o700) !== 0o700;
29
+ return (ownerBits & 0o400) === 0;
30
+ }
31
+
32
+ /**
33
+ * Inspect an existing Unix npm cache without following symlinks. The limits are
34
+ * deliberately part of the result contract: an incomplete inspection cannot prove
35
+ * that replacing the live package will succeed.
36
+ */
37
+ export function inspectNpmCacheDirectory(cachePath, options = {}) {
38
+ const expectedUid = options.expectedUid ?? process.getuid?.();
39
+ const deadline = (options.nowMs ?? Date.now)() + (options.timeoutMs ?? INSPECTION_TIMEOUT_MS);
40
+ const maxEntries = options.maxEntries ?? MAX_ENTRIES;
41
+ const maxDepth = options.maxDepth ?? MAX_DEPTH;
42
+ const nowMs = options.nowMs ?? Date.now;
43
+ // Injected uid seam. A test cannot create a genuinely foreign-owned file without a second
44
+ // account, and without this the symlink-before-ownership rule cannot be pinned: `!isDirectory`
45
+ // skips a link anyway, so removing the rule leaves every assertion green.
46
+ const uidOf = options.uidOf ?? ((_path, stat) => stat.uid);
47
+ const stack = [{ path: cachePath, depth: 0 }];
48
+ let inspected = 0;
49
+ let rootResolved = false;
50
+
51
+ while (stack.length > 0) {
52
+ // Budget exhausted is NOT a failure. A mature npm cache legitimately holds hundreds of
53
+ // thousands of entries — this machine's has ~256k — and treating "we ran out of time to
54
+ // look" as "your cache is broken" would block updates for ordinary users, which is worse
55
+ // than the bug this preflight exists to prevent. We looked at a bounded prefix, found
56
+ // nothing wrong, and let the update proceed.
57
+ if (inspected >= maxEntries || nowMs() > deadline) {
58
+ return { ok: true, reason: "inspection_incomplete" };
59
+ }
60
+ const current = stack.pop();
61
+ let stat;
62
+ try {
63
+ stat = lstatSync(current.path);
64
+ } catch (error) {
65
+ if (current.depth === 0 && error?.code === "ENOENT") {
66
+ return { ok: true, reason: "cache_accessible" };
67
+ }
68
+ return { ok: false, reason: "cache_entry_inaccessible" };
69
+ }
70
+ inspected += 1;
71
+
72
+ // A symlinked cache ROOT used to be rejected outright, but pointing ~/.npm at another volume
73
+ // is ordinary npm configuration, and blocking those users would be the same false-positive
74
+ // failure this preflight exists to avoid. Resolve the root once and inspect the target;
75
+ // only an unresolvable root is a real problem. Nested links are still never followed.
76
+ if (current.depth === 0 && stat.isSymbolicLink()) {
77
+ // Resolve exactly once. realpath already collapses a chain, so a second pass would only
78
+ // happen if the target is itself reported as a link — treat that as unresolvable rather
79
+ // than looping.
80
+ if (rootResolved) return { ok: false, reason: "cache_entry_inaccessible" };
81
+ rootResolved = true;
82
+ let resolved;
83
+ try {
84
+ resolved = (options.realpathFn ?? realpathSync)(current.path);
85
+ } catch {
86
+ return { ok: false, reason: "cache_entry_inaccessible" };
87
+ }
88
+ stack.push({ path: resolved, depth: 0 });
89
+ continue;
90
+ }
91
+ // A nested symlink is not. npm creates them constantly below _npx, node_modules and .bin,
92
+ // and we never follow them — so its owner is irrelevant and must not abort the update.
93
+ // This has to come BEFORE the ownership check: a foreign-owned but never-followed link is
94
+ // exactly the false positive that made the previous attempt at this feature unusable.
95
+ if (stat.isSymbolicLink()) continue;
96
+
97
+ if (expectedUid !== undefined && uidOf(current.path, stat) !== expectedUid) {
98
+ return { ok: false, reason: "cache_entry_foreign_owner" };
99
+ }
100
+ if (inaccessibleByMode(stat)) {
101
+ return { ok: false, reason: "cache_entry_inaccessible" };
102
+ }
103
+ if (!stat.isDirectory()) continue;
104
+ // Same reasoning as the entry budget: too deep to finish is not evidence of a bad cache.
105
+ if (current.depth >= maxDepth) return { ok: true, reason: "inspection_incomplete" };
106
+
107
+ let entries;
108
+ try {
109
+ entries = readdirSync(current.path, { withFileTypes: true });
110
+ } catch {
111
+ return { ok: false, reason: "cache_entry_inaccessible" };
112
+ }
113
+ for (const entry of entries) {
114
+ stack.push({ path: resolve(current.path, entry.name), depth: current.depth + 1 });
115
+ }
116
+ }
117
+
118
+ return { ok: true, reason: "cache_accessible" };
119
+ }
120
+
121
+ function workerResult() {
122
+ const invocation = npmInvocation(["config", "get", "cache"]);
123
+ if (!invocation) return { ok: false, reason: "npm_unavailable" };
124
+ const npm = spawnSync(invocation.file, invocation.args, {
125
+ encoding: "utf8",
126
+ timeout: NPM_CONFIG_TIMEOUT_MS,
127
+ windowsHide: true,
128
+ ...invocation.options,
129
+ });
130
+ if (npm.status !== 0) return { ok: false, reason: "npm_config_failed" };
131
+
132
+ const output = typeof npm.stdout === "string" ? npm.stdout.trim() : "";
133
+ if (!output || output.length > 4096 || output.includes("\0") || /[\r\n]/.test(output) || !isAbsolute(output)) {
134
+ return { ok: false, reason: "cache_path_malformed" };
135
+ }
136
+ return inspectNpmCacheDirectory(output);
137
+ }
138
+
139
+ // Reasons that legitimately accompany `ok: true`. The parser below cross-checks the flag against
140
+ // this set so a worker cannot claim success with a failure reason (or the reverse). It is a SET,
141
+ // not a single value: a bounded inspection that ran out of budget without finding a problem is a
142
+ // pass, and hardcoding `cache_accessible` here silently rejected exactly that — the pass never
143
+ // reached the caller and every large cache still failed, as `worker_output_malformed`.
144
+ const OK_REASONS = new Set([
145
+ "cache_accessible",
146
+ "inspection_incomplete",
147
+ "windows_skip",
148
+ ]);
149
+
150
+ function parseWorkerOutput(stdout) {
151
+ if (typeof stdout !== "string" || stdout.length > 1024) return null;
152
+ try {
153
+ const parsed = JSON.parse(stdout);
154
+ if (!parsed || parsed.protocol !== PROTOCOL_VERSION || typeof parsed.ok !== "boolean") return null;
155
+ if (typeof parsed.reason !== "string" || !RESULT_REASONS.has(parsed.reason)) return null;
156
+ if (parsed.ok !== OK_REASONS.has(parsed.reason)) return null;
157
+ if (Object.keys(parsed).sort().join(",") !== "ok,protocol,reason") return null;
158
+ return { ok: parsed.ok, reason: parsed.reason };
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
163
+
164
+ /** Run the bounded cache inspection in an isolated, synchronously-timeboxed worker. */
165
+ export function runNpmCachePreflight(options = {}) {
166
+ if ((options.platform ?? process.platform) === "win32") {
167
+ return { ok: true, reason: "windows_skip" };
168
+ }
169
+ const spawn = options.spawnSyncFn ?? spawnSync;
170
+ const result = spawn(
171
+ options.execPath ?? process.execPath,
172
+ [fileURLToPath(import.meta.url), WORKER_ARG],
173
+ {
174
+ encoding: "utf8",
175
+ timeout: options.timeoutMs ?? WORKER_TIMEOUT_MS,
176
+ windowsHide: true,
177
+ env: options.env ?? process.env,
178
+ },
179
+ );
180
+ if (result.status === null) return { ok: false, reason: "worker_timeout" };
181
+ if (result.status !== 0) return { ok: false, reason: "worker_failed" };
182
+ return parseWorkerOutput(result.stdout) ?? { ok: false, reason: "worker_output_malformed" };
183
+ }
184
+
185
+ /** Fixed operator guidance; worker/npm output is intentionally never interpolated. */
186
+ export function npmCachePreflightFailureMessage(reason) {
187
+ return `npm cache access pre-flight failed (${reason}); fix cache ownership and permissions, then retry`;
188
+ }
189
+
190
+ const isWorker = process.argv[1]
191
+ && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
192
+ && process.argv[2] === WORKER_ARG;
193
+ if (isWorker) {
194
+ let result;
195
+ try {
196
+ result = workerResult();
197
+ } catch {
198
+ result = { ok: false, reason: "cache_entry_inaccessible" };
199
+ }
200
+ process.stdout.write(JSON.stringify({ protocol: PROTOCOL_VERSION, ...result }));
201
+ }
package/src/usage/log.ts CHANGED
@@ -385,7 +385,7 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
385
385
  usageStatus: entry.usageStatus,
386
386
  ...(entry.usage ? { usage: normalizeUsageValue(entry.usage) } : {}),
387
387
  ...(typeof entry.totalTokens === "number" ? { totalTokens: entry.totalTokens } : {}),
388
- ...(attempts.length > 0 ? { attempts } : {}),
388
+ ...(Array.isArray(entry.attempts) ? { attempts } : {}),
389
389
  ...(entry.errorCode ? { errorCode: entry.errorCode } : {}),
390
390
  ...(entry.terminalStatus ? { terminalStatus: entry.terminalStatus } : {}),
391
391
  ...(entry.closeReason ? { closeReason: entry.closeReason } : {}),
@@ -275,6 +275,72 @@ function renderDescription(out: { text: string; error?: string }): OcxTextConten
275
275
  };
276
276
  }
277
277
 
278
+ const IMAGE_OMITTED_TEXT = "[image omitted: this model is text-only and the vision sidecar is unavailable (no ChatGPT login)]";
279
+
280
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
281
+ return typeof value === "object" && value !== null && !Array.isArray(value);
282
+ }
283
+
284
+ /**
285
+ * Keep the native Responses passthrough body aligned with image replacements made in the parsed
286
+ * message graph. The passthrough adapter serializes `_rawBody`, while translated adapters serialize
287
+ * `context.messages`; updating only the latter would send the original pixels to a text-only
288
+ * Responses upstream even after the vision sidecar produced a caption.
289
+ *
290
+ * Rewrites only image-bearing user/developer messages and tool outputs. All other native Responses
291
+ * items (reasoning, calls, ids, compaction, and provider-specific metadata) remain byte-structurally
292
+ * untouched.
293
+ */
294
+ function syncRawBodyImageDescriptions(parsed: OcxParsedRequest, descriptions: readonly string[]): void {
295
+ const rawBody = parsed._rawBody;
296
+ if (!isPlainRecord(rawBody) || !Array.isArray(rawBody.input)) return;
297
+
298
+ let nextDescription = 0;
299
+ const rewriteImages = (value: unknown, nonEmptyImageUrlsOnly: boolean): unknown => {
300
+ if (Array.isArray(value)) {
301
+ let changed = false;
302
+ const rewritten = value.map(entry => {
303
+ const next = rewriteImages(entry, nonEmptyImageUrlsOnly);
304
+ if (next !== entry) changed = true;
305
+ return next;
306
+ });
307
+ return changed ? rewritten : value;
308
+ }
309
+ if (!isPlainRecord(value)) return value;
310
+ if (value.type === "input_image" && typeof value.image_url === "string") {
311
+ if (nonEmptyImageUrlsOnly && value.image_url.length === 0) {
312
+ return { type: "input_text", text: IMAGE_OMITTED_TEXT };
313
+ }
314
+ const description = descriptions[nextDescription++];
315
+ return { type: "input_text", text: description ?? IMAGE_OMITTED_TEXT };
316
+ }
317
+ return value;
318
+ };
319
+
320
+ let changed = false;
321
+ const input = rawBody.input.map(item => {
322
+ if (!isPlainRecord(item)) return item;
323
+ const type = typeof item.type === "string" ? item.type : (typeof item.role === "string" ? "message" : "");
324
+ const role = typeof item.role === "string" ? item.role : "";
325
+ const isMessageContent = (
326
+ (type === "message" && (role === "user" || role === "developer"))
327
+ || type === "agent_message"
328
+ );
329
+ const field = isMessageContent
330
+ ? "content"
331
+ : (type === "function_call_output" || type === "custom_tool_call_output")
332
+ ? "output"
333
+ : undefined;
334
+ if (!field) return item;
335
+ const rewritten = rewriteImages(item[field], isMessageContent);
336
+ if (rewritten === item[field]) return item;
337
+ changed = true;
338
+ return { ...item, [field]: rewritten };
339
+ });
340
+
341
+ if (changed) rawBody.input = input;
342
+ }
343
+
278
344
  function sha256(value: string | Uint8Array): string {
279
345
  return createHash("sha256").update(value).digest("hex");
280
346
  }
@@ -369,7 +435,10 @@ export async function describeImagesInPlace(
369
435
  }
370
436
  targets.push({ msg, parts });
371
437
  }
372
- if (jobs.length === 0) return;
438
+ if (jobs.length === 0) {
439
+ syncRawBodyImageDescriptions(parsed, []);
440
+ return;
441
+ }
373
442
 
374
443
  // 2. Admit misses in source order. Cache hits and same-turn waiters do not consume the cap.
375
444
  const inFlight = new Map<string, Promise<DescribeOutcome>>();
@@ -425,6 +494,7 @@ export async function describeImagesInPlace(
425
494
 
426
495
  // 3. Rebuild each message, replacing image parts with their descriptions in order.
427
496
  let oi = 0;
497
+ const descriptions: string[] = [];
428
498
  for (const { msg, parts } of targets) {
429
499
  const newParts: OcxContentPart[] = [];
430
500
  for (const p of parts) {
@@ -433,6 +503,7 @@ export async function describeImagesInPlace(
433
503
  continue;
434
504
  }
435
505
  const replacement = renderDescription(outcomes[oi++]);
506
+ descriptions.push(replacement.text);
436
507
  const reservation = translatorBudget?.reserveTransient(
437
508
  descriptionEncoder.encode(replacement.text).byteLength,
438
509
  { kind: "request_copies" },
@@ -442,6 +513,7 @@ export async function describeImagesInPlace(
442
513
  }
443
514
  msg.content = newParts;
444
515
  }
516
+ syncRawBodyImageDescriptions(parsed, descriptions);
445
517
  }
446
518
 
447
519
  /**
@@ -452,13 +524,15 @@ export async function describeImagesInPlace(
452
524
  */
453
525
  export function stripImagesInPlace(parsed: OcxParsedRequest, translatorBudget?: TranslatorBudget): boolean {
454
526
  let stripped = false;
527
+ const descriptions: string[] = [];
455
528
  for (const msg of parsed.context.messages) {
456
529
  if (!carriesImages(msg.role) || !Array.isArray(msg.content)) continue;
457
530
  const parts = msg.content as OcxContentPart[];
458
531
  if (!parts.some(p => p.type === "image")) continue;
459
532
  msg.content = parts.map(p => {
460
533
  if (p.type !== "image") return p;
461
- const replacement = { type: "text", text: "[image omitted: this model is text-only and the vision sidecar is unavailable (no ChatGPT login)]" } as OcxContentPart;
534
+ const replacement = { type: "text", text: IMAGE_OMITTED_TEXT } as OcxContentPart;
535
+ descriptions.push((replacement as OcxTextContent).text);
462
536
  const reservation = translatorBudget?.reserveTransient(
463
537
  descriptionEncoder.encode((replacement as OcxTextContent).text).byteLength,
464
538
  { kind: "request_copies" },
@@ -468,5 +542,6 @@ export function stripImagesInPlace(parsed: OcxParsedRequest, translatorBudget?:
468
542
  });
469
543
  stripped = true;
470
544
  }
545
+ syncRawBodyImageDescriptions(parsed, descriptions);
471
546
  return stripped;
472
547
  }
@@ -770,7 +770,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
770
770
  }
771
771
 
772
772
  const sse = bridgeToResponsesSSE(
773
- produce(), parsed.modelId, toolNsMap, freeform, toolSearch, () => {
773
+ produce(), parsed._responseModelId ?? parsed.modelId, toolNsMap, freeform, toolSearch, () => {
774
774
  const elapsed = Date.now() - loopT0;
775
775
  if (executedSearchCount > 0 || searchesExecuted > 0) {
776
776
  console.warn(`[web-search-loop] cancelled — ${executedSearchCount} real searches, ${searchesExecuted - executedSearchCount} placeholders, ${elapsed}ms`);
@@ -1,3 +1,5 @@
1
+ import { sseFieldValue } from "../lib/sse-decoder";
2
+
1
3
  /** A single web source backing the sidecar's answer. */
2
4
  export interface WebSearchSource {
3
5
  url: string;
@@ -187,7 +189,8 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
187
189
  const lines = buffer.split("\n");
188
190
  buffer = lines.pop() ?? "";
189
191
  for (const line of lines) {
190
- if (line.startsWith("data: ")) handle(line.slice(6).trim());
192
+ const data = sseFieldValue(line, "data");
193
+ if (data !== null) handle(data.trim());
191
194
  }
192
195
  }
193
196
  } finally {