@bitkyc08/opencodex 2.88.0 → 2.90.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 +11 -1
- package/bin/ocx.mjs +15 -4
- package/gui/dist/assets/App-DgxkE6Md.js +58 -0
- package/gui/dist/assets/App-dnkJ85te.css +1 -0
- package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-DbTNc_88.js} +1 -1
- package/gui/dist/assets/{index-B5bsvxor.js → index-TmaHa54u.js} +15 -15
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/adapters/anthropic/cache-retention.ts +117 -0
- package/src/adapters/anthropic/passthrough.ts +7 -4
- package/src/adapters/anthropic.ts +10 -3
- package/src/adapters/base.ts +4 -0
- package/src/adapters/claude-cli/adapter.ts +13 -7
- package/src/adapters/claude-cli/usage-admission.ts +14 -2
- package/src/adapters/coding-agent/protocol.ts +6 -0
- package/src/adapters/coding-agent/turn.ts +14 -4
- package/src/adapters/cursor/protobuf-request.ts +3 -0
- package/src/adapters/google-http.ts +3 -2
- package/src/adapters/google-progress-history.ts +19 -1
- package/src/adapters/google-vertex-endpoint.ts +67 -0
- package/src/adapters/google.ts +62 -37
- package/src/adapters/image.ts +5 -3
- package/src/adapters/input-media-guard.ts +5 -2
- package/src/adapters/kiro-refusal.ts +41 -1
- package/src/adapters/ollama-native.ts +1 -0
- package/src/adapters/openai-chat/messages.ts +34 -4
- package/src/adapters/openai-chat/passthrough.ts +37 -4
- package/src/adapters/openai-chat/reasoning-replay.ts +98 -13
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +95 -134
- package/src/adapters/openai-chat/serialized-tool-call-markup.ts +108 -0
- package/src/adapters/openai-chat/standalone-bare-tool-calls.ts +136 -0
- package/src/adapters/openai-chat.ts +7 -6
- package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
- package/src/adapters/openai-responses/opengateway-input.ts +139 -0
- package/src/adapters/openai-responses/passthrough.ts +22 -13
- package/src/adapters/openai-responses/reasoning.ts +68 -1
- package/src/adapters/openai-responses/transport.ts +13 -1
- package/src/adapters/registry.ts +1 -1
- package/src/adapters/upstream-http-error.ts +3 -2
- package/src/adapters/zed.ts +7 -4
- package/src/bridge/internal.ts +11 -1
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +10 -4
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +20 -10
- package/src/claude/desktop-picker-profile.ts +18 -9
- package/src/claude/desktop-remote-store-io.ts +2 -2
- package/src/claude/inbound.ts +12 -26
- package/src/claude/outbound.ts +5 -0
- package/src/cli/account-api.ts +13 -1
- package/src/cli/account-extended.ts +17 -5
- package/src/cli/account-key-quota.ts +4 -1
- package/src/cli/account.ts +1 -1
- package/src/cli/capabilities-agents-routing.ts +45 -8
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-provider-models.ts +5 -4
- package/src/cli/combo-result.ts +8 -2
- package/src/cli/combo.ts +17 -0
- package/src/cli/ensure-desktop-cold-start.ts +2 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/message-command.ts +4 -1
- package/src/cli/message-remote-args.ts +69 -0
- package/src/cli/message-remote-command.ts +77 -0
- package/src/cli/provider-runtime.ts +69 -1
- package/src/cli/registry.ts +3 -3
- package/src/codex/account-lifecycle.ts +19 -6
- package/src/codex/account-pause.ts +6 -1
- package/src/codex/account-runtime-state.ts +5 -0
- package/src/codex/account-store.ts +55 -9
- package/src/codex/account-usability.ts +4 -1
- package/src/codex/auth-api/account-list.ts +13 -5
- package/src/codex/auth-api/account-pause-group.ts +2 -22
- package/src/codex/auth-api/login-collision.ts +59 -0
- package/src/codex/auth-api/login-flow.ts +12 -7
- package/src/codex/auth-api/main-account-probe.ts +3 -2
- package/src/codex/auth-api/pool-mode-gate.ts +81 -13
- package/src/codex/auth-collision.ts +31 -4
- package/src/codex/auth-context.ts +57 -10
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/auto-review.ts +2 -1
- package/src/codex/catalog/build-entries.ts +17 -7
- package/src/codex/catalog/derive-entry.ts +3 -1
- package/src/codex/catalog/metadata.ts +2 -0
- package/src/codex/catalog/model-visibility.ts +0 -2
- package/src/codex/catalog/ownership.ts +71 -0
- package/src/codex/catalog/parsing.ts +12 -3
- package/src/codex/catalog/retained-sync.ts +2 -1
- package/src/codex/catalog/routed-removal.ts +4 -4
- package/src/codex/cli-update-plan.ts +18 -3
- package/src/codex/collision-identity.ts +78 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/low-quota-desktop-notifier.ts +125 -0
- package/src/codex/low-quota-protection.ts +57 -31
- package/src/codex/main-account-cache.ts +77 -3
- package/src/codex/main-account-hard-lock.ts +14 -0
- package/src/codex/main-account.ts +6 -2
- package/src/codex/native-residue.ts +2 -3
- package/src/codex/quota.ts +26 -2
- package/src/codex/routing/cooldown-math.ts +5 -0
- package/src/codex/routing/failure-window.ts +61 -9
- package/src/codex/routing.ts +4 -3
- package/src/codex/shim-diagnostics.ts +2 -1
- package/src/codex/shim-fingerprint.ts +11 -2
- package/src/codex/shim-templates.ts +16 -5
- package/src/codex/shim-windows-refresh.ts +224 -0
- package/src/codex/shim.ts +45 -21
- package/src/combos/failover.ts +10 -2
- package/src/combos/jev-model-backend.ts +30 -7
- package/src/combos/jev-model-request.ts +20 -0
- package/src/combos/jev-quota-activation.ts +153 -0
- package/src/combos/jev-quota-config.ts +62 -0
- package/src/combos/jev-quota-route.ts +26 -0
- package/src/combos/jev-quota.ts +88 -0
- package/src/combos/jev.ts +25 -12
- package/src/combos/types.ts +27 -0
- package/src/compatibility/index.ts +3 -0
- package/src/compatibility/openai-responses.ts +8 -1
- package/src/compatibility/opengateway-responses.ts +61 -0
- package/src/config/account-auto-pause-revisions.ts +136 -0
- package/src/config/diagnostics.ts +13 -0
- package/src/config/live-reconcile.ts +9 -0
- package/src/config/persist-unlocked.ts +10 -1
- package/src/config/provider-validation.ts +7 -2
- package/src/config/removed-opencode-free.ts +135 -0
- package/src/config/schema/account-auto-pause.ts +14 -0
- package/src/config/schema/config-schema.ts +15 -0
- package/src/config/schema/leaf-validators.ts +7 -0
- package/src/config.ts +10 -3
- package/src/generated/compatibility-version.json +476 -216
- package/src/generated/model-metadata.ts +1 -1
- package/src/images/loop.ts +34 -11
- package/src/lib/account-auto-pause-response.ts +16 -0
- package/src/lib/bun-runtime.ts +18 -1
- package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
- package/src/lib/bun-transpiler-cache-scope.mjs +82 -0
- package/src/lib/decision-quota-sink.ts +76 -0
- package/src/lib/provider-admission-wait.ts +14 -0
- package/src/lib/service-secrets.ts +37 -2
- package/src/lib/state-store-registrations.ts +25 -0
- package/src/lib/translator-budget.ts +1 -1
- package/src/lib/upstream-retry.ts +15 -0
- package/src/link/ssh-argv.ts +3 -0
- package/src/messaging/envelope.ts +6 -3
- package/src/messaging/remote-auth.ts +83 -0
- package/src/messaging/remote-bridge.ts +270 -0
- package/src/messaging/remote-contract.ts +108 -0
- package/src/messaging/remote-enrollment-recovery.ts +59 -0
- package/src/messaging/remote-enrollment.ts +244 -0
- package/src/messaging/remote-envelope.ts +40 -0
- package/src/messaging/remote-files.ts +117 -0
- package/src/messaging/remote-owner.ts +169 -0
- package/src/messaging/remote-ports.ts +56 -0
- package/src/messaging/remote-process.ts +131 -0
- package/src/messaging/remote-replies.ts +98 -0
- package/src/messaging/remote-rpc-admission.ts +32 -0
- package/src/messaging/remote-send.ts +72 -0
- package/src/messaging/remote-store.ts +111 -0
- package/src/messaging/remote-tunnels.ts +65 -0
- package/src/messaging/rpc.ts +13 -1
- package/src/oauth/account-auto-pause-policy.ts +66 -0
- package/src/oauth/account-auto-pause.ts +171 -0
- package/src/oauth/account-import/google-antigravity-adapter.ts +38 -2
- package/src/oauth/account-import/parser.ts +15 -2
- package/src/oauth/account-import/types.ts +7 -0
- package/src/oauth/account-quota-rank.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +6 -0
- package/src/oauth/anthropic-model-family.ts +8 -0
- package/src/oauth/anthropic-model-quota.ts +2 -6
- package/src/oauth/api-key-pool-preserve.ts +37 -0
- package/src/oauth/chatgpt-shared.ts +10 -1
- package/src/oauth/dsh.ts +45 -2
- package/src/oauth/index.ts +5 -35
- package/src/oauth/kiro.ts +44 -1
- package/src/oauth/login-flow-state.ts +11 -2
- package/src/oauth/store.ts +35 -8
- package/src/oauth/types.ts +2 -0
- package/src/plugins/upstream-hooks.ts +14 -1
- package/src/providers/api-key-pause.ts +32 -0
- package/src/providers/api-key-resolve.ts +10 -4
- package/src/providers/api-key-selection.ts +6 -1
- package/src/providers/api-keys.ts +8 -4
- package/src/providers/claude-cli-usage.ts +25 -5
- package/src/providers/initial-model-selection.ts +2 -2
- package/src/providers/key-failover.ts +21 -13
- package/src/providers/key-store.ts +8 -3
- package/src/providers/mistral-transport.ts +207 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/opencode-zen-rate-limit.ts +6 -72
- package/src/providers/quota/account-cache.ts +15 -1
- package/src/providers/quota/anthropic-account-quota.ts +20 -7
- package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
- package/src/providers/quota/anthropic-decision-windows.ts +47 -0
- package/src/providers/quota/antigravity.ts +45 -8
- package/src/providers/quota/failure-codes.ts +18 -0
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +15 -6
- package/src/providers/quota-decision-publication.ts +77 -0
- package/src/providers/quota-decision-snapshot.ts +145 -0
- package/src/providers/quota-routing-cache.ts +4 -1
- package/src/providers/quota-types.ts +6 -1
- package/src/providers/quota.ts +8 -3
- package/src/providers/registry/entries-extended.ts +14 -53
- package/src/providers/registry/model-seeds.ts +1 -1
- package/src/providers/registry.ts +1 -2
- package/src/providers/routed-config.ts +1 -1
- package/src/providers/stale-vision-classification-migration.ts +1 -1
- package/src/providers/xai-transport.ts +2 -1
- package/src/responses/audio.ts +46 -0
- package/src/responses/input-media.ts +37 -15
- package/src/responses/parser-content.ts +22 -27
- package/src/responses/parser.ts +18 -10
- package/src/responses/reasoning-retention.ts +218 -11
- package/src/responses/schema.ts +3 -4
- package/src/responses/web-search-replay.ts +147 -0
- package/src/routing/compatibility/behavior.ts +4 -1
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +2 -1
- package/src/server/chat-native.ts +36 -20
- package/src/server/claude-messages.ts +2 -1
- package/src/server/index/optional-listeners.ts +6 -0
- package/src/server/index/serve-options.ts +8 -2
- package/src/server/index/websocket-handler.ts +10 -0
- package/src/server/lifecycle.ts +17 -1
- package/src/server/management/combo-routes.ts +17 -2
- package/src/server/management/oauth-account-routes.ts +12 -2
- package/src/server/management/provider-completion-test.ts +289 -0
- package/src/server/management/provider-routes.ts +9 -1
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/messages-native.ts +29 -4
- package/src/server/messages-shared.ts +4 -1
- package/src/server/request-log.ts +23 -9
- package/src/server/responses/account-auto-pause-recovery.ts +26 -0
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +79 -30
- package/src/server/responses/codex-ws-exchange.ts +3 -0
- package/src/server/responses/codex-ws-metadata.ts +44 -0
- package/src/server/responses/codex-ws-session.ts +21 -1
- package/src/server/responses/combo-session-recall.ts +25 -0
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/combo-tool-loop-pin.ts +70 -0
- package/src/server/responses/compact.ts +10 -5
- package/src/server/responses/context-overflow.ts +141 -2
- package/src/server/responses/core-auth.ts +20 -0
- package/src/server/responses/core-codex-account.ts +28 -15
- package/src/server/responses/core-combo.ts +34 -5
- package/src/server/responses/core-opaque-recovery.ts +20 -11
- package/src/server/responses/core.ts +12 -9
- package/src/server/responses/deepseek-image-files.ts +121 -0
- package/src/server/responses/empty-completion-guard.ts +10 -0
- package/src/server/responses/fetch-helpers.ts +65 -14
- package/src/server/responses/input-admission.ts +6 -2
- package/src/server/responses/jev-model-invoke.ts +13 -14
- package/src/server/responses/main-hard-lock-routing.ts +33 -8
- package/src/server/responses/native-steering-log.ts +2 -2
- package/src/server/responses/opengateway-completion-guard.ts +232 -0
- package/src/server/responses/passthrough-completion.ts +41 -0
- package/src/server/responses/passthrough-delivery.ts +5 -2
- package/src/server/responses/passthrough-dispatch.ts +42 -45
- package/src/server/responses/passthrough-key-rotation.ts +40 -0
- package/src/server/responses/passthrough-observations.ts +38 -0
- package/src/server/responses/reasoning-archive-error.ts +21 -0
- package/src/server/responses/request-compression.ts +98 -0
- package/src/server/responses/request-prepare.ts +11 -1
- package/src/server/responses/request-transport.ts +92 -5
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +16 -3
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/startup-action-control.ts +2 -2
- package/src/server/startup-health-cache.ts +2 -2
- package/src/server/windows-tray-control.ts +2 -2
- package/src/service/definition.ts +9 -1
- package/src/service/orchestration.ts +17 -0
- package/src/types/account-auto-pause.ts +15 -0
- package/src/types/config.ts +23 -1
- package/src/types/provider.ts +11 -5
- package/src/types/request.ts +36 -3
- package/src/types.ts +2 -0
- package/src/usage/cost.ts +17 -1
- package/src/usage/expected-prices.ts +3 -1
- package/src/usage/jev-stats.ts +4 -0
- package/src/usage/log.ts +2 -0
- package/src/usage/summary.ts +7 -0
- package/src/web-search/loop.ts +39 -13
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -3
- package/src/web-search/sidecar-search-replay.ts +96 -0
- package/gui/dist/assets/App-CHC89EXQ.js +0 -58
- package/gui/dist/assets/App-wT3mXqFz.css +0 -1
- package/gui/dist/assets/quota-summary-D8y2wTsB.js +0 -1
|
@@ -114,3 +114,30 @@ export function renderCompactionSearchHistoryItems(items: readonly unknown[], re
|
|
|
114
114
|
if (omissionIndex >= 0) out[omissionIndex] = omissionNote(omitted);
|
|
115
115
|
return out;
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
function isHostedSearchTool(tool: unknown): boolean {
|
|
119
|
+
return isPlainObject(tool) && typeof tool.type === "string" && tool.type.startsWith("web_search");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* True when the outbound body declares a hosted search tool, either top-level or inside a Lite
|
|
124
|
+
* `additional_tools` input wrapper. Read on the final body, after unsupported hosted tools were
|
|
125
|
+
* stripped, so a declaration the destination never sees does not count.
|
|
126
|
+
*/
|
|
127
|
+
export function declaresHostedWebSearch(body: Record<string, unknown>): boolean {
|
|
128
|
+
if (Array.isArray(body.tools) && body.tools.some(isHostedSearchTool)) return true;
|
|
129
|
+
return Array.isArray(body.input) && body.input.some(item => isPlainObject(item)
|
|
130
|
+
&& item.type === "additional_tools" && Array.isArray(item.tools) && item.tools.some(isHostedSearchTool));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Project unreferenced hosted `web_search_call` history into reference notes. A same-input
|
|
135
|
+
* `item_reference` to a hosted cell keeps that cell structural. Returns the original body when
|
|
136
|
+
* there is nothing to project.
|
|
137
|
+
*/
|
|
138
|
+
export function projectHostedSearchHistory(body: Record<string, unknown>): Record<string, unknown> {
|
|
139
|
+
if (!Array.isArray(body.input) || !body.input.some(item => isPlainObject(item) && item.type === "web_search_call")) return body;
|
|
140
|
+
const referencedIds = new Set(body.input.flatMap(item => isPlainObject(item)
|
|
141
|
+
&& item.type === "item_reference" && typeof item.id === "string" ? [item.id] : []));
|
|
142
|
+
return { ...body, input: renderCompactionSearchHistoryItems(body.input, referencedIds) };
|
|
143
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { isPlainObject } from "./internal";
|
|
2
|
+
import { normalizeResponsesToolResultAdjacency } from "./tool-output-recovery";
|
|
3
|
+
|
|
4
|
+
export class OpenGatewayInputCompatibilityError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("OpenGateway cannot preserve this tool result; use text or valid input_image parts with a call_id.");
|
|
7
|
+
this.name = "OpenGatewayInputCompatibilityError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const TOOL_ROUND_TYPES = new Set([
|
|
12
|
+
"function_call",
|
|
13
|
+
"function_call_output",
|
|
14
|
+
"custom_tool_call",
|
|
15
|
+
"custom_tool_call_output",
|
|
16
|
+
"local_shell_call",
|
|
17
|
+
"local_shell_call_output",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function normalizeImageDetail(part: unknown): unknown {
|
|
21
|
+
return isPlainObject(part) && part.type === "input_image" && part.detail === "original"
|
|
22
|
+
? { ...part, detail: "high" }
|
|
23
|
+
: part;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isToolRound(item: unknown): boolean {
|
|
27
|
+
return isPlainObject(item) && typeof item.type === "string" && TOOL_ROUND_TYPES.has(item.type);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pairedCallId(item: Record<string, unknown>): string | undefined {
|
|
31
|
+
return typeof item.call_id === "string" && item.call_id.length > 0 ? item.call_id : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ProjectedInput = { readonly changed: boolean; readonly input: unknown[] };
|
|
35
|
+
|
|
36
|
+
function projectOpenGatewayInput(input: readonly unknown[]): ProjectedInput {
|
|
37
|
+
let changed = false;
|
|
38
|
+
const projected: unknown[] = [];
|
|
39
|
+
const pendingImages: unknown[] = [];
|
|
40
|
+
const flushImages = (): void => {
|
|
41
|
+
if (pendingImages.length === 0) return;
|
|
42
|
+
projected.push(...pendingImages);
|
|
43
|
+
pendingImages.length = 0;
|
|
44
|
+
};
|
|
45
|
+
for (const item of input) {
|
|
46
|
+
if (!isToolRound(item)) flushImages();
|
|
47
|
+
if (!isPlainObject(item)) {
|
|
48
|
+
projected.push(item);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (item.type === "function_call_output" && Array.isArray(item.output)) {
|
|
52
|
+
const callId = pairedCallId(item);
|
|
53
|
+
const content: Record<string, unknown>[] = [];
|
|
54
|
+
const text: string[] = [];
|
|
55
|
+
let supported = callId !== undefined;
|
|
56
|
+
let hasImage = false;
|
|
57
|
+
if (supported) {
|
|
58
|
+
for (const part of item.output) {
|
|
59
|
+
if (!isPlainObject(part)) {
|
|
60
|
+
supported = false;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
switch (part.type) {
|
|
64
|
+
case "text":
|
|
65
|
+
case "input_text":
|
|
66
|
+
case "output_text":
|
|
67
|
+
if (typeof part.text !== "string") {
|
|
68
|
+
supported = false;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
text.push(part.text);
|
|
72
|
+
content.push({ type: "input_text", text: part.text });
|
|
73
|
+
break;
|
|
74
|
+
case "input_image":
|
|
75
|
+
if (!((typeof part.image_url === "string" && part.image_url.length > 0)
|
|
76
|
+
|| (typeof part.file_id === "string" && part.file_id.length > 0))
|
|
77
|
+
|| (part.image_url !== undefined && typeof part.image_url !== "string")
|
|
78
|
+
|| (part.file_id !== undefined && typeof part.file_id !== "string")
|
|
79
|
+
|| (part.detail !== undefined && (typeof part.detail !== "string"
|
|
80
|
+
|| !["auto", "low", "high", "original"].includes(part.detail)))) {
|
|
81
|
+
supported = false;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
content.push({ ...part, ...(part.detail === "original" ? { detail: "high" } : {}) });
|
|
85
|
+
hasImage = true;
|
|
86
|
+
break;
|
|
87
|
+
default:
|
|
88
|
+
supported = false;
|
|
89
|
+
}
|
|
90
|
+
if (!supported) break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (supported && callId !== undefined) {
|
|
94
|
+
changed = true;
|
|
95
|
+
const marker = `[tool output for ${callId}]`;
|
|
96
|
+
projected.push({
|
|
97
|
+
...item,
|
|
98
|
+
output: text.length > 0 ? text.join("\n") : hasImage ? marker : "",
|
|
99
|
+
});
|
|
100
|
+
if (hasImage) {
|
|
101
|
+
pendingImages.push({
|
|
102
|
+
type: "message",
|
|
103
|
+
role: "user",
|
|
104
|
+
content: [{ type: "input_text", text: marker }, ...content],
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
throw new OpenGatewayInputCompatibilityError();
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(item.content)) {
|
|
112
|
+
const originalContent = item.content;
|
|
113
|
+
const content = originalContent.map(normalizeImageDetail);
|
|
114
|
+
if (content.some((part, index) => part !== originalContent[index])) {
|
|
115
|
+
changed = true;
|
|
116
|
+
projected.push({ ...item, content });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
projected.push(item);
|
|
121
|
+
}
|
|
122
|
+
flushImages();
|
|
123
|
+
return { changed, input: projected };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** OpenGateway requires string tool results and auto/low/high image detail. */
|
|
127
|
+
export function normalizeOpenGatewayResponsesInput(body: unknown, url: string): unknown {
|
|
128
|
+
// Same exact-URL boundary as completion recovery; a query-routed variant is a different destination.
|
|
129
|
+
if (url !== "https://apis.opengateway.ai/v1/responses" || !isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
130
|
+
|
|
131
|
+
const direct = projectOpenGatewayInput(body.input);
|
|
132
|
+
if (!direct.changed) return body;
|
|
133
|
+
const aligned = normalizeResponsesToolResultAdjacency(body);
|
|
134
|
+
if (!isPlainObject(aligned) || !Array.isArray(aligned.input) || aligned === body) {
|
|
135
|
+
return { ...body, input: direct.input };
|
|
136
|
+
}
|
|
137
|
+
const paired = projectOpenGatewayInput(aligned.input);
|
|
138
|
+
return { ...aligned, input: paired.changed ? paired.input : direct.input };
|
|
139
|
+
}
|
|
@@ -53,8 +53,9 @@ import { stripMuseSparkUnsupportedWebSearchFields, stripOpenAiOnlyWebSearchField
|
|
|
53
53
|
import { observeOutbound } from "../../usage/cache-diagnostic";
|
|
54
54
|
|
|
55
55
|
import { normalizeMuseToolChoice } from "./muse-tool-choice";
|
|
56
|
-
import { renderCompactionSearchHistoryItems } from "./compaction-search-history";
|
|
56
|
+
import { declaresHostedWebSearch, projectHostedSearchHistory, renderCompactionSearchHistoryItems } from "./compaction-search-history";
|
|
57
57
|
import { stripKimiEmptyAssistantMessages } from "./empty-assistant";
|
|
58
|
+
import { normalizeOpenGatewayResponsesInput } from "./opengateway-input";
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
61
|
* Identifies DeepSeek's strict Responses replay contract: tool-bearing continuations need
|
|
@@ -135,7 +136,8 @@ function usageFromResponsesPayload(payload: unknown): OcxUsage | undefined {
|
|
|
135
136
|
const knownKeys = new Set(["input_tokens", "output_tokens", "total_tokens", "input_tokens_details", "output_tokens_details"]);
|
|
136
137
|
const hasExtras = Object.keys(usage).some(key => !knownKeys.has(key))
|
|
137
138
|
|| (isPlainObject(usage.input_tokens_details)
|
|
138
|
-
&& Object.keys(usage.input_tokens_details).some(key => key !== "cached_tokens" && key !== "cache_write_tokens"
|
|
139
|
+
&& Object.keys(usage.input_tokens_details).some(key => key !== "cached_tokens" && key !== "cache_write_tokens"
|
|
140
|
+
&& key !== "cache_write_1h_tokens"))
|
|
139
141
|
|| (isPlainObject(usage.output_tokens_details)
|
|
140
142
|
&& Object.keys(usage.output_tokens_details).some(key => key !== "reasoning_tokens"));
|
|
141
143
|
if (inputTokens === 0 && outputTokens === 0 && !hasExtras) return undefined;
|
|
@@ -147,6 +149,8 @@ function usageFromResponsesPayload(payload: unknown): OcxUsage | undefined {
|
|
|
147
149
|
...(typeof usage.total_tokens === "number" ? { totalTokens: usage.total_tokens } : {}),
|
|
148
150
|
...(typeof inputDetails?.cached_tokens === "number" ? { cachedInputTokens: inputDetails.cached_tokens } : {}),
|
|
149
151
|
...(typeof inputDetails?.cache_write_tokens === "number" ? { cacheCreationInputTokens: inputDetails.cache_write_tokens } : {}),
|
|
152
|
+
...(typeof inputDetails?.cache_write_1h_tokens === "number"
|
|
153
|
+
? { cacheCreation1hInputTokens: inputDetails.cache_write_1h_tokens } : {}),
|
|
150
154
|
...(typeof outputDetails?.reasoning_tokens === "number" ? { reasoningOutputTokens: outputDetails.reasoning_tokens } : {}),
|
|
151
155
|
...(hasExtras ? { rawUsage: { ...usage } } : {}),
|
|
152
156
|
};
|
|
@@ -492,22 +496,26 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
492
496
|
// here, on the serialized body, not on the parsed selector. One place covers both the
|
|
493
497
|
// HTTP and the WebSocket outbound, because the WS path transports this same request
|
|
494
498
|
// instead of rebuilding it.
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
499
|
+
// Hosted-search history projection, after bridge restoration and tool validation. Two
|
|
500
|
+
// triggers share one bounded renderer, and tool declarations and other input stay intact:
|
|
501
|
+
// 1. The opt-in Codex Lite policy covers ordinary turns and native v2 compaction triggers
|
|
502
|
+
// (#7268). Portable compaction was already projected by the summary builder above.
|
|
503
|
+
// 2. Any request whose final tool surface declares no hosted search. Native Sol answers a
|
|
504
|
+
// replayed web_search_call without a matching declaration with a generic server_error
|
|
505
|
+
// (#7250), so the cells become untrusted reference notes instead of a doomed request.
|
|
506
|
+
if (isPlainObject(finalBody)
|
|
507
|
+
&& ((provider.projectResponsesLiteHostedSearchHistory === true && url === `${CODEX_FORWARD_BASE_URL}/responses`
|
|
508
|
+
&& !parsed._portableCompaction && codexResponsesLiteSelected(headers, finalBody))
|
|
509
|
+
|| !declaresHostedWebSearch(finalBody))) {
|
|
510
|
+
finalBody = projectHostedSearchHistory(finalBody);
|
|
505
511
|
}
|
|
512
|
+
finalBody = normalizeOpenGatewayResponsesInput(finalBody, url);
|
|
506
513
|
observeOutbound(parsed._rawBody, finalBody, headers);
|
|
507
514
|
if (!isCanonicalOpenAiForwardProvider(provider)) {
|
|
508
515
|
validateFinalCustomToolCompatibility(finalBody, provider.supportsResponsesCustomTools);
|
|
509
516
|
}
|
|
510
|
-
const
|
|
517
|
+
const wireBody = applyGithubCopilotContextTier(finalBody, provider, parsed.modelId, incoming.providerName);
|
|
518
|
+
const body = JSON.stringify(wireBody);
|
|
511
519
|
const releaseBodyObservation = translatorBudget.observeExternallyCapped(
|
|
512
520
|
"passthrough_serialization",
|
|
513
521
|
Buffer.byteLength(body, "utf8"),
|
|
@@ -517,6 +525,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
517
525
|
method: "POST",
|
|
518
526
|
headers,
|
|
519
527
|
body,
|
|
528
|
+
...(isPlainObject(wireBody) && typeof wireBody.model === "string" ? { outboundModelId: wireBody.model } : {}),
|
|
520
529
|
releaseBodyObservation,
|
|
521
530
|
...(convertedRoutedCustomToolNames ? { convertedRoutedCustomToolNames } : {}),
|
|
522
531
|
...(routedCustomToolRepairNames ? { routedCustomToolRepairNames } : {}),
|
|
@@ -163,7 +163,74 @@ export function sanitizeReasoningInputContent(
|
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
if (blankedPlaintext) opts?.onPlaintextReasoningBlanked?.();
|
|
166
|
-
|
|
166
|
+
// Runs after the per-item pass so an item it emptied has already been backfilled, and a
|
|
167
|
+
// placeholder is only invented for a turn that carries no reasoning item at all.
|
|
168
|
+
const withTurnReasoning = opts?.requirePlaintextReasoning === true
|
|
169
|
+
? backfillBareToolCallTurnReasoning(input)
|
|
170
|
+
: input;
|
|
171
|
+
if (withTurnReasoning !== input) changed = true;
|
|
172
|
+
return changed ? { ...raw, input: withTurnReasoning } : body;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Assistant-authored history items: what DeepSeek merges into one assistant message. */
|
|
176
|
+
function isAssistantTurnItem(item: unknown): boolean {
|
|
177
|
+
if (!isPlainObject(item) || typeof item.type !== "string") return false;
|
|
178
|
+
if (item.type === "reasoning") return true;
|
|
179
|
+
if (item.type === "message") return item.role === "assistant";
|
|
180
|
+
return item.type.endsWith("_call");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isClientToolCallItem(item: unknown): boolean {
|
|
184
|
+
return isPlainObject(item)
|
|
185
|
+
&& (item.type === "function_call" || item.type === "custom_tool_call" || item.type === "local_shell_call");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Give every assistant tool-call turn that has NO reasoning item a minimal plaintext one (#7298).
|
|
190
|
+
*
|
|
191
|
+
* A Combo can serve the turn that issued a tool call on one model and the continuation on
|
|
192
|
+
* another. When the issuing model was a GPT turn that produced no reasoning item (or the item
|
|
193
|
+
* was dropped upstream), the strict destination sees a bare `function_call` and DeepSeek's
|
|
194
|
+
* thinking mode refuses the whole request with `The reasoning_text in the thinking mode must be
|
|
195
|
+
* passed back to the API`. The per-item backfill above cannot help, because there is no item to
|
|
196
|
+
* backfill. This is the Responses-wire twin of the Chat adapter's `reasoning_content: " "`
|
|
197
|
+
* placeholder (#5421): the same single-space text, never fabricated reasoning, inserted at the
|
|
198
|
+
* head of the turn so it precedes the call exactly where a native turn's reasoning would be.
|
|
199
|
+
*
|
|
200
|
+
* A turn is a maximal run of assistant-authored items; any user message or tool output ends it.
|
|
201
|
+
* Turns that already carry a reasoning item, and turns with no client tool call, are untouched.
|
|
202
|
+
*/
|
|
203
|
+
export function backfillBareToolCallTurnReasoning(input: unknown[]): unknown[] {
|
|
204
|
+
const bareTurnStarts = new Set<number>();
|
|
205
|
+
let turnStart = -1;
|
|
206
|
+
let turnHasReasoning = false;
|
|
207
|
+
let turnHasToolCall = false;
|
|
208
|
+
const closeTurn = (): void => {
|
|
209
|
+
if (turnStart >= 0 && turnHasToolCall && !turnHasReasoning) bareTurnStarts.add(turnStart);
|
|
210
|
+
turnStart = -1;
|
|
211
|
+
turnHasReasoning = false;
|
|
212
|
+
turnHasToolCall = false;
|
|
213
|
+
};
|
|
214
|
+
for (let index = 0; index < input.length; index++) {
|
|
215
|
+
const item = input[index];
|
|
216
|
+
if (!isAssistantTurnItem(item)) {
|
|
217
|
+
closeTurn();
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (turnStart < 0) turnStart = index;
|
|
221
|
+
if ((item as Record<string, unknown>).type === "reasoning") turnHasReasoning = true;
|
|
222
|
+
if (isClientToolCallItem(item)) turnHasToolCall = true;
|
|
223
|
+
}
|
|
224
|
+
closeTurn();
|
|
225
|
+
if (bareTurnStarts.size === 0) return input;
|
|
226
|
+
const result: unknown[] = [];
|
|
227
|
+
for (let index = 0; index < input.length; index++) {
|
|
228
|
+
if (bareTurnStarts.has(index)) {
|
|
229
|
+
result.push({ type: "reasoning", summary: [], content: [{ type: "reasoning_text", text: " " }] });
|
|
230
|
+
}
|
|
231
|
+
result.push(input[index]);
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
167
234
|
}
|
|
168
235
|
|
|
169
236
|
/**
|
|
@@ -23,6 +23,18 @@ export function copyResponsesClientMetadata(target: Headers, incoming: Headers,
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Strip trailing slashes with a linear scan rather than a regex.
|
|
28
|
+
*
|
|
29
|
+
* `/\/+$/` retries candidate end positions when a long slash run is not at the end of the
|
|
30
|
+
* string; the scan walks each character once and produces the same result on every input.
|
|
31
|
+
*/
|
|
32
|
+
function stripTrailingSlashes(value: string): string {
|
|
33
|
+
let end = value.length;
|
|
34
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
|
|
35
|
+
return end === value.length ? value : value.slice(0, end);
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
/** Shared Responses URL/auth/header construction; capabilities never grant auth authority. */
|
|
27
39
|
export function buildResponsesTransport(provider: OcxProviderConfig, incoming: Pick<IncomingMeta, "headers">): {
|
|
28
40
|
url: string; headers: Record<string, string>;
|
|
@@ -31,7 +43,7 @@ export function buildResponsesTransport(provider: OcxProviderConfig, incoming: P
|
|
|
31
43
|
let url: string;
|
|
32
44
|
if (provider.authMode === "forward") {
|
|
33
45
|
const mayForwardCallerCredentials = isCanonicalOpenAiForwardProvider(provider);
|
|
34
|
-
const baseUrl = mayForwardCallerCredentials ? CODEX_FORWARD_BASE_URL : provider.baseUrl
|
|
46
|
+
const baseUrl = mayForwardCallerCredentials ? CODEX_FORWARD_BASE_URL : stripTrailingSlashes(provider.baseUrl);
|
|
35
47
|
url = `${baseUrl}/responses`;
|
|
36
48
|
if (provider.headers) Object.assign(headers, provider.headers);
|
|
37
49
|
const runtimeProvider = provider as {
|
package/src/adapters/registry.ts
CHANGED
|
@@ -138,7 +138,7 @@ export const ADAPTER_REGISTRY = {
|
|
|
138
138
|
zed: {
|
|
139
139
|
wire: "zed",
|
|
140
140
|
mutation: "codex-owned",
|
|
141
|
-
create: (provider: OcxProviderConfig,
|
|
141
|
+
create: (provider: OcxProviderConfig, context: AdapterFactoryContext) => createZedAdapter(provider, context.cacheRetention),
|
|
142
142
|
},
|
|
143
143
|
"mimo-free": {
|
|
144
144
|
contractParent: "openai-chat",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inheritAccountAutoPauseResponse } from "../lib/account-auto-pause-response";
|
|
1
2
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
2
3
|
import { redactSecretString } from "../lib/redact";
|
|
3
4
|
|
|
@@ -40,9 +41,9 @@ export async function normalizeUpstreamHttpErrorResponse(
|
|
|
40
41
|
const headers = new Headers(res.headers);
|
|
41
42
|
headers.delete("content-encoding");
|
|
42
43
|
headers.delete("content-length");
|
|
43
|
-
return new Response(await opts.formatMessage(payloadText), {
|
|
44
|
+
return inheritAccountAutoPauseResponse(res, new Response(await opts.formatMessage(payloadText), {
|
|
44
45
|
status: res.status,
|
|
45
46
|
statusText: res.statusText,
|
|
46
47
|
headers,
|
|
47
|
-
});
|
|
48
|
+
}));
|
|
48
49
|
}
|
package/src/adapters/zed.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "./base";
|
|
3
3
|
import { createAnthropicAdapter } from "./anthropic";
|
|
4
|
+
import type { AnthropicCacheRetention } from "./anthropic/cache-retention";
|
|
4
5
|
import { createGoogleAdapter } from "./google";
|
|
5
6
|
import { createOpenAIChatAdapter } from "./openai-chat";
|
|
6
7
|
import { createResponsesPassthroughAdapter } from "./openai-responses";
|
|
@@ -218,15 +219,17 @@ export function zedEventStream(
|
|
|
218
219
|
return body.pipeThrough(output);
|
|
219
220
|
}
|
|
220
221
|
|
|
221
|
-
function createDelegate(provider: OcxProviderConfig, zedProvider: ZedProvider): ProviderAdapter {
|
|
222
|
+
function createDelegate(provider: OcxProviderConfig, zedProvider: ZedProvider, cacheRetention?: AnthropicCacheRetention): ProviderAdapter {
|
|
222
223
|
const config = delegateProvider(provider, zedProvider);
|
|
223
|
-
|
|
224
|
+
// The delegate only borrows the api.anthropic.com body builder; the wire goes to Zed's
|
|
225
|
+
// gateway, which does not document the 1h TTL, so an omitted retention stays 5m here.
|
|
226
|
+
if (zedProvider === "anthropic") return createAnthropicAdapter(config, cacheRetention ?? "short");
|
|
224
227
|
if (zedProvider === "google") return createGoogleAdapter(config);
|
|
225
228
|
if (zedProvider === "open_ai") return createResponsesPassthroughAdapter(config);
|
|
226
229
|
return createOpenAIChatAdapter(config);
|
|
227
230
|
}
|
|
228
231
|
|
|
229
|
-
export function createZedAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
232
|
+
export function createZedAdapter(provider: OcxProviderConfig, cacheRetention?: AnthropicCacheRetention): ProviderAdapter {
|
|
230
233
|
let delegate: ZedDelegate | undefined;
|
|
231
234
|
let credentials: ZedCredentials | undefined;
|
|
232
235
|
|
|
@@ -246,7 +249,7 @@ export function createZedAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
|
246
249
|
/* Model inference fallback below; a transient catalog outage must not block passthrough. */
|
|
247
250
|
}
|
|
248
251
|
const zedProvider = providerFromCatalog(catalog, parsed.modelId);
|
|
249
|
-
const selected = createDelegate(provider, zedProvider);
|
|
252
|
+
const selected = createDelegate(provider, zedProvider, cacheRetention);
|
|
250
253
|
const safeIncoming: IncomingMeta = {
|
|
251
254
|
headers: incoming?.headers ?? new Headers(),
|
|
252
255
|
translatorBudget: incoming?.translatorBudget ?? createTranslatorBudget(),
|
package/src/bridge/internal.ts
CHANGED
|
@@ -23,6 +23,11 @@ export function uuid(): string {
|
|
|
23
23
|
return crypto.randomUUID().replace(/-/g, "");
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** The client-visible id of a hosted search cell: the producer's `ws_` id when well-formed, else a fresh one. */
|
|
27
|
+
export function webSearchCellItemId(candidate: string | undefined): string {
|
|
28
|
+
return typeof candidate === "string" && /^ws_[A-Za-z0-9_-]{1,128}$/.test(candidate) ? candidate : `ws_${uuid()}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
/** Test-only: bound the abandoned-owned-budget watchdog delay (null restores). */
|
|
27
32
|
export let ownedBudgetAbandonedMs = 10 * 60 * 1000;
|
|
28
33
|
const OWNED_BUDGET_ABANDONED_DEFAULT_MS = ownedBudgetAbandonedMs;
|
|
@@ -64,7 +69,7 @@ export function responsesUsage(usage: OcxUsage | undefined): Record<string, unkn
|
|
|
64
69
|
// normalized contract).
|
|
65
70
|
const rawInputDetails = isRecord(raw.input_tokens_details)
|
|
66
71
|
? Object.fromEntries(Object.entries(raw.input_tokens_details as Record<string, unknown>)
|
|
67
|
-
.filter(([key]) => key !== "cache_write_tokens"))
|
|
72
|
+
.filter(([key]) => key !== "cache_write_tokens" && key !== "cache_write_1h_tokens"))
|
|
68
73
|
: {} as Record<string, unknown>;
|
|
69
74
|
const rawOutputDetails = isRecord(raw.output_tokens_details)
|
|
70
75
|
? raw.output_tokens_details as Record<string, unknown>
|
|
@@ -92,6 +97,11 @@ export function responsesUsage(usage: OcxUsage | undefined): Record<string, unkn
|
|
|
92
97
|
usage.cacheCreationInputTokens,
|
|
93
98
|
Math.max(0, inputTokens - cacheRead),
|
|
94
99
|
);
|
|
100
|
+
// The 1-hour-TTL share of the write (Anthropic cache_creation split), only when reported,
|
|
101
|
+
// so a translated Messages client can price it at the 1h rate.
|
|
102
|
+
if (usage.cacheCreation1hInputTokens !== undefined) {
|
|
103
|
+
inputDetails.cache_write_1h_tokens = Math.min(usage.cacheCreation1hInputTokens, inputDetails.cache_write_tokens as number);
|
|
104
|
+
}
|
|
95
105
|
}
|
|
96
106
|
out.input_tokens_details = inputDetails;
|
|
97
107
|
out.output_tokens_details = { ...rawOutputDetails, reasoning_tokens: usage.reasoningOutputTokens ?? 0 };
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
type TranslatorBudget,
|
|
43
43
|
type TranslatorBufferKind,
|
|
44
44
|
} from "../lib/translator-budget";
|
|
45
|
-
import { adapterFailureFromEvent, emptyChunks, joinChunks, responsesUsage, toolCallArgumentsUsable, uuid, webSearchAction } from "./internal";
|
|
45
|
+
import { adapterFailureFromEvent, emptyChunks, joinChunks, responsesUsage, toolCallArgumentsUsable, uuid, webSearchAction, webSearchCellItemId } from "./internal";
|
|
46
46
|
import type { OutputItem, StringChunks } from "./internal";
|
|
47
47
|
import { bridgeToResponsesSSE } from "./sse";
|
|
48
48
|
|
|
@@ -546,7 +546,7 @@ function buildResponseJSONWithBudget(
|
|
|
546
546
|
flushToolCall();
|
|
547
547
|
const safeSources = safeWebSearchSources(e.sources);
|
|
548
548
|
pushOutput({
|
|
549
|
-
type: "web_search_call", id:
|
|
549
|
+
type: "web_search_call", id: webSearchCellItemId(e.itemId), status: e.status ?? "completed",
|
|
550
550
|
action: webSearchAction(e.queries, e.anthropic),
|
|
551
551
|
...(safeSources.length > 0 ? { sources: safeSources } : {}),
|
|
552
552
|
...(e.anthropic ? { anthropic_blocks: e.anthropic } : {}),
|
package/src/bridge/sse.ts
CHANGED
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
type TranslatorBudget,
|
|
51
51
|
type TranslatorBufferKind,
|
|
52
52
|
} from "../lib/translator-budget";
|
|
53
|
-
import { adapterFailureFromEvent, emptyChunks, joinChunks, ownedBudgetAbandonedMs, responsesUsage, toolCallArgumentsCouldBeJson, toolCallArgumentsUsable, uuid, webSearchAction } from "./internal";
|
|
53
|
+
import { adapterFailureFromEvent, emptyChunks, joinChunks, ownedBudgetAbandonedMs, responsesUsage, toolCallArgumentsCouldBeJson, toolCallArgumentsUsable, uuid, webSearchAction, webSearchCellItemId } from "./internal";
|
|
54
54
|
import type { OutputItem, StringChunks } from "./internal";
|
|
55
55
|
import { terminalRefusalFailure } from "./terminal-refusal";
|
|
56
56
|
|
|
@@ -770,9 +770,14 @@ export function bridgeToResponsesSSE(
|
|
|
770
770
|
returnIterator();
|
|
771
771
|
};
|
|
772
772
|
let handlingTranslatorOverflow = false;
|
|
773
|
-
terminateForTranslatorOverflow =
|
|
773
|
+
terminateForTranslatorOverflow = error => {
|
|
774
774
|
if (handlingTranslatorOverflow || terminated || clientCancelled || closed) return;
|
|
775
775
|
handlingTranslatorOverflow = true;
|
|
776
|
+
// An adapter-raised overflow event carries the usage the turn already consumed; the
|
|
777
|
+
// provider billed it, so it must not be logged as zero.
|
|
778
|
+
const usage = (error as AdapterEvent | null | undefined)?.type === "error"
|
|
779
|
+
? (error as Extract<AdapterEvent, { type: "error" }>).usage : undefined;
|
|
780
|
+
if (usage) options?.onUsage?.(usage);
|
|
776
781
|
abortCurrentToolCallForTranslatorOverflow();
|
|
777
782
|
currentWebSearch = null;
|
|
778
783
|
releasePendingWebSources();
|
|
@@ -788,6 +793,7 @@ export function bridgeToResponsesSSE(
|
|
|
788
793
|
sequence_number: seq++,
|
|
789
794
|
response: {
|
|
790
795
|
...responseSnapshot("failed", finishedItems),
|
|
796
|
+
...(usage ? { usage: responsesUsage(usage) } : {}),
|
|
791
797
|
error: failure,
|
|
792
798
|
last_error: failure,
|
|
793
799
|
},
|
|
@@ -1211,7 +1217,7 @@ export function bridgeToResponsesSSE(
|
|
|
1211
1217
|
flushHiddenRawReasoning();
|
|
1212
1218
|
if (currentToolCall) closeCurrentToolCall();
|
|
1213
1219
|
if (currentWebSearch) closeCurrentWebSearch("completed", []);
|
|
1214
|
-
const wsItemId =
|
|
1220
|
+
const wsItemId = webSearchCellItemId(event.itemId);
|
|
1215
1221
|
emit("response.output_item.added", {
|
|
1216
1222
|
output_index: outputIndex,
|
|
1217
1223
|
item: { type: "web_search_call", id: wsItemId, status: "in_progress" },
|
|
@@ -1224,7 +1230,7 @@ export function bridgeToResponsesSSE(
|
|
|
1224
1230
|
// (defensive), synthesize the added frame first so the done has a matching item.
|
|
1225
1231
|
if (!currentWebSearch || currentWebSearch.eventId !== event.id) {
|
|
1226
1232
|
if (currentWebSearch) closeCurrentWebSearch("completed", []);
|
|
1227
|
-
const wsItemId2 =
|
|
1233
|
+
const wsItemId2 = webSearchCellItemId(event.itemId);
|
|
1228
1234
|
emit("response.output_item.added", {
|
|
1229
1235
|
output_index: outputIndex,
|
|
1230
1236
|
item: { type: "web_search_call", id: wsItemId2, status: "in_progress" },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { closeSync, constants, fstatSync, lstatSync, openSync,
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, type BigIntStats } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { claudeDesktopConfigLibraryDir, resolveConfigLibraryDir } from "./desktop-3p-paths";
|
|
@@ -44,6 +44,20 @@ export interface Desktop3pMetadata {
|
|
|
44
44
|
|
|
45
45
|
export const DESKTOP_PICKER_ENTRY_NAME = "opencodex-picker";
|
|
46
46
|
|
|
47
|
+
export const MAX_DESKTOP_PROFILE_FILE_BYTES = 4 * 1024 * 1024;
|
|
48
|
+
|
|
49
|
+
export function assertDesktopLibraryTextSize(text: string): void {
|
|
50
|
+
if (Buffer.byteLength(text, "utf8") > MAX_DESKTOP_PROFILE_FILE_BYTES) {
|
|
51
|
+
throw new Error("desktop_config_too_large");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function serializeDesktopLibraryJson(value: unknown): string {
|
|
56
|
+
const text = JSON.stringify(value, null, 2) + "\n";
|
|
57
|
+
assertDesktopLibraryTextSize(text);
|
|
58
|
+
return text;
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
/** A path that no longer names the observed file was swapped; a same-path failure keeps its own errno. */
|
|
48
62
|
function pathStillNamesExpectedFile(path: string, expected: BigIntStats): boolean {
|
|
49
63
|
try {
|
|
@@ -54,7 +68,13 @@ function pathStillNamesExpectedFile(path: string, expected: BigIntStats): boolea
|
|
|
54
68
|
}
|
|
55
69
|
|
|
56
70
|
/** Read regular config files only; a path swap must not turn an observation into a FIFO wait. */
|
|
57
|
-
export function readDesktopLibraryBytes(
|
|
71
|
+
export function readDesktopLibraryBytes(
|
|
72
|
+
path: string,
|
|
73
|
+
maxBytes: number = MAX_DESKTOP_PROFILE_FILE_BYTES,
|
|
74
|
+
): Buffer {
|
|
75
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > MAX_DESKTOP_PROFILE_FILE_BYTES) {
|
|
76
|
+
throw new RangeError("invalid Desktop library read limit");
|
|
77
|
+
}
|
|
58
78
|
const expected = lstatSync(path, { bigint: true });
|
|
59
79
|
if (!expected.isFile()) throw new Error("desktop_config_not_regular");
|
|
60
80
|
// POSIX open of a FIFO can itself block, before fstat is possible. Windows has no POSIX
|
|
@@ -73,7 +93,27 @@ export function readDesktopLibraryBytes(path: string): Buffer {
|
|
|
73
93
|
const opened = fstatSync(fd, { bigint: true });
|
|
74
94
|
if (!opened.isFile() || opened.dev !== expected.dev || opened.ino !== expected.ino
|
|
75
95
|
|| opened.ctimeNs !== expected.ctimeNs) throw new Error("desktop_config_not_regular");
|
|
76
|
-
|
|
96
|
+
if (opened.size > BigInt(maxBytes)) throw new Error("desktop_config_too_large");
|
|
97
|
+
// Allocate for the observed size plus one sentinel byte, not the full cap: callers scan many
|
|
98
|
+
// small profiles, and the cap would zero-fill megabytes per read. A file that fills the
|
|
99
|
+
// buffer grew after validation — keep reading within the bound so it classifies as
|
|
100
|
+
// desktop_config_too_large rather than silently truncating.
|
|
101
|
+
let bytes = Buffer.alloc(Number(opened.size) + 1);
|
|
102
|
+
let length = 0;
|
|
103
|
+
for (;;) {
|
|
104
|
+
const count = readSync(fd, bytes, length, Math.min(64 * 1024, bytes.length - length), null);
|
|
105
|
+
if (count === 0) break;
|
|
106
|
+
length += count;
|
|
107
|
+
if (length > maxBytes) throw new Error("desktop_config_too_large");
|
|
108
|
+
if (length === bytes.length && bytes.length <= maxBytes) {
|
|
109
|
+
const grown = Buffer.alloc(Math.min(maxBytes + 1, bytes.length * 2));
|
|
110
|
+
bytes.copy(grown, 0, 0, length);
|
|
111
|
+
bytes = grown;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (length > maxBytes) throw new Error("desktop_config_too_large");
|
|
115
|
+
if (!pathStillNamesExpectedFile(path, opened)) throw new Error("desktop_config_not_regular");
|
|
116
|
+
return bytes.subarray(0, length);
|
|
77
117
|
} finally {
|
|
78
118
|
closeSync(fd);
|
|
79
119
|
}
|