@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.
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/index-Bk-PN-70.css +1 -0
- package/gui/dist/assets/index-BynIEIV-.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -2
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/live-transport.ts +11 -0
- package/src/adapters/cursor/native-exec-fs.ts +9 -6
- package/src/adapters/cursor/native-exec.ts +4 -2
- package/src/adapters/cursor/protobuf-events.ts +176 -4
- package/src/adapters/cursor/request-builder.ts +15 -4
- package/src/adapters/cursor/tool-definitions.ts +118 -2
- package/src/adapters/google.ts +15 -5
- package/src/adapters/openai-chat.ts +24 -2
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +9 -5
- package/src/chat/outbound.ts +4 -3
- package/src/claude/desktop-3p.ts +222 -2
- package/src/claude/outbound.ts +15 -6
- package/src/cli/account-api.ts +4 -0
- package/src/cli/account-extended.ts +112 -0
- package/src/cli/account.ts +23 -6
- package/src/cli/claude-desktop.ts +26 -3
- package/src/cli/config-command.ts +9 -0
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +277 -55
- package/src/cli/models.ts +5 -1
- package/src/cli/provider.ts +8 -2
- package/src/cli/ready.ts +301 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/clients/config-export.ts +12 -19
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/auth-api.ts +83 -0
- package/src/codex/auth-context.ts +5 -2
- package/src/codex/catalog/provider-fetch.ts +11 -0
- package/src/codex/catalog/sync.ts +23 -1
- package/src/codex/codex-write-lock.ts +16 -4
- package/src/codex/desired-state.ts +37 -4
- package/src/codex/history-job.ts +15 -5
- package/src/codex/history-provider.ts +31 -14
- package/src/codex/history-worker.ts +28 -4
- package/src/codex/inject-coordination.ts +13 -1
- package/src/codex/inject.ts +360 -66
- package/src/codex/internal/history-writer.ts +1 -1
- package/src/codex/native-main-lock-file.ts +5 -1
- package/src/codex/native-main-owner.ts +17 -3
- package/src/codex/native-profile-manager.ts +19 -0
- package/src/codex/native-profile-startup.ts +8 -0
- package/src/codex/native-residue.ts +140 -27
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/refresh.ts +7 -0
- package/src/codex/routing.ts +177 -36
- package/src/codex/subagent-model-fallback.ts +34 -4
- package/src/codex/sync.ts +61 -0
- package/src/codex/upstream-host-health.ts +329 -31
- package/src/combos/request.ts +2 -0
- package/src/config.ts +221 -2
- package/src/images/loop.ts +1 -1
- package/src/integrations/native/ownership-preflight.ts +39 -2
- package/src/lib/bun-stream-caps.ts +3 -3
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/windows-secret-acl.ts +141 -39
- package/src/lib/windows-user-principal.ts +283 -0
- package/src/lib/winsw.ts +18 -2
- package/src/oauth/key-providers.ts +12 -0
- package/src/providers/derive.ts +54 -2
- package/src/providers/free-directory.ts +6 -5
- package/src/providers/model-discovery.ts +9 -3
- package/src/providers/quota.ts +592 -0
- package/src/providers/registry.ts +316 -13
- package/src/responses/parser.ts +26 -10
- package/src/responses/reasoning-replay-cache.ts +1 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +2 -1
- package/src/server/auth-cors.ts +44 -13
- package/src/server/chat-completions.ts +0 -4
- package/src/server/claude-messages.ts +73 -15
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/index.ts +328 -111
- package/src/server/lifecycle.ts +36 -0
- package/src/server/management/agent-settings-routes.ts +147 -56
- package/src/server/management/config-routes.ts +7 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/native-integration-routes.ts +199 -20
- package/src/server/management/provider-routes.ts +41 -0
- package/src/server/management/routing-profile-routes.ts +234 -5
- package/src/server/management/system-restart.ts +12 -10
- package/src/server/management/system-routes.ts +20 -0
- package/src/server/management-auth.ts +51 -3
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +129 -4
- package/src/server/readiness.ts +99 -0
- package/src/server/relay.ts +113 -97
- package/src/server/request-log.ts +10 -4
- package/src/server/responses/compact.ts +107 -12
- package/src/server/responses/core.ts +220 -39
- package/src/server/responses-item-id-repair.ts +22 -3
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +25 -14
- package/src/server/ws-bridge.ts +27 -22
- package/src/service-manager-probe.ts +520 -10
- package/src/service.ts +134 -2
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +74 -9
- package/src/types.ts +68 -2
- package/src/update/index.ts +12 -0
- package/src/update/job.ts +392 -18
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/usage/log.ts +1 -1
- package/src/vision/index.ts +77 -2
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/parse.ts +4 -1
- package/gui/dist/assets/index-BKVqyYqT.js +0 -70
- 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
|
|
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 } : {}),
|
package/src/vision/index.ts
CHANGED
|
@@ -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)
|
|
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:
|
|
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
|
}
|
package/src/web-search/loop.ts
CHANGED
|
@@ -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`);
|
package/src/web-search/parse.ts
CHANGED
|
@@ -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
|
-
|
|
192
|
+
const data = sseFieldValue(line, "data");
|
|
193
|
+
if (data !== null) handle(data.trim());
|
|
191
194
|
}
|
|
192
195
|
}
|
|
193
196
|
} finally {
|