@bitkyc08/opencodex 2.28.0 → 2.29.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 +9 -1
- package/gui/dist/assets/index-BNESwCzn.js +102 -0
- package/gui/dist/assets/index-CH7ncHCC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/base.ts +3 -1
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/discovery.ts +25 -0
- package/src/adapters/cursor/live-transport.ts +19 -2
- package/src/adapters/cursor/native-exec.ts +47 -2
- package/src/adapters/cursor/protobuf-request.ts +80 -23
- package/src/adapters/cursor/request-builder.ts +119 -5
- package/src/adapters/cursor/transport.ts +5 -0
- package/src/adapters/cursor/types.ts +13 -0
- package/src/adapters/cursor.ts +109 -3
- package/src/adapters/google-antigravity-replay.ts +31 -4
- package/src/adapters/google.ts +323 -38
- package/src/adapters/openai-chat.ts +19 -0
- package/src/adapters/openai-responses.ts +218 -37
- package/src/bridge.ts +38 -20
- package/src/claude/desktop-3p.ts +15 -6
- package/src/cli/agent.ts +44 -1
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/dispatch.ts +24 -1
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +40 -38
- package/src/cli/integrations.ts +1 -1
- package/src/cli/registry.ts +2 -2
- package/src/clients/config-export.ts +119 -20
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/inject.ts +46 -14
- package/src/codex/journal.ts +22 -8
- package/src/config.ts +1 -0
- package/src/generated/compatibility-version.json +134 -66
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/registry.ts +25 -2
- package/src/integrations/writer.ts +32 -1
- package/src/lab/public/signature.ts +25 -1
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/redact.ts +2 -2
- package/src/oauth/log.ts +3 -1
- package/src/providers/derive.ts +9 -0
- package/src/providers/fastwire.ts +24 -18
- package/src/providers/openai-tiers.ts +60 -1
- package/src/providers/registry.ts +14 -7
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/responses/compaction.ts +18 -0
- package/src/responses/custom-tool-compat.ts +70 -5
- package/src/responses/namespace-tool-compat.ts +356 -0
- package/src/responses/parser.ts +2 -2
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/reasoning-replay-cache.ts +125 -7
- package/src/responses/spill-store.ts +6 -1
- package/src/responses/state.ts +11 -0
- package/src/router.ts +14 -0
- package/src/routing/compatibility/behavior.ts +1 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/management/agent-settings-routes.ts +57 -9
- package/src/server/management/config-routes.ts +134 -15
- package/src/server/management/integration-routes.ts +8 -55
- package/src/server/management/model-routes.ts +23 -1
- package/src/server/management/provider-routes.ts +22 -0
- package/src/server/management/vision-sidecar-options.ts +18 -11
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/responses/core.ts +588 -78
- package/src/server/responses/responses-field-backfill.ts +7 -11
- package/src/server/responses/terminal-guard.ts +22 -11
- package/src/server/responses-custom-tool-repair.ts +3 -1
- package/src/server/responses-reasoning-summary-rewrite.ts +7 -0
- package/src/server/responses-tool-search-repair.ts +64 -14
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/types/config.ts +26 -5
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +26 -0
- package/src/types.ts +1 -0
- package/src/usage/log.ts +2 -0
- package/src/vision/index.ts +8 -9
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +88 -0
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +139 -12
- package/src/web-search/loop.ts +45 -5
- package/src/web-search/parse.ts +34 -23
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-D2sP-biU.js +0 -102
- package/gui/dist/assets/index-DQsMZzI5.css +0 -1
|
@@ -2,16 +2,22 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import type { IncomingMeta, ProviderAdapter } from "./base";
|
|
3
3
|
import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../types";
|
|
4
4
|
import { catalogModelSupportsReasoningSummaries } from "../codex/catalog";
|
|
5
|
-
import { COMPACT_PROMPT, decodeCompactionSummary,
|
|
5
|
+
import { COMPACT_PROMPT, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "../responses/compaction";
|
|
6
6
|
import { collectResponsesToolGroups } from "../responses/tool-groups";
|
|
7
7
|
import { isHostedToolUnsupportedForModel } from "../responses/hosted-tool-policy";
|
|
8
8
|
import { decodeServerSentEvents } from "../lib/sse-decoder";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
CODEX_FORWARD_BASE_URL,
|
|
11
|
+
destinationDecodesNativeCompactionBlob,
|
|
12
|
+
isCanonicalOpenAiForwardProvider,
|
|
13
|
+
isOpenAiOperatedResponsesDestination,
|
|
14
|
+
} from "../providers/openai-tiers";
|
|
10
15
|
import { OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
|
|
11
16
|
import { modelRecordValue } from "../reasoning-effort";
|
|
12
17
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
13
18
|
import { rewriteRoutedCustomToolsForUpstream } from "../responses/custom-tool-compat";
|
|
14
19
|
import { rewriteRoutedToolSearchForUpstream } from "../responses/tool-search-compat";
|
|
20
|
+
import { rewriteRoutedNamespaceToolsForUpstream } from "../responses/namespace-tool-compat";
|
|
15
21
|
import { openaiResponsesUrl } from "./openai-responses-url";
|
|
16
22
|
import {
|
|
17
23
|
createAdapterTierMetadata,
|
|
@@ -41,7 +47,11 @@ export const FORWARD_HEADERS = [
|
|
|
41
47
|
|
|
42
48
|
export function sanitizeReasoningInputContent(
|
|
43
49
|
body: unknown,
|
|
44
|
-
opts?: {
|
|
50
|
+
opts?: {
|
|
51
|
+
preserveRawReasoningContent?: boolean;
|
|
52
|
+
dropNullContentChannel?: boolean;
|
|
53
|
+
stripEncryptedContent?: boolean;
|
|
54
|
+
},
|
|
45
55
|
): unknown {
|
|
46
56
|
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
|
47
57
|
const raw = body as Record<string, unknown>;
|
|
@@ -56,14 +66,40 @@ export function sanitizeReasoningInputContent(
|
|
|
56
66
|
// ocxr1 envelopes are proxy-minted (Anthropic signatures), not OpenAI encryption — the native
|
|
57
67
|
// backend cannot decrypt them and would reject the request. Strip regardless of content shape.
|
|
58
68
|
const hasOcxEnvelope = typeof rec.encrypted_content === "string" && rec.encrypted_content.startsWith(OCX_REASONING_PREFIX);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
const hasOutputStatus = Object.prototype.hasOwnProperty.call(rec, "status");
|
|
70
|
+
const hasEncryptedContent = Object.prototype.hasOwnProperty.call(rec, "encrypted_content");
|
|
71
|
+
const stripEncryptedContent = hasOcxEnvelope
|
|
72
|
+
|| (opts?.stripEncryptedContent === true && hasEncryptedContent);
|
|
73
|
+
// Codex serializes an absent reasoning content channel as `"content": null`. The field is
|
|
74
|
+
// optional and null carries nothing, but a strict gateway rejects the item on its declared type
|
|
75
|
+
// — xAI answers `Could not decode the compaction blob`, naming the sibling `encrypted_content`
|
|
76
|
+
// rather than the field it actually refused, which is why this reads as a blob failure. Drop the
|
|
77
|
+
// key so the item matches the shape the upstream issued.
|
|
78
|
+
//
|
|
79
|
+
// Gated to routed destinations. An OpenAI-operated backend binds the blob to the item's exact
|
|
80
|
+
// shape, so deleting a field there invalidates it (`The encrypted content ... could not be
|
|
81
|
+
// verified`); the two requirements are exactly opposed, and a live regression proved it. That
|
|
82
|
+
// gate is also why this drop may touch an item that keeps its blob: xAI demonstrably accepts its
|
|
83
|
+
// own blob without the null channel, and the destinations that bind blobs to item shape never
|
|
84
|
+
// reach this branch. This is independent of the output-only status removal below.
|
|
85
|
+
const dropNullContentChannel = opts?.dropNullContentChannel === true
|
|
86
|
+
&& "content" in rec && !Array.isArray(rec.content);
|
|
87
|
+
// `status` is output-only. Measured OpenAI reasoning items never contain it, and Grok accepts
|
|
88
|
+
// its own encrypted_content with status removed. Keeping a foreign status beside a retained
|
|
89
|
+
// blob makes OpenAI reject the field before blob validation, starving the provenance recovery
|
|
90
|
+
// of the opaque-blob error it needs. Content blanking remains the separate pre-existing rule.
|
|
91
|
+
const stripOutputStatus = hasOutputStatus;
|
|
92
|
+
const blankContent = !dropNullContentChannel
|
|
93
|
+
&& !opts?.preserveRawReasoningContent
|
|
94
|
+
&& (hasRawContent || hasOcxEnvelope);
|
|
95
|
+
if (!blankContent && !stripOutputStatus && !stripEncryptedContent && !dropNullContentChannel) {
|
|
96
|
+
return item;
|
|
66
97
|
}
|
|
98
|
+
changed = true;
|
|
99
|
+
const next: Record<string, unknown> = { ...rec };
|
|
100
|
+
if (dropNullContentChannel) delete next.content;
|
|
101
|
+
if (stripOutputStatus) delete next.status;
|
|
102
|
+
if (stripEncryptedContent) delete next.encrypted_content;
|
|
67
103
|
// Routed models can produce raw `reasoning_text` output items. Codex echoes those in later
|
|
68
104
|
// native GPT requests, but ChatGPT's Responses backend accepts reasoning input only with empty
|
|
69
105
|
// `content`; keep summaries/ids and drop the raw content so native passthrough does not 400.
|
|
@@ -71,9 +107,8 @@ export function sanitizeReasoningInputContent(
|
|
|
71
107
|
// guide merges reasoning items into the adjacent assistant message), so providers flagged
|
|
72
108
|
// `preserveResponsesReasoningContent` keep it — deleting valid replay content there breaks
|
|
73
109
|
// continuations after tool calls (issue #875 family).
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
return { ...rec, content: [] };
|
|
110
|
+
if (blankContent) next.content = [];
|
|
111
|
+
return next;
|
|
77
112
|
});
|
|
78
113
|
|
|
79
114
|
return changed ? { ...raw, input } : body;
|
|
@@ -120,6 +155,71 @@ function stripInvalidItemIds(body: unknown): unknown {
|
|
|
120
155
|
return changed ? { ...body, input } : body;
|
|
121
156
|
}
|
|
122
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Codex-private tool fields that only the ChatGPT backend understands.
|
|
160
|
+
*
|
|
161
|
+
* A third-party Responses gateway validates its schema and rejects the whole request before
|
|
162
|
+
* inference — xAI answers `Argument not supported: external_web_access` — so these are removed at
|
|
163
|
+
* the noncanonical boundary while the tool and every public option stay.
|
|
164
|
+
*
|
|
165
|
+
* Keep this a table. Each private bit Codex attaches has so far arrived as its own bespoke strip
|
|
166
|
+
* with its own traversal, and the traversals disagreed about which containers they covered; a new
|
|
167
|
+
* one should be a row here instead. `toolTypes` omitted means the field is private on any tool.
|
|
168
|
+
*/
|
|
169
|
+
const CANONICAL_ONLY_TOOL_FIELDS: readonly { field: string; toolTypes?: ReadonlySet<string>; capabilityGated?: boolean }[] = [
|
|
170
|
+
// ChatGPT's browsing policy bit. The public hosted tool is enabled by its presence alone.
|
|
171
|
+
// OWNERSHIP: official OpenAI API-key traffic and unclassified gateways ACCEPT this field, so
|
|
172
|
+
// it is only stripped when the provider capability denies it (supportsOpenAiWebSearchToolFields
|
|
173
|
+
// === false), matching stripOpenAiOnlyWebSearchFields; see
|
|
174
|
+
// tests/responses-routed-web-search-fields.test.ts.
|
|
175
|
+
{ field: "external_web_access", toolTypes: new Set(["web_search", "web_search_preview"]), capabilityGated: true },
|
|
176
|
+
// Deferred-discovery marker. `activateDeferredTool` clears it only for tools a `tool_search_output`
|
|
177
|
+
// already loaded, so a still-deferred declaration — including one promoted out of a namespace
|
|
178
|
+
// group — otherwise reaches the wire carrying it.
|
|
179
|
+
{ field: "defer_loading" },
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
function stripCanonicalOnlyToolFields(body: unknown, includeCapabilityGated: boolean): unknown {
|
|
183
|
+
if (!isPlainObject(body)) return body;
|
|
184
|
+
|
|
185
|
+
const rewriteTools = (tools: unknown[]): unknown[] => {
|
|
186
|
+
let changed = false;
|
|
187
|
+
const rewritten = tools.map(tool => {
|
|
188
|
+
if (!isPlainObject(tool)) return tool;
|
|
189
|
+
let next = tool;
|
|
190
|
+
for (const { field, toolTypes, capabilityGated } of CANONICAL_ONLY_TOOL_FIELDS) {
|
|
191
|
+
if (capabilityGated && !includeCapabilityGated) continue;
|
|
192
|
+
if (!Object.hasOwn(next, field)) continue;
|
|
193
|
+
if (toolTypes && (typeof next.type !== "string" || !toolTypes.has(next.type))) continue;
|
|
194
|
+
const { [field]: _private, ...rest } = next;
|
|
195
|
+
next = rest;
|
|
196
|
+
}
|
|
197
|
+
if (next === tool) return tool;
|
|
198
|
+
changed = true;
|
|
199
|
+
return next;
|
|
200
|
+
});
|
|
201
|
+
return changed ? rewritten : tools;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
let rewrittenBody = body;
|
|
205
|
+
if (Array.isArray(body.tools)) {
|
|
206
|
+
const tools = rewriteTools(body.tools);
|
|
207
|
+
if (tools !== body.tools) rewrittenBody = { ...rewrittenBody, tools };
|
|
208
|
+
}
|
|
209
|
+
if (!Array.isArray(body.input)) return rewrittenBody;
|
|
210
|
+
|
|
211
|
+
let input: unknown[] | undefined;
|
|
212
|
+
for (let index = 0; index < body.input.length; index += 1) {
|
|
213
|
+
const item = body.input[index];
|
|
214
|
+
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) continue;
|
|
215
|
+
const tools = rewriteTools(item.tools);
|
|
216
|
+
if (tools === item.tools) continue;
|
|
217
|
+
input ??= [...body.input];
|
|
218
|
+
input[index] = { ...item, tools };
|
|
219
|
+
}
|
|
220
|
+
return input ? { ...rewrittenBody, input } : rewrittenBody;
|
|
221
|
+
}
|
|
222
|
+
|
|
123
223
|
/**
|
|
124
224
|
* When `store` is false, the upstream API does not persist response items. Any item ID
|
|
125
225
|
* forwarded in `input` is then interpreted as a reference to a stored item that does not
|
|
@@ -143,25 +243,41 @@ function stripItemIdsWhenUnstored(body: unknown): unknown {
|
|
|
143
243
|
}
|
|
144
244
|
|
|
145
245
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
246
|
+
* Normalize replayed compaction items for the destination backend.
|
|
247
|
+
*
|
|
248
|
+
* A compaction item carries an `encrypted_content` blob the client replays verbatim on every later
|
|
249
|
+
* turn, and only the backend that minted it can decode it. Proxy-minted `ocx1:` envelopes are
|
|
250
|
+
* transparent base64 rather than encryption, so no upstream can read them and they always become
|
|
251
|
+
* plain user messages. Native blobs have multiple possible minters, so a destination's ability to
|
|
252
|
+
* decode its own blobs does not make a blob from a previous serving identity portable. On a known
|
|
253
|
+
* identity mismatch the blob degrades to the same note the bridged parser uses, even when the
|
|
254
|
+
* destination normally accepts native blobs. Without a known mismatch, the destination capability
|
|
255
|
+
* keeps the existing behavior.
|
|
256
|
+
*
|
|
257
|
+
* A bare `context_compaction` marker carries no blob and is forwarded untouched.
|
|
150
258
|
*/
|
|
151
|
-
function scrubOcxCompactionItems(
|
|
259
|
+
function scrubOcxCompactionItems(
|
|
260
|
+
body: unknown,
|
|
261
|
+
destinationDecodesNativeBlob: boolean,
|
|
262
|
+
threadServingIdentityChanged: boolean,
|
|
263
|
+
): unknown {
|
|
152
264
|
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
153
265
|
|
|
154
266
|
let changed = false;
|
|
155
267
|
const input = body.input.map(item => {
|
|
156
|
-
if (!isPlainObject(item)) return item;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (
|
|
268
|
+
if (!isPlainObject(item) || !isCompactionItemType(item.type)) return item;
|
|
269
|
+
const encrypted = typeof item.encrypted_content === "string" ? item.encrypted_content : undefined;
|
|
270
|
+
if (encrypted === undefined) return item;
|
|
271
|
+
if (
|
|
272
|
+
decodeCompactionSummary(encrypted) === null
|
|
273
|
+
&& destinationDecodesNativeBlob
|
|
274
|
+
&& !threadServingIdentityChanged
|
|
275
|
+
) return item;
|
|
160
276
|
changed = true;
|
|
161
277
|
return {
|
|
162
278
|
type: "message",
|
|
163
279
|
role: "user",
|
|
164
|
-
content: [{ type: "input_text", text:
|
|
280
|
+
content: [{ type: "input_text", text: compactionItemToText(encrypted) }],
|
|
165
281
|
};
|
|
166
282
|
});
|
|
167
283
|
|
|
@@ -725,8 +841,18 @@ function repairOrphanedInputItems(body: unknown, dropReasoning: boolean, synthes
|
|
|
725
841
|
};
|
|
726
842
|
const reorderBatchOutputs = (items: unknown[]): unknown[] => {
|
|
727
843
|
const ordered: unknown[] = [];
|
|
844
|
+
const claimedOutputIndexes = new Set<number>();
|
|
845
|
+
const outputIndexesByKey = new Map<string, { indexes: number[]; offset: number }>();
|
|
846
|
+
for (let outputIndex = 0; outputIndex < items.length; outputIndex += 1) {
|
|
847
|
+
const outputKey = outputKeyOf(items[outputIndex]);
|
|
848
|
+
if (outputKey === null) continue;
|
|
849
|
+
const bucket = outputIndexesByKey.get(outputKey);
|
|
850
|
+
if (bucket) bucket.indexes.push(outputIndex);
|
|
851
|
+
else outputIndexesByKey.set(outputKey, { indexes: [outputIndex], offset: 0 });
|
|
852
|
+
}
|
|
728
853
|
let index = 0;
|
|
729
854
|
while (index < items.length) {
|
|
855
|
+
if (claimedOutputIndexes.has(index)) { index += 1; continue; }
|
|
730
856
|
const key = callKeyOf(items[index]);
|
|
731
857
|
if (key === null) { ordered.push(items[index]); index += 1; continue; }
|
|
732
858
|
const batch: unknown[] = [];
|
|
@@ -745,20 +871,24 @@ function repairOrphanedInputItems(body: unknown, dropReasoning: boolean, synthes
|
|
|
745
871
|
index = cursor;
|
|
746
872
|
continue;
|
|
747
873
|
}
|
|
748
|
-
const
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
}
|
|
755
|
-
|
|
874
|
+
const batchOutputs: unknown[] = [];
|
|
875
|
+
for (const batchKey of batchKeys) {
|
|
876
|
+
const bucket = outputIndexesByKey.get(batchKey);
|
|
877
|
+
if (!bucket) continue;
|
|
878
|
+
while (bucket.offset < bucket.indexes.length && bucket.indexes[bucket.offset]! < cursor) {
|
|
879
|
+
bucket.offset += 1;
|
|
880
|
+
}
|
|
881
|
+
while (bucket.offset < bucket.indexes.length) {
|
|
882
|
+
const outputIndex = bucket.indexes[bucket.offset]!;
|
|
883
|
+
bucket.offset += 1;
|
|
884
|
+
if (claimedOutputIndexes.has(outputIndex)) continue;
|
|
885
|
+
claimedOutputIndexes.add(outputIndex);
|
|
886
|
+
batchOutputs.push(items[outputIndex]);
|
|
887
|
+
break;
|
|
756
888
|
}
|
|
757
889
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
ordered.push(...reorderBatchOutputs(remainder));
|
|
761
|
-
return ordered;
|
|
890
|
+
ordered.push(...batch, ...batchOutputs);
|
|
891
|
+
index = cursor;
|
|
762
892
|
}
|
|
763
893
|
return ordered;
|
|
764
894
|
};
|
|
@@ -1362,6 +1492,30 @@ function stripUnsupportedHostedTools(body: unknown): unknown {
|
|
|
1362
1492
|
return tools.length === body.tools.length ? body : { ...body, tools };
|
|
1363
1493
|
}
|
|
1364
1494
|
|
|
1495
|
+
/**
|
|
1496
|
+
* OpenAI hosted web_search config fields that a capability-classified Responses
|
|
1497
|
+
* upstream may reject wholesale. xAI's /v1/responses 400s the entire request on
|
|
1498
|
+
* `external_web_access` and `search_context_size` ("Argument not supported"),
|
|
1499
|
+
* which killed every routed Grok turn whose client (Codex) attaches its
|
|
1500
|
+
* default web_search tool config (probe 2026-08-21: both fields 400
|
|
1501
|
+
* individually; `user_location` and `filters` are accepted and kept).
|
|
1502
|
+
* The caller decides whether to apply this compatibility transform from explicit
|
|
1503
|
+
* provider capability metadata; an unclassified upstream keeps the fields.
|
|
1504
|
+
*/
|
|
1505
|
+
const OPENAI_ONLY_WEB_SEARCH_FIELDS = ["external_web_access", "search_context_size"] as const;
|
|
1506
|
+
export function stripOpenAiOnlyWebSearchFields(body: unknown): unknown {
|
|
1507
|
+
if (!isPlainObject(body) || !Array.isArray(body.tools)) return body;
|
|
1508
|
+
let changed = false;
|
|
1509
|
+
const tools = body.tools.map(t => {
|
|
1510
|
+
if (!isPlainObject(t) || (t.type !== "web_search" && t.type !== "web_search_preview")) return t;
|
|
1511
|
+
if (!OPENAI_ONLY_WEB_SEARCH_FIELDS.some(field => Object.hasOwn(t, field))) return t;
|
|
1512
|
+
const { external_web_access: _access, search_context_size: _size, ...rest } = t;
|
|
1513
|
+
changed = true;
|
|
1514
|
+
return rest;
|
|
1515
|
+
});
|
|
1516
|
+
return changed ? { ...body, tools } : body;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1365
1519
|
/** Replace every `input_image` part under a routed-compaction body with a short marker. */
|
|
1366
1520
|
function stripInputImagesDeep(value: unknown): unknown {
|
|
1367
1521
|
if (Array.isArray(value)) return value.map(stripInputImagesDeep);
|
|
@@ -1492,6 +1646,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1492
1646
|
const forward = provider.authMode === "forward";
|
|
1493
1647
|
let convertedRoutedCustomToolNames: Set<string> | undefined;
|
|
1494
1648
|
let convertedRoutedToolSearchNames: Set<string> | undefined;
|
|
1649
|
+
let convertedRoutedNamespaceToolAliases: Map<string, { namespace: string; name: string }> | undefined;
|
|
1495
1650
|
const unexpandedMiss = !!parsed.previousResponseId && parsed._previousResponseInputExpanded !== true;
|
|
1496
1651
|
let outBody = stripPreviousResponseId(
|
|
1497
1652
|
parsed._rawBody,
|
|
@@ -1546,7 +1701,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1546
1701
|
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
1547
1702
|
outBody = promoteClientLoadedTools(outBody);
|
|
1548
1703
|
}
|
|
1549
|
-
if (provider
|
|
1704
|
+
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
1550
1705
|
const rewritten = rewriteRoutedCustomToolsForUpstream(outBody);
|
|
1551
1706
|
outBody = rewritten.body;
|
|
1552
1707
|
convertedRoutedCustomToolNames = rewritten.names;
|
|
@@ -1557,8 +1712,33 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1557
1712
|
const rewritten = rewriteRoutedToolSearchForUpstream(outBody);
|
|
1558
1713
|
outBody = rewritten.body;
|
|
1559
1714
|
convertedRoutedToolSearchNames = rewritten.names;
|
|
1715
|
+
// xAI rejects these OpenAI web_search extensions with HTTP 400. Keep them
|
|
1716
|
+
// for OpenAI API-key traffic and unclassified gateways; only an explicit
|
|
1717
|
+
// provider capability denial activates the compatibility transform.
|
|
1718
|
+
if (provider.supportsOpenAiWebSearchToolFields === false) {
|
|
1719
|
+
outBody = stripOpenAiOnlyWebSearchFields(outBody);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
1723
|
+
// Codex 0.147 emits private namespace tool groups, while public/third-party Responses
|
|
1724
|
+
// gateways accept only flat tool variants. Run after custom/tool-search lowering so
|
|
1725
|
+
// namespace children already carry their final public kind before they are promoted.
|
|
1726
|
+
const rewritten = rewriteRoutedNamespaceToolsForUpstream(outBody);
|
|
1727
|
+
outBody = rewritten.body;
|
|
1728
|
+
convertedRoutedNamespaceToolAliases = rewritten.aliases;
|
|
1729
|
+
// Last, so promoted namespace children are also cleared of Codex-private fields.
|
|
1730
|
+
outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false);
|
|
1560
1731
|
}
|
|
1561
|
-
const
|
|
1732
|
+
const threadServingIdentityChanged = parsed._stripReasoningEncryptedContent === true;
|
|
1733
|
+
const sanitizedBody = normalizeToolSchemas(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(
|
|
1734
|
+
outBody,
|
|
1735
|
+
destinationDecodesNativeCompactionBlob(provider),
|
|
1736
|
+
threadServingIdentityChanged,
|
|
1737
|
+
), {
|
|
1738
|
+
preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true,
|
|
1739
|
+
dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider),
|
|
1740
|
+
stripEncryptedContent: threadServingIdentityChanged,
|
|
1741
|
+
})))))));
|
|
1562
1742
|
const finalBody = stripDisabledReasoningSummaries(
|
|
1563
1743
|
normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId),
|
|
1564
1744
|
provider,
|
|
@@ -1586,6 +1766,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1586
1766
|
releaseBodyObservation,
|
|
1587
1767
|
...(convertedRoutedCustomToolNames ? { convertedRoutedCustomToolNames } : {}),
|
|
1588
1768
|
...(convertedRoutedToolSearchNames ? { convertedRoutedToolSearchNames } : {}),
|
|
1769
|
+
...(convertedRoutedNamespaceToolAliases ? { convertedRoutedNamespaceToolAliases } : {}),
|
|
1589
1770
|
...(tierLog ? { tierLog } : {}),
|
|
1590
1771
|
};
|
|
1591
1772
|
},
|
package/src/bridge.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./responses/thought-signature-replay";
|
|
20
20
|
import { resolveStallTimeoutSec } from "./stall-timeout";
|
|
21
21
|
import { usageDisplayTotalTokens } from "./usage/totals";
|
|
22
|
+
import { appendSafeWebSearchSource, safeWebSearchSources } from "./web-search/sources";
|
|
22
23
|
import {
|
|
23
24
|
isTranslatorBudgetExceededError,
|
|
24
25
|
releaseTranslatedEvent,
|
|
@@ -526,17 +527,22 @@ export function bridgeToResponsesSSE(
|
|
|
526
527
|
// url_citation annotations on that message (the desktop app's Sources chip), then cleared so
|
|
527
528
|
// they bind to exactly one message. Deduped by URL across multiple searches in the turn.
|
|
528
529
|
let pendingWebSources: { url: string; title?: string }[] = [];
|
|
530
|
+
let pendingWebSourceBytes = 0;
|
|
531
|
+
const releasePendingWebSources = () => {
|
|
532
|
+
if (pendingWebSources.length === 0) return;
|
|
533
|
+
pendingWebSources = [];
|
|
534
|
+
budget?.releaseRetained(pendingWebSourceBytes, { kind: "tool_search_sources" });
|
|
535
|
+
pendingWebSourceBytes = 0;
|
|
536
|
+
};
|
|
529
537
|
const takeWebAnnotations = (): { type: string; url: string; title?: string; start_index: number; end_index: number }[] => {
|
|
530
538
|
if (pendingWebSources.length === 0) return [];
|
|
531
539
|
const anns = pendingWebSources.map(s => ({
|
|
532
540
|
type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0,
|
|
533
541
|
}));
|
|
534
|
-
const sourceBytes = pendingWebSources.reduce((sum, source) => sum + bytesOf(JSON.stringify(source)), 0);
|
|
535
542
|
const annotationBytes = bytesOf(JSON.stringify(anns));
|
|
536
543
|
const reservation = budget?.reserveTransient(annotationBytes, { kind: "retained_collectors" });
|
|
537
|
-
pendingWebSources = [];
|
|
538
544
|
reservation?.commitRetained();
|
|
539
|
-
|
|
545
|
+
releasePendingWebSources();
|
|
540
546
|
return anns;
|
|
541
547
|
};
|
|
542
548
|
|
|
@@ -792,6 +798,7 @@ export function bridgeToResponsesSSE(
|
|
|
792
798
|
handlingTranslatorOverflow = true;
|
|
793
799
|
abortCurrentToolCallForTranslatorOverflow();
|
|
794
800
|
currentWebSearch = null;
|
|
801
|
+
releasePendingWebSources();
|
|
795
802
|
const failure = adapterFailureFromEvent({
|
|
796
803
|
type: "error",
|
|
797
804
|
status: 502,
|
|
@@ -1157,15 +1164,13 @@ export function bridgeToResponsesSSE(
|
|
|
1157
1164
|
});
|
|
1158
1165
|
currentWebSearch = { itemId: wsItemId2, eventId: event.id, outputIndex };
|
|
1159
1166
|
}
|
|
1160
|
-
|
|
1167
|
+
const safeSources = safeWebSearchSources(event.sources);
|
|
1168
|
+
closeCurrentWebSearch(event.status ?? "completed", event.queries, safeSources);
|
|
1161
1169
|
// Queue this search's sources for the next assistant message (dedup by URL).
|
|
1162
|
-
if (
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
seen.add(s.url);
|
|
1167
|
-
chargeValue(s, "tool_search_sources");
|
|
1168
|
-
pendingWebSources.push(s);
|
|
1170
|
+
if (safeSources.length > 0) {
|
|
1171
|
+
for (const source of safeSources) {
|
|
1172
|
+
if (appendSafeWebSearchSource(pendingWebSources, source)) {
|
|
1173
|
+
pendingWebSourceBytes += chargeValue(source, "tool_search_sources");
|
|
1169
1174
|
}
|
|
1170
1175
|
}
|
|
1171
1176
|
}
|
|
@@ -1178,6 +1183,7 @@ export function bridgeToResponsesSSE(
|
|
|
1178
1183
|
flushHiddenRawReasoning();
|
|
1179
1184
|
if (currentToolCall) closeCurrentToolCall();
|
|
1180
1185
|
if (currentWebSearch) closeCurrentWebSearch("completed", []);
|
|
1186
|
+
releasePendingWebSources();
|
|
1181
1187
|
// Redacted-only turns (or hidden thinking without a trailing signature event) still
|
|
1182
1188
|
// need their envelope-only reasoning item so the blocks replay next turn.
|
|
1183
1189
|
flushHiddenReasoningEnvelope();
|
|
@@ -1241,6 +1247,7 @@ export function bridgeToResponsesSSE(
|
|
|
1241
1247
|
flushHiddenRawReasoning();
|
|
1242
1248
|
if (currentToolCall) failCurrentToolCall();
|
|
1243
1249
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
1250
|
+
releasePendingWebSources();
|
|
1244
1251
|
flushHiddenReasoningEnvelope();
|
|
1245
1252
|
options?.onUsage?.(event.usage);
|
|
1246
1253
|
await awaitThoughtSignatureDurability();
|
|
@@ -1270,6 +1277,7 @@ export function bridgeToResponsesSSE(
|
|
|
1270
1277
|
flushHiddenRawReasoning();
|
|
1271
1278
|
if (currentToolCall) failCurrentToolCall();
|
|
1272
1279
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
1280
|
+
releasePendingWebSources();
|
|
1273
1281
|
const failure = adapterFailureFromEvent(event);
|
|
1274
1282
|
if (event.usage) options?.onUsage?.(event.usage);
|
|
1275
1283
|
await awaitThoughtSignatureDurability();
|
|
@@ -1304,6 +1312,7 @@ export function bridgeToResponsesSSE(
|
|
|
1304
1312
|
flushHiddenRawReasoning();
|
|
1305
1313
|
if (currentToolCall) failCurrentToolCall();
|
|
1306
1314
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
1315
|
+
releasePendingWebSources();
|
|
1307
1316
|
emit("response.failed", {
|
|
1308
1317
|
response: {
|
|
1309
1318
|
...responseSnapshot("failed", finishedItems),
|
|
@@ -1333,6 +1342,7 @@ export function bridgeToResponsesSSE(
|
|
|
1333
1342
|
flushHiddenRawReasoning();
|
|
1334
1343
|
if (currentToolCall) failCurrentToolCall();
|
|
1335
1344
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
1345
|
+
releasePendingWebSources();
|
|
1336
1346
|
options?.onUsage?.(undefined);
|
|
1337
1347
|
await awaitThoughtSignatureDurability();
|
|
1338
1348
|
emit("response.incomplete", {
|
|
@@ -1375,6 +1385,7 @@ export function bridgeToResponsesSSE(
|
|
|
1375
1385
|
flushHiddenRawReasoning();
|
|
1376
1386
|
if (currentToolCall) failCurrentToolCall();
|
|
1377
1387
|
if (currentWebSearch) closeCurrentWebSearch("failed", []);
|
|
1388
|
+
releasePendingWebSources();
|
|
1378
1389
|
// #1926 gap 2 residual: this beat callback is synchronous, so the durability
|
|
1379
1390
|
// barrier is not awaited on the stall-timeout kill path. The in-memory store is
|
|
1380
1391
|
// already updated; only a crash between here and the queued write loses it,
|
|
@@ -1427,6 +1438,7 @@ export function bridgeToResponsesSSE(
|
|
|
1427
1438
|
clearOwnedWatchdog();
|
|
1428
1439
|
if (beat !== undefined) clearBeatInterval(beat);
|
|
1429
1440
|
cancelUpstreamOnce();
|
|
1441
|
+
releasePendingWebSources();
|
|
1430
1442
|
disposeOwnedBudget();
|
|
1431
1443
|
},
|
|
1432
1444
|
});
|
|
@@ -1566,6 +1578,7 @@ function buildResponseJSONWithBudget(
|
|
|
1566
1578
|
const flushText = (inferredPhase?: OcxMessagePhase) => {
|
|
1567
1579
|
if (!currentText) return;
|
|
1568
1580
|
const phase = currentTextPhase ?? inferredPhase;
|
|
1581
|
+
const sourceBytes = pendingWebSources.reduce((sum, source) => sum + bytesOf(JSON.stringify(source)), 0);
|
|
1569
1582
|
const annotations = pendingWebSources.map(s => ({
|
|
1570
1583
|
type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0,
|
|
1571
1584
|
}));
|
|
@@ -1576,6 +1589,7 @@ function buildResponseJSONWithBudget(
|
|
|
1576
1589
|
...(phase ? { phase } : {}),
|
|
1577
1590
|
} as OutputItem;
|
|
1578
1591
|
pushOutput(item, currentTextBytes);
|
|
1592
|
+
budget?.releaseRetained(sourceBytes, { kind: "tool_search_sources" });
|
|
1579
1593
|
currentText = "";
|
|
1580
1594
|
currentTextBytes = 0;
|
|
1581
1595
|
currentTextPhase = undefined;
|
|
@@ -1820,27 +1834,26 @@ function buildResponseJSONWithBudget(
|
|
|
1820
1834
|
// Batch/non-streaming output has no in_progress phase to animate — the search cell is a
|
|
1821
1835
|
// single finalized item, emitted on `end`. Begin is a no-op here.
|
|
1822
1836
|
break;
|
|
1823
|
-
case "web_search_call_end":
|
|
1837
|
+
case "web_search_call_end": {
|
|
1824
1838
|
if (currentText) flushText("commentary");
|
|
1825
1839
|
if (currentSummaryReasoning) flushSummaryReasoning();
|
|
1826
1840
|
if (currentRawReasoning) flushRawReasoning();
|
|
1827
1841
|
flushToolCall();
|
|
1842
|
+
const safeSources = safeWebSearchSources(e.sources);
|
|
1828
1843
|
pushOutput({
|
|
1829
1844
|
type: "web_search_call", id: `ws_${uuid()}`, status: e.status ?? "completed",
|
|
1830
1845
|
action: webSearchAction(e.queries),
|
|
1831
|
-
...(
|
|
1846
|
+
...(safeSources.length > 0 ? { sources: safeSources } : {}),
|
|
1832
1847
|
});
|
|
1833
|
-
if (
|
|
1834
|
-
const
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
seen.add(s.url);
|
|
1838
|
-
budget?.chargeRetained(bytesOf(JSON.stringify(s)), { kind: "tool_search_sources" });
|
|
1839
|
-
pendingWebSources.push(s);
|
|
1848
|
+
if (safeSources.length > 0) {
|
|
1849
|
+
for (const source of safeSources) {
|
|
1850
|
+
if (appendSafeWebSearchSource(pendingWebSources, source)) {
|
|
1851
|
+
budget?.chargeRetained(bytesOf(JSON.stringify(source)), { kind: "tool_search_sources" });
|
|
1840
1852
|
}
|
|
1841
1853
|
}
|
|
1842
1854
|
}
|
|
1843
1855
|
break;
|
|
1856
|
+
}
|
|
1844
1857
|
case "error":
|
|
1845
1858
|
errorEvent = e;
|
|
1846
1859
|
sawTerminal = true;
|
|
@@ -1872,6 +1885,11 @@ function buildResponseJSONWithBudget(
|
|
|
1872
1885
|
if (budget) releaseTranslatedEvent(e, budget);
|
|
1873
1886
|
}
|
|
1874
1887
|
flushText(cleanDone && !errorEvent && !incompleteEvent ? "final_answer" : undefined);
|
|
1888
|
+
if (pendingWebSources.length > 0) {
|
|
1889
|
+
const sourceBytes = pendingWebSources.reduce((sum, source) => sum + bytesOf(JSON.stringify(source)), 0);
|
|
1890
|
+
pendingWebSources = [];
|
|
1891
|
+
budget?.releaseRetained(sourceBytes, { kind: "tool_search_sources" });
|
|
1892
|
+
}
|
|
1875
1893
|
flushSummaryReasoning();
|
|
1876
1894
|
flushRawReasoning();
|
|
1877
1895
|
// Open tool call on a failed/incomplete turn must not land as status:"completed" — and neither
|
package/src/claude/desktop-3p.ts
CHANGED
|
@@ -474,6 +474,12 @@ export function inspectDesktop3pConfigLibrary(
|
|
|
474
474
|
* Select a credential-free standard profile before deleting an owned gateway.
|
|
475
475
|
* The old metadata row remains as a retry locator only until both its profile
|
|
476
476
|
* and backup are absent; successful cleanup removes it in the same operation.
|
|
477
|
+
*
|
|
478
|
+
* `gateway_drifted` is still an owned opencodex gateway (name + valid shape); the
|
|
479
|
+
* fingerprint only says on-disk bytes differ from the last saved marker. Refusing
|
|
480
|
+
* OFF for drift left users unable to disable after a lost `appliedFingerprint`
|
|
481
|
+
* (or any other benign mismatch), while the Integrations card still showed the
|
|
482
|
+
* leftover profile as applied/stale.
|
|
477
483
|
*/
|
|
478
484
|
export function removeDesktop3pStandardPivot(
|
|
479
485
|
options: Desktop3pConfigLibraryOptions & {
|
|
@@ -485,7 +491,7 @@ export function removeDesktop3pStandardPivot(
|
|
|
485
491
|
if (inspected.kind === "not_installed" || inspected.kind === "no_owned_state") {
|
|
486
492
|
return { ok: true, changed: false, kind: "noop", libraryPath: inspected.libraryPath };
|
|
487
493
|
}
|
|
488
|
-
if (inspected.kind === "broken" || inspected.kind === "unsafe"
|
|
494
|
+
if (inspected.kind === "broken" || inspected.kind === "unsafe") {
|
|
489
495
|
return { ok: false, changed: false, kind: "unsafe", libraryPath: inspected.libraryPath, reason: inspected.reason };
|
|
490
496
|
}
|
|
491
497
|
if (!inspected.appliedId || !SAFE_DESKTOP_PROFILE_ID.test(inspected.appliedId)) {
|
|
@@ -496,10 +502,13 @@ export function removeDesktop3pStandardPivot(
|
|
|
496
502
|
try {
|
|
497
503
|
const metadata = parseMetadata(metadataPath);
|
|
498
504
|
const selectedId = inspected.appliedId;
|
|
499
|
-
// When Desktop is actively using our gateway, pivot only
|
|
500
|
-
// first. Any second owned row is residue for a later
|
|
501
|
-
// this preserves the selected-row preference after an
|
|
502
|
-
|
|
505
|
+
// When Desktop is actively using our gateway (current or drifted), pivot only
|
|
506
|
+
// that selected row first. Any second owned row is residue for a later
|
|
507
|
+
// standard-mode retry; this preserves the selected-row preference after an
|
|
508
|
+
// interrupted cleanup.
|
|
509
|
+
const selectedOwnedGatewayActive =
|
|
510
|
+
inspected.kind === "gateway_ours" || inspected.kind === "gateway_drifted";
|
|
511
|
+
const targetIds = selectedOwnedGatewayActive
|
|
503
512
|
? [selectedId]
|
|
504
513
|
: metadata.entries
|
|
505
514
|
.filter(isOwnedDesktopGatewayEntry)
|
|
@@ -508,7 +517,7 @@ export function removeDesktop3pStandardPivot(
|
|
|
508
517
|
if (targetIds.length === 0) return { ok: true, changed: false, kind: "noop", libraryPath: inspected.libraryPath };
|
|
509
518
|
|
|
510
519
|
let metadataAfterPivot = metadata;
|
|
511
|
-
if (
|
|
520
|
+
if (selectedOwnedGatewayActive) {
|
|
512
521
|
const standardId = randomUUID();
|
|
513
522
|
const standardPath = profilePath(inspected.libraryPath, standardId);
|
|
514
523
|
atomicWriteFile(standardPath, "{}\n");
|