@yansigit/opencodex 2.31.3 → 2.33.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 +2 -2
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-DKLr4LTE.js +102 -0
- package/gui/dist/assets/index-DrSQdTRd.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +6 -5
- package/src/adapters/anthropic.ts +25 -13
- package/src/adapters/azure.ts +20 -4
- package/src/adapters/base.ts +5 -1
- package/src/adapters/command-code.ts +42 -10
- package/src/adapters/cursor/live-models.ts +8 -0
- package/src/adapters/cursor/live-transport.ts +1 -1
- package/src/adapters/cursor/native-exec-desktop.ts +16 -0
- package/src/adapters/cursor/protobuf-events.ts +158 -7
- package/src/adapters/cursor/protobuf-request.ts +33 -15
- package/src/adapters/cursor/request-builder.ts +4 -3
- package/src/adapters/cursor/tool-definitions.ts +27 -1
- package/src/adapters/cursor/types.ts +4 -3
- package/src/adapters/cursor.ts +9 -0
- package/src/adapters/google-antigravity-replay.ts +2 -2
- package/src/adapters/google-antigravity-wire.ts +7 -0
- package/src/adapters/google-errors.ts +6 -2
- package/src/adapters/google-http.ts +30 -7
- package/src/adapters/google-truncation.ts +5 -0
- package/src/adapters/google-wire-compiler.ts +38 -6
- package/src/adapters/google.ts +154 -33
- package/src/adapters/kiro-tools.ts +20 -9
- package/src/adapters/kiro.ts +0 -3
- package/src/adapters/openai-chat.ts +9 -0
- package/src/adapters/openai-responses.ts +4 -1
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-web-search.ts +7 -2
- package/src/bridge.ts +133 -24
- package/src/claude/context-windows.ts +16 -9
- package/src/cli/dispatch.ts +50 -2
- package/src/cli/doctor.ts +26 -13
- package/src/cli/help.ts +4 -3
- package/src/cli/index.ts +20 -6
- package/src/cli/models.ts +13 -3
- package/src/cli/observe.ts +20 -5
- package/src/cli/provider.ts +8 -1
- package/src/cli/registry.ts +7 -5
- package/src/cli/status.ts +25 -1
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +134 -0
- package/src/codex/app-server-processes.ts +3 -1
- package/src/codex/auth-api.ts +4 -2
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +12 -0
- package/src/codex/catalog/effort.ts +42 -12
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/model-metadata.ts +593 -0
- package/src/codex/catalog/parsing.ts +71 -27
- package/src/codex/catalog/provider-fetch.ts +189 -33
- package/src/codex/catalog/sync.ts +6 -5
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-app-restart.ts +342 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject.ts +49 -21
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/shim.ts +56 -3
- package/src/combos/failover.ts +27 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +81 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +308 -0
- package/src/config/provider-validation.ts +214 -0
- package/src/config.ts +153 -814
- package/src/generated/compatibility-version.json +232 -148
- package/src/images/loop.ts +37 -6
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +141 -0
- package/src/integrations/ownership.ts +10 -0
- package/src/integrations/state.ts +44 -5
- package/src/integrations/writer.ts +6 -0
- package/src/lib/azure-identity.ts +154 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/debug.ts +42 -0
- package/src/lib/errors.ts +14 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/provider-outbound.ts +45 -33
- package/src/lib/provider-tls-profile.ts +309 -0
- package/src/lib/proxy-env.ts +49 -0
- package/src/lib/redact.ts +10 -1
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/antigravity-routing.ts +282 -236
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/command-code.ts +5 -16
- package/src/oauth/google-antigravity.ts +42 -5
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +15 -3
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +6 -0
- package/src/providers/antigravity-quota.ts +3 -1
- package/src/providers/api-keys.ts +2 -1
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/key-failover.ts +5 -1
- package/src/providers/openai-tiers.ts +5 -0
- package/src/providers/provider-id-rewrite.ts +1 -0
- package/src/providers/quota.ts +59 -13
- package/src/providers/registry.ts +4 -2
- package/src/providers/request-pacing.ts +33 -6
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/custom-tool-compat.ts +132 -38
- package/src/responses/google-provider-options.ts +36 -0
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +14 -2
- package/src/responses/provider-opaque-metadata.ts +3 -3
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +37 -0
- package/src/responses/state.ts +94 -4
- package/src/router.ts +8 -2
- package/src/server/auth-cors.ts +37 -7
- package/src/server/images.ts +19 -35
- package/src/server/index.ts +102 -21
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +206 -16
- package/src/server/management/combo-routes.ts +6 -0
- package/src/server/management/config-routes.ts +35 -6
- package/src/server/management/context.ts +1 -1
- package/src/server/management/logs-usage-routes.ts +27 -6
- package/src/server/management/model-routes.ts +8 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +25 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +113 -15
- package/src/server/management/routing-profile-routes.ts +3 -0
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +2 -1
- package/src/server/request-log-conversation.ts +30 -0
- package/src/server/request-log.ts +21 -0
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/combo-stream-preflight.ts +171 -0
- package/src/server/responses/compact.ts +36 -22
- package/src/server/responses/core.ts +584 -247
- package/src/server/responses/empty-completion-guard.ts +35 -6
- package/src/server/responses/fetch-helpers.ts +20 -102
- package/src/server/responses/v2-native-parent-override.ts +59 -0
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +41 -5
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/service.ts +9 -5
- package/src/types/config.ts +16 -1
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +34 -1
- package/src/types/tools.ts +114 -11
- package/src/types.ts +7 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/log.ts +16 -8
- package/src/usage/summary.ts +201 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/gemini-executor.ts +6 -4
- package/src/web-search/loop.ts +42 -6
- package/gui/dist/assets/index-CGoDO3uO.css +0 -1
- package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
package/src/responses/parser.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
OcxToolCall,
|
|
12
12
|
OcxReasoningReplayScopeRef,
|
|
13
13
|
} from "../types";
|
|
14
|
-
import {
|
|
14
|
+
import { createToolChoiceResolver, namespacedToolName } from "../types";
|
|
15
15
|
import { responsesRequestSchema } from "./schema";
|
|
16
16
|
import { providerMetadataFromResponsesFunctionCall } from "./provider-opaque-metadata";
|
|
17
17
|
import { lookupReplayThoughtSignature } from "./thought-signature-replay";
|
|
@@ -758,8 +758,9 @@ export function parseRequest(
|
|
|
758
758
|
const tc = mapToolChoice(data.tool_choice);
|
|
759
759
|
if (tc && typeof tc === "object") {
|
|
760
760
|
const selectors = "allowedTools" in tc ? tc.allowedTools : [tc.name];
|
|
761
|
+
const resolver = createToolChoiceResolver(mergedTools);
|
|
761
762
|
for (const selector of selectors) {
|
|
762
|
-
if (
|
|
763
|
+
if (resolver.candidateCount(selector) > 1) {
|
|
763
764
|
throw new Error(`ambiguous tool_choice name: ${selector}`);
|
|
764
765
|
}
|
|
765
766
|
}
|
|
@@ -780,6 +781,17 @@ export function parseRequest(
|
|
|
780
781
|
if (data.frequency_penalty !== undefined) options.frequencyPenalty = data.frequency_penalty;
|
|
781
782
|
if (data.service_tier !== undefined) options.serviceTier = data.service_tier;
|
|
782
783
|
if (data.prompt_cache_key !== undefined) options.promptCacheKey = data.prompt_cache_key;
|
|
784
|
+
if (data.provider_options?.google) {
|
|
785
|
+
const google = data.provider_options.google;
|
|
786
|
+
options.providerOptions = {
|
|
787
|
+
google: {
|
|
788
|
+
...(google.thinking_budget !== undefined ? { thinkingBudget: google.thinking_budget } : {}),
|
|
789
|
+
...(google.include_thoughts !== undefined ? { includeThoughts: google.include_thoughts } : {}),
|
|
790
|
+
...(google.safety_settings !== undefined ? { safetySettings: google.safety_settings } : {}),
|
|
791
|
+
...(google.cached_content !== undefined ? { cachedContent: google.cached_content } : {}),
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
}
|
|
783
795
|
|
|
784
796
|
// Stash the hosted web_search config (if Codex enabled it) so the proxy can run searches via the
|
|
785
797
|
// gpt-mini sidecar for routed providers. buildTools still drops the hosted tool; the sidecar path
|
|
@@ -24,11 +24,11 @@ function isObj(value: unknown): value is Record<string, unknown> {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* Ceiling on stored thought signatures. Accommodates deep thinking models (e.g. Gemini 3.7
|
|
28
|
+
* Flash up to 64k tokens of reasoning, whose signatures can exceed 100 KiB) while bounding
|
|
29
29
|
* state through history replay.
|
|
30
30
|
*/
|
|
31
|
-
const MAX_SIGNATURE_BYTES =
|
|
31
|
+
const MAX_SIGNATURE_BYTES = 1024 * 1024;
|
|
32
32
|
|
|
33
33
|
export function isCarryableSignature(value: unknown): value is string {
|
|
34
34
|
if (typeof value !== "string" || value.length === 0) return false;
|
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
const MAX_ENTRIES = 64;
|
|
30
30
|
const MAX_TOTAL_BYTES = 256 * 1024;
|
|
31
31
|
const TTL_MS = 60 * 60 * 1000;
|
|
32
|
+
const OPAQUE_BLOB_REJECTION_TTL_MS = 5 * 60 * 1000;
|
|
32
33
|
const replayIdentityKey = randomBytes(32);
|
|
33
34
|
const CREDENTIAL_HEADER_NAMES = new Set([
|
|
34
35
|
"authorization",
|
|
@@ -58,10 +59,17 @@ interface ServingIdentityEntry {
|
|
|
58
59
|
at: number;
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
interface OpaqueBlobRejectionEntry {
|
|
63
|
+
bytes: number;
|
|
64
|
+
at: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
const entries = new Map<string, CacheEntry>();
|
|
62
68
|
const servingIdentities = new Map<string, ServingIdentityEntry>();
|
|
69
|
+
const opaqueBlobRejections = new Map<string, OpaqueBlobRejectionEntry>();
|
|
63
70
|
let totalBytes = 0;
|
|
64
71
|
let servingIdentityTotalBytes = 0;
|
|
72
|
+
let opaqueBlobRejectionTotalBytes = 0;
|
|
65
73
|
let clockForTests: (() => number) | null = null;
|
|
66
74
|
|
|
67
75
|
const now = (): number => clockForTests?.() ?? Date.now();
|
|
@@ -142,11 +150,31 @@ function servingIdentityFor(
|
|
|
142
150
|
return { threadId, identity: JSON.stringify(identityTuple) };
|
|
143
151
|
}
|
|
144
152
|
|
|
153
|
+
function opaqueBlobRejectionKeyFor(
|
|
154
|
+
scope: OcxReasoningReplayScopeRef | undefined,
|
|
155
|
+
): string | undefined {
|
|
156
|
+
const current = servingIdentityFor(scope);
|
|
157
|
+
return current ? JSON.stringify([current.threadId, current.identity]) : undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function deleteOpaqueBlobRejection(key: string): void {
|
|
161
|
+
const entry = opaqueBlobRejections.get(key);
|
|
162
|
+
if (!entry) return;
|
|
163
|
+
opaqueBlobRejections.delete(key);
|
|
164
|
+
opaqueBlobRejectionTotalBytes -= entry.bytes;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function sweepExpiredOpaqueBlobRejections(at: number): void {
|
|
168
|
+
for (const [key, entry] of opaqueBlobRejections) {
|
|
169
|
+
if (at - entry.at >= OPAQUE_BLOB_REJECTION_TTL_MS) deleteOpaqueBlobRejection(key);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
145
173
|
/**
|
|
146
|
-
* Compare this request's route with the last successfully serving route for its
|
|
174
|
+
* Compare this request's route with the last successfully serving route for its conversation.
|
|
147
175
|
* A live mismatch means replayed opaque reasoning was minted by another backend and must not be
|
|
148
176
|
* forwarded to this one. Comparison deliberately does not refresh or replace the recorded route:
|
|
149
|
-
* a failed candidate request did not serve the
|
|
177
|
+
* a failed candidate request did not serve the conversation.
|
|
150
178
|
*
|
|
151
179
|
* Serving provenance uses restart-stable destination and credential dimensions so token
|
|
152
180
|
* generations and other volatile credential material cannot create false route changes. Missing
|
|
@@ -164,7 +192,7 @@ export function reasoningReplayServingIdentityChanged(
|
|
|
164
192
|
return previous !== undefined && previous.identity !== current.identity;
|
|
165
193
|
}
|
|
166
194
|
|
|
167
|
-
/** Record the route only after it has successfully served the
|
|
195
|
+
/** Record the route only after it has successfully served the conversation. */
|
|
168
196
|
export function commitReasoningReplayServingIdentity(
|
|
169
197
|
scope: OcxReasoningReplayScopeRef | undefined,
|
|
170
198
|
): void {
|
|
@@ -200,6 +228,54 @@ export function commitReasoningReplayServingIdentity(
|
|
|
200
228
|
}
|
|
201
229
|
}
|
|
202
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Whether this exact conversation and durable serving identity previously rejected opaque replay.
|
|
233
|
+
*
|
|
234
|
+
* The five serving dimensions deliberately match the serving record. Missing durable destination
|
|
235
|
+
* or credential identity is unknown and never falls back to process-local dimensions. The five
|
|
236
|
+
* minute TTL is shorter than the serving record's hour: a stale memo silently degrades reasoning,
|
|
237
|
+
* while expiry costs one visible recovery round trip and can safely re-establish the memo.
|
|
238
|
+
*/
|
|
239
|
+
export function reasoningReplayOpaqueBlobRejectionMemoized(
|
|
240
|
+
scope: OcxReasoningReplayScopeRef | undefined,
|
|
241
|
+
): boolean {
|
|
242
|
+
const key = opaqueBlobRejectionKeyFor(scope);
|
|
243
|
+
if (!key) return false;
|
|
244
|
+
const at = now();
|
|
245
|
+
sweepExpiredOpaqueBlobRejections(at);
|
|
246
|
+
return opaqueBlobRejections.has(key);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Record only after a blobless retry succeeded for this durable serving identity. */
|
|
250
|
+
export function rememberReasoningReplayOpaqueBlobRejection(
|
|
251
|
+
scope: OcxReasoningReplayScopeRef | undefined,
|
|
252
|
+
): void {
|
|
253
|
+
const key = opaqueBlobRejectionKeyFor(scope);
|
|
254
|
+
if (!key) return;
|
|
255
|
+
const bytes = Buffer.byteLength(key, "utf8");
|
|
256
|
+
if (bytes > MAX_TOTAL_BYTES) return;
|
|
257
|
+
const at = now();
|
|
258
|
+
sweepExpiredOpaqueBlobRejections(at);
|
|
259
|
+
if (opaqueBlobRejections.has(key)) deleteOpaqueBlobRejection(key);
|
|
260
|
+
opaqueBlobRejections.set(key, { bytes, at });
|
|
261
|
+
opaqueBlobRejectionTotalBytes += bytes;
|
|
262
|
+
while (
|
|
263
|
+
(opaqueBlobRejectionTotalBytes > MAX_TOTAL_BYTES || opaqueBlobRejections.size > MAX_ENTRIES)
|
|
264
|
+
&& opaqueBlobRejections.size > 1
|
|
265
|
+
) {
|
|
266
|
+
let oldestKey: string | undefined;
|
|
267
|
+
let oldestAt = Infinity;
|
|
268
|
+
for (const [candidateKey, entry] of opaqueBlobRejections) {
|
|
269
|
+
if (entry.at < oldestAt) {
|
|
270
|
+
oldestAt = entry.at;
|
|
271
|
+
oldestKey = candidateKey;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (oldestKey === undefined) break;
|
|
275
|
+
deleteOpaqueBlobRejection(oldestKey);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
203
279
|
function processLocalIdentity(domain: string, material: string): string {
|
|
204
280
|
return createHmac("sha256", replayIdentityKey)
|
|
205
281
|
.update(domain)
|
|
@@ -420,7 +496,9 @@ export function peekReasoningForCall(
|
|
|
420
496
|
export function clearReasoningReplayCacheForTests(clock?: (() => number) | null): void {
|
|
421
497
|
entries.clear();
|
|
422
498
|
servingIdentities.clear();
|
|
499
|
+
opaqueBlobRejections.clear();
|
|
423
500
|
totalBytes = 0;
|
|
424
501
|
servingIdentityTotalBytes = 0;
|
|
502
|
+
opaqueBlobRejectionTotalBytes = 0;
|
|
425
503
|
clockForTests = clock ?? null;
|
|
426
504
|
}
|
package/src/responses/schema.ts
CHANGED
|
@@ -134,6 +134,42 @@ export const reasoningConfigSchema = z.object({
|
|
|
134
134
|
summary: z.enum(["auto", "concise", "detailed", "none"]).optional(),
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
+
const googleSafetyCategorySchema = z.enum([
|
|
138
|
+
"HARM_CATEGORY_HATE_SPEECH",
|
|
139
|
+
"HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
|
140
|
+
"HARM_CATEGORY_DANGEROUS_CONTENT",
|
|
141
|
+
"HARM_CATEGORY_HARASSMENT",
|
|
142
|
+
"HARM_CATEGORY_CIVIC_INTEGRITY",
|
|
143
|
+
"HARM_CATEGORY_JAILBREAK",
|
|
144
|
+
]);
|
|
145
|
+
const googleSafetyThresholdSchema = z.enum([
|
|
146
|
+
"HARM_BLOCK_THRESHOLD_UNSPECIFIED",
|
|
147
|
+
"BLOCK_LOW_AND_ABOVE",
|
|
148
|
+
"BLOCK_MEDIUM_AND_ABOVE",
|
|
149
|
+
"BLOCK_ONLY_HIGH",
|
|
150
|
+
"BLOCK_NONE",
|
|
151
|
+
"OFF",
|
|
152
|
+
]);
|
|
153
|
+
const googleSafetySettingSchema = z.object({
|
|
154
|
+
category: googleSafetyCategorySchema,
|
|
155
|
+
threshold: googleSafetyThresholdSchema,
|
|
156
|
+
}).strict();
|
|
157
|
+
const googleProviderOptionsSchema = z.object({
|
|
158
|
+
thinking_budget: z.number().safe().int().gte(-1).optional(),
|
|
159
|
+
include_thoughts: z.boolean().optional(),
|
|
160
|
+
safety_settings: z.array(googleSafetySettingSchema).max(16).superRefine((settings, ctx) => {
|
|
161
|
+
const categories = new Set(settings.map(setting => setting.category));
|
|
162
|
+
if (categories.size !== settings.length) {
|
|
163
|
+
ctx.addIssue({ code: "custom", message: "safety_settings categories must be unique" });
|
|
164
|
+
}
|
|
165
|
+
}).optional(),
|
|
166
|
+
cached_content: z.string().regex(
|
|
167
|
+
/^(?:cachedContents\/[^/?#\s]+|projects\/[^/?#\s]+\/locations\/[^/?#\s]+\/cachedContents\/[^/?#\s]+)$/,
|
|
168
|
+
"cached_content must be a valid Google cached content resource name",
|
|
169
|
+
).optional(),
|
|
170
|
+
}).strict();
|
|
171
|
+
const providerOptionsSchema = z.object({ google: googleProviderOptionsSchema.optional() }).strict();
|
|
172
|
+
|
|
137
173
|
export const stopSchema = z.union([z.string(), z.array(z.string()), z.null()]);
|
|
138
174
|
|
|
139
175
|
export const responsesRequestSchema = z.object({
|
|
@@ -162,4 +198,5 @@ export const responsesRequestSchema = z.object({
|
|
|
162
198
|
prompt: z.unknown().optional(),
|
|
163
199
|
text: z.unknown().optional(),
|
|
164
200
|
truncation: z.unknown().optional(),
|
|
201
|
+
provider_options: providerOptionsSchema.optional(),
|
|
165
202
|
});
|
package/src/responses/state.ts
CHANGED
|
@@ -18,6 +18,12 @@ import {
|
|
|
18
18
|
const MAX_STORED_RESPONSES = 1_000;
|
|
19
19
|
const RESPONSE_TTL_MS = 60 * 60 * 1_000;
|
|
20
20
|
const SNAPSHOT_DEBOUNCE_MS = 2_000;
|
|
21
|
+
/** Snapshot size below which the debounce stays at its base value. */
|
|
22
|
+
const SNAPSHOT_DEBOUNCE_SCALE_FROM_BYTES = 1 * 1024 * 1024;
|
|
23
|
+
/** Ceiling for the stretched debounce. Continuation state is only read after a
|
|
24
|
+
* restart, and a graceful shutdown flushes, so the exposure a longer debounce adds
|
|
25
|
+
* is bounded by a hard kill — paid against rewriting the whole snapshot every 2 s. */
|
|
26
|
+
const SNAPSHOT_DEBOUNCE_MAX_MS = 30_000;
|
|
21
27
|
/** In-memory high-water byte cap across all entries. Forced store:false retention (kiro/cursor
|
|
22
28
|
* continuation chains) stores the full expanded input each turn — ~quadratic bytes per chain —
|
|
23
29
|
* so a count cap alone cannot bound memory. Oldest-first eviction applies past this mark. */
|
|
@@ -90,6 +96,43 @@ let oldestResidentId: string | undefined;
|
|
|
90
96
|
let oldestResidentAt: number | null = null;
|
|
91
97
|
let byteCapOverride: number | null = null;
|
|
92
98
|
let stateRevision = 0;
|
|
99
|
+
/** Byte length and digest of the last snapshot actually written, for the
|
|
100
|
+
* identical-payload skip and the size-scaled debounce. The payload itself is not
|
|
101
|
+
* retained: at the 24 MiB bound that would double the snapshot's memory cost. */
|
|
102
|
+
let lastSnapshotBytes = 0;
|
|
103
|
+
let lastSnapshotDigest: string | null = null;
|
|
104
|
+
// The resolved file the digest above describes. Keeping it means a config-dir
|
|
105
|
+
// change or a retargeted symlink is a miss rather than a false "unchanged".
|
|
106
|
+
let lastSnapshotTarget: string | null = null;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Is the snapshot on disk still byte-for-byte what we last wrote?
|
|
110
|
+
*
|
|
111
|
+
* The cached digest proves what this process wrote, not what is there now. Size is
|
|
112
|
+
* checked first so the common mismatch costs a `stat`, and the content comparison
|
|
113
|
+
* only runs when the size already agrees. Any read failure answers "no" and the
|
|
114
|
+
* caller rewrites — the safe direction.
|
|
115
|
+
*/
|
|
116
|
+
async function snapshotOnDiskMatches(path: string, payload: string, payloadBytes: number): Promise<boolean> {
|
|
117
|
+
try {
|
|
118
|
+
const file = Bun.file(path);
|
|
119
|
+
if (file.size !== payloadBytes) return false;
|
|
120
|
+
if (await file.text() !== payload) return false;
|
|
121
|
+
// Content matching is not the whole invariant. This file holds persisted request
|
|
122
|
+
// and response bodies, and `atomicWriteFileAsync` writes it owner-only; the
|
|
123
|
+
// unconditional rewrite used to restore that on every mutation. Skipping without
|
|
124
|
+
// checking would let a broadened mode persist indefinitely, so treat a widened
|
|
125
|
+
// file as "does not match" and let the caller rewrite it through the hardening
|
|
126
|
+
// path. POSIX only — Windows ACLs are re-applied by that same write path.
|
|
127
|
+
if (process.platform !== "win32") {
|
|
128
|
+
const mode = statSync(path).mode & 0o777;
|
|
129
|
+
if (mode !== 0o600) return false;
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
93
136
|
const spillCounters = { writes: 0, writeFailures: 0, readFailures: 0 };
|
|
94
137
|
/**
|
|
95
138
|
* Admission-boundary observability (test-visible). directSpills: oversized
|
|
@@ -788,9 +831,38 @@ async function writeBoundedSnapshot(path: string): Promise<SnapshotWriteOutcome>
|
|
|
788
831
|
entries.push(persistEntry);
|
|
789
832
|
}
|
|
790
833
|
entries.reverse();
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
834
|
+
const payload = JSON.stringify({ version: 2, states: entries });
|
|
835
|
+
const payloadBytes = Buffer.byteLength(payload, "utf8");
|
|
836
|
+
const payloadDigest = Bun.hash(payload).toString(36);
|
|
837
|
+
// A mutation does not always change what gets persisted: entries past the
|
|
838
|
+
// per-entry or total byte bound are dropped from the selection, and spill
|
|
839
|
+
// demotion moves bytes out of it. Re-writing a byte-identical 24 MiB file
|
|
840
|
+
// buys nothing, so compare first — but the cached digest describes what THIS
|
|
841
|
+
// process last wrote, which is not the same claim as "that is what is on disk
|
|
842
|
+
// now". A second proxy sharing the home, or anything that rewrites the file
|
|
843
|
+
// in place, leaves the digest describing bytes that are gone. Before every
|
|
844
|
+
// release-of-a-write, the previous behaviour rewrote unconditionally and so
|
|
845
|
+
// repaired that silently; skipping without checking would turn a repaired
|
|
846
|
+
// snapshot into a lost one at the next restart.
|
|
847
|
+
//
|
|
848
|
+
// Verify against the file itself, keyed to the resolved target so a retargeted
|
|
849
|
+
// symlink is also a miss. Reading back a matching-size file costs far less
|
|
850
|
+
// than the atomic replace it avoids, and only happens when the digest already
|
|
851
|
+
// matched — the amplification this fixes is the repeated WRITE, not the read.
|
|
852
|
+
const unchanged = lastSnapshotDigest !== null
|
|
853
|
+
&& payloadDigest === lastSnapshotDigest
|
|
854
|
+
&& payloadBytes === lastSnapshotBytes
|
|
855
|
+
&& lastSnapshotTarget === resolveWriteTarget(path)
|
|
856
|
+
&& existsSync(path)
|
|
857
|
+
&& await snapshotOnDiskMatches(path, payload, payloadBytes);
|
|
858
|
+
if (!unchanged) {
|
|
859
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
860
|
+
try { chmodSync(dirname(path), 0o700); } catch { /* best-effort (e.g. Windows) */ }
|
|
861
|
+
await atomicWriteFileAsync(path, payload);
|
|
862
|
+
lastSnapshotDigest = payloadDigest;
|
|
863
|
+
lastSnapshotBytes = payloadBytes;
|
|
864
|
+
lastSnapshotTarget = resolveWriteTarget(path);
|
|
865
|
+
}
|
|
794
866
|
persistAttemptHookForTests?.();
|
|
795
867
|
if (revision === stateRevision) return "stable";
|
|
796
868
|
}
|
|
@@ -809,11 +881,26 @@ function drainPendingSpillUnlinks(): void {
|
|
|
809
881
|
}
|
|
810
882
|
}
|
|
811
883
|
|
|
884
|
+
/**
|
|
885
|
+
* Debounce scaled by the size of the last snapshot written.
|
|
886
|
+
*
|
|
887
|
+
* The whole snapshot is re-serialized and atomically replaced on every flush, so at
|
|
888
|
+
* the 24 MiB bound a fixed 2 s debounce is up to ~12 MB/s of write amplification for
|
|
889
|
+
* state nothing reads until the next start (#2460). Small snapshots keep the base
|
|
890
|
+
* cadence; the stretch is linear in size and clamped, so the write rate is roughly
|
|
891
|
+
* flat instead of growing with the file.
|
|
892
|
+
*/
|
|
893
|
+
function snapshotDebounceMs(): number {
|
|
894
|
+
if (lastSnapshotBytes <= SNAPSHOT_DEBOUNCE_SCALE_FROM_BYTES) return SNAPSHOT_DEBOUNCE_MS;
|
|
895
|
+
const scaled = Math.round(SNAPSHOT_DEBOUNCE_MS * (lastSnapshotBytes / SNAPSHOT_DEBOUNCE_SCALE_FROM_BYTES));
|
|
896
|
+
return Math.min(scaled, SNAPSHOT_DEBOUNCE_MAX_MS);
|
|
897
|
+
}
|
|
898
|
+
|
|
812
899
|
function schedulePersistAt(path: string, replace = false): void {
|
|
813
900
|
if (persistTimer && !replace) return;
|
|
814
901
|
if (persistTimer) clearTimeout(persistTimer);
|
|
815
902
|
pendingPersistPath = path;
|
|
816
|
-
persistTimer = setTimeout(() => { void persistNow(path); },
|
|
903
|
+
persistTimer = setTimeout(() => { void persistNow(path); }, snapshotDebounceMs());
|
|
817
904
|
(persistTimer as { unref?: () => void }).unref?.();
|
|
818
905
|
}
|
|
819
906
|
|
|
@@ -1418,6 +1505,9 @@ export function clearResponseStateMemoryForTests(): void {
|
|
|
1418
1505
|
replayScopeMismatchDrops = 0;
|
|
1419
1506
|
replayOverlapSkips = 0;
|
|
1420
1507
|
persistAttemptHookForTests = null;
|
|
1508
|
+
lastSnapshotBytes = 0;
|
|
1509
|
+
lastSnapshotDigest = null;
|
|
1510
|
+
lastSnapshotTarget = null;
|
|
1421
1511
|
loaded = false;
|
|
1422
1512
|
}
|
|
1423
1513
|
|
package/src/router.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "./combos";
|
|
10
10
|
import type { NormalizedComboConfig } from "./combos/types";
|
|
11
11
|
import { hasOwnProvider } from "./config/provider-name";
|
|
12
|
+
import { isAzureIdentityProvider } from "./config/provider-validation";
|
|
12
13
|
import { resolveEnvValue } from "./config";
|
|
13
14
|
import { assertProviderDestinationAllowed } from "./lib/destination-policy";
|
|
14
15
|
import { redactSecretString, redactUrlForLog } from "./lib/redact";
|
|
@@ -271,10 +272,15 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
271
272
|
const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
|
|
272
273
|
if (!registryEntry || !providerMatchesRegistryTransportWithStaticGuards(providerName, provider)) {
|
|
273
274
|
assertProviderDestinationAllowed(providerName, provider);
|
|
274
|
-
return {
|
|
275
|
+
return {
|
|
276
|
+
...provider,
|
|
277
|
+
apiKey: usableResolvedApiKey(provider.apiKey),
|
|
278
|
+
...(isAzureIdentityProvider(provider) ? { liveModels: false } : {}),
|
|
279
|
+
};
|
|
275
280
|
}
|
|
276
281
|
const resolvedApiKey = usableResolvedApiKey(provider.apiKey);
|
|
277
|
-
const staticModelCatalog =
|
|
282
|
+
const staticModelCatalog = isAzureIdentityProvider(provider)
|
|
283
|
+
|| !providerSupportsLiveModelDiscovery(providerName, provider);
|
|
278
284
|
const repairLegacyMimoFreeAuth = providerName === "mimo-free"
|
|
279
285
|
&& staticModelCatalog
|
|
280
286
|
&& (provider.authMode === undefined || provider.authMode === "local");
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -1,31 +1,37 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { formatErrorResponse } from "../bridge";
|
|
3
|
+
import {
|
|
4
|
+
codexAutoStartEnabled,
|
|
5
|
+
modelPreferHostedToolsConfigError,
|
|
6
|
+
providerModelCostsConfigError,
|
|
7
|
+
requestPacingConfigError,
|
|
8
|
+
retryOn429PolicyConfigError,
|
|
9
|
+
sanitizeModelCostsForDisplay,
|
|
10
|
+
} from "../config";
|
|
3
11
|
import {
|
|
4
12
|
apiKeyTransportConfigError,
|
|
13
|
+
azureCredentialConfigError,
|
|
5
14
|
booleanRecordConfigError,
|
|
6
15
|
modelAdapterRecordConfigError,
|
|
7
|
-
modelPreferHostedToolsConfigError,
|
|
8
|
-
codexAutoStartEnabled,
|
|
9
16
|
nonBlankStringArrayConfigError,
|
|
10
17
|
positiveIntegerConfigError,
|
|
11
18
|
positiveIntegerRecordConfigError,
|
|
12
19
|
providerBaseUrlConfigError,
|
|
13
20
|
providerHeadersConfigError,
|
|
14
|
-
providerModelCostsConfigError,
|
|
15
21
|
reasoningSummaryDeliveryRecordConfigError,
|
|
16
|
-
retryOn429PolicyConfigError,
|
|
17
|
-
requestPacingConfigError,
|
|
18
|
-
sanitizeModelCostsForDisplay,
|
|
19
22
|
upstreamHttpVersionConfigError,
|
|
20
|
-
|
|
23
|
+
isAzureIdentityProvider,
|
|
24
|
+
} from "../config/provider-validation";
|
|
21
25
|
import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
22
26
|
import { redactSecretString } from "../lib/redact";
|
|
23
27
|
import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
|
|
24
28
|
import { providerConfigSeed } from "../providers/derive";
|
|
25
29
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
26
30
|
import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
|
|
31
|
+
import { modelAutoCompactTokenLimitsConfigError } from "../providers/auto-compact-budget";
|
|
27
32
|
import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
|
|
28
33
|
import { xaiResponsesOptInState } from "../providers/xai-responses-opt-in";
|
|
34
|
+
import { antigravityOAuthDestinationConfigError, getProviderTlsProfileStatus, providerTlsProfileConfigError } from "../lib/provider-tls-profile";
|
|
29
35
|
|
|
30
36
|
let _corsOrigin = "http://localhost:10100";
|
|
31
37
|
export function setCorsOrigin(port: number): void { _corsOrigin = `http://localhost:${port}`; }
|
|
@@ -563,6 +569,8 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
563
569
|
if (contextOverlayError) return contextOverlayError;
|
|
564
570
|
delete canonicalCandidate.contextWindow;
|
|
565
571
|
delete canonicalCandidate.modelContextWindows;
|
|
572
|
+
// User-owned soft compaction policy; it does not alter the canonical transport seed.
|
|
573
|
+
delete canonicalCandidate.modelAutoCompactTokenLimits;
|
|
566
574
|
const canonical = seed && sameCanonicalProviderSeed(canonicalCandidate, seed);
|
|
567
575
|
if (!canonical) {
|
|
568
576
|
return `provider ${name} must equal the canonical built-in provider seed`;
|
|
@@ -571,6 +579,12 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
571
579
|
return `provider ${name} must not include codexAccountMode`;
|
|
572
580
|
}
|
|
573
581
|
const typed = provider as unknown as OcxProviderConfig;
|
|
582
|
+
const tlsProfileError = providerTlsProfileConfigError(name, typed);
|
|
583
|
+
if (tlsProfileError) {
|
|
584
|
+
return `provider ${JSON.stringify(redactSecretString(name))} ${tlsProfileError}`;
|
|
585
|
+
}
|
|
586
|
+
const antigravityError = antigravityOAuthDestinationConfigError(name, typed);
|
|
587
|
+
if (antigravityError) return `provider ${name} ${antigravityError}`;
|
|
574
588
|
const baseUrlError = providerBaseUrlConfigError(typed.baseUrl);
|
|
575
589
|
if (baseUrlError) return `provider ${name} ${baseUrlError}`;
|
|
576
590
|
if (effectiveGoogleMode(name, typed) === "vertex" && typed.location !== undefined) {
|
|
@@ -603,8 +617,17 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
603
617
|
}
|
|
604
618
|
const apiKeyTransportError = apiKeyTransportConfigError(typed);
|
|
605
619
|
if (apiKeyTransportError) return `provider ${name} ${apiKeyTransportError}`;
|
|
620
|
+
const azureCredentialError = azureCredentialConfigError(raw);
|
|
621
|
+
if (azureCredentialError) return `provider ${JSON.stringify(redactSecretString(name))} ${azureCredentialError}`;
|
|
606
622
|
const maxInputError = positiveIntegerRecordConfigError(raw.modelMaxInputTokens, "modelMaxInputTokens");
|
|
607
623
|
if (maxInputError) return `provider ${name} ${maxInputError}`;
|
|
624
|
+
const autoCompactError = modelAutoCompactTokenLimitsConfigError(
|
|
625
|
+
raw.modelAutoCompactTokenLimits,
|
|
626
|
+
{ requireNativeIds: name === "openai" },
|
|
627
|
+
);
|
|
628
|
+
if (autoCompactError) {
|
|
629
|
+
return `provider ${JSON.stringify(redactSecretString(name))} ${autoCompactError}`;
|
|
630
|
+
}
|
|
608
631
|
const reasoningSummariesError = booleanRecordConfigError(raw.modelSupportsReasoningSummaries, "modelSupportsReasoningSummaries");
|
|
609
632
|
if (reasoningSummariesError) return `provider ${name} ${reasoningSummariesError}`;
|
|
610
633
|
const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError(
|
|
@@ -687,6 +710,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
687
710
|
adapter: provider.adapter,
|
|
688
711
|
baseUrl: publicProviderBaseUrl(provider.baseUrl),
|
|
689
712
|
hasApiKey: !!provider.apiKey,
|
|
713
|
+
hasAzureCredential: isAzureIdentityProvider(provider),
|
|
690
714
|
hasHeaders: !!provider.headers && Object.keys(provider.headers).length > 0,
|
|
691
715
|
};
|
|
692
716
|
if (name === "xai") {
|
|
@@ -702,9 +726,11 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
702
726
|
"freeTier",
|
|
703
727
|
"liveModels",
|
|
704
728
|
"requestPacing",
|
|
729
|
+
"tlsProfile",
|
|
705
730
|
"models",
|
|
706
731
|
"contextWindow",
|
|
707
732
|
"modelContextWindows",
|
|
733
|
+
"modelAutoCompactTokenLimits",
|
|
708
734
|
"defaultMaxOutputTokens",
|
|
709
735
|
"modelMaxOutputTokens",
|
|
710
736
|
"openRouterRouting",
|
|
@@ -726,6 +752,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
726
752
|
] as const) {
|
|
727
753
|
copyIfDefined(dto, provider, key);
|
|
728
754
|
}
|
|
755
|
+
dto.tlsProfileStatus = provider.tlsProfile === undefined ? "disabled" : getProviderTlsProfileStatus(name, provider);
|
|
729
756
|
const modelCosts = sanitizeModelCostsForDisplay(provider.modelCosts);
|
|
730
757
|
if (modelCosts) dto.modelCosts = modelCosts;
|
|
731
758
|
// Resolve the note by DESTINATION, not by name. A preset saved under a custom name is
|
|
@@ -746,6 +773,9 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
746
773
|
defaultProvider: config.defaultProvider,
|
|
747
774
|
codexAutoStart: codexAutoStartEnabled(config),
|
|
748
775
|
websockets: config.websockets,
|
|
776
|
+
// The GUI's browser-open toggle reads and writes this; absent means the
|
|
777
|
+
// historical auto-open behavior.
|
|
778
|
+
oauthOpenBrowser: config.oauthOpenBrowser !== false,
|
|
749
779
|
providers,
|
|
750
780
|
};
|
|
751
781
|
}
|
package/src/server/images.ts
CHANGED
|
@@ -33,7 +33,9 @@ import { readJsonRequestBody } from "./request-decompress";
|
|
|
33
33
|
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
|
|
34
34
|
import type { RequestLogContext } from "./request-log";
|
|
35
35
|
import { codexLogAccountId, decodeRequestErrorResponse } from "./responses";
|
|
36
|
-
import { getValidAccessToken, getOAuthCredentialProjectId } from "../oauth/index";
|
|
36
|
+
import { getValidAccessToken, getOAuthCredentialProjectId, getValidAccessTokenSnapshot, type OAuthAccessSnapshot } from "../oauth/index";
|
|
37
|
+
import { isCanonicalAntigravityUrl, providerTlsFetch } from "../lib/provider-tls-profile";
|
|
38
|
+
import { redactErrorMessage } from "../lib/redact";
|
|
37
39
|
import { safeAntigravityHttpErrorMessage } from "../adapters/google-errors";
|
|
38
40
|
import { sanitizeUpstreamErrorText } from "../adapters/upstream-http-error";
|
|
39
41
|
import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire";
|
|
@@ -156,6 +158,9 @@ async function tryCcaImageGeneration(
|
|
|
156
158
|
if (endpoint !== "generations") return undefined;
|
|
157
159
|
const provider = config.providers?.["google-antigravity"];
|
|
158
160
|
if (!provider || provider.disabled) return undefined;
|
|
161
|
+
if (provider.baseUrl && !isCanonicalAntigravityUrl(provider.baseUrl)) {
|
|
162
|
+
return formatErrorResponse(400, "invalid_request_error", "google-antigravity requires a canonical destination");
|
|
163
|
+
}
|
|
159
164
|
|
|
160
165
|
const prompt = (body as { prompt?: unknown })?.prompt;
|
|
161
166
|
if (typeof prompt !== "string" || !prompt.trim()) {
|
|
@@ -174,36 +179,25 @@ async function tryCcaImageGeneration(
|
|
|
174
179
|
// OAuth token refresh and project discovery, not just the upstream fetch.
|
|
175
180
|
const timeoutMs = config.images?.timeoutMs ?? IMAGES_UPSTREAM_TIMEOUT_MS;
|
|
176
181
|
const linkedSignal = signalWithTimeout(timeoutMs, signal);
|
|
177
|
-
let
|
|
182
|
+
let snapshot: OAuthAccessSnapshot;
|
|
178
183
|
try {
|
|
179
|
-
|
|
180
|
-
// chains through 4 layers (resolveAccessSnapshotForAccount →
|
|
181
|
-
// refreshAndPersistAccessToken → refreshGenericAccountWithLock →
|
|
182
|
-
// def.refresh()) that do HTTP calls without accepting a signal. Rather than
|
|
183
|
-
// threading signal through the entire chain, race the whole call against
|
|
184
|
-
// linkedSignal: when the signal aborts we stop awaiting and surface the
|
|
185
|
-
// cancellation immediately instead of hanging on the refresh HTTP call.
|
|
186
|
-
token = await abortableRace(getValidAccessToken("google-antigravity"), linkedSignal.signal);
|
|
184
|
+
snapshot = await abortableRace(getValidAccessTokenSnapshot("google-antigravity"), linkedSignal.signal);
|
|
187
185
|
} catch (err) {
|
|
188
186
|
linkedSignal.cleanup();
|
|
189
|
-
// abortableRace rejects immediately when the signal fires, so client
|
|
190
|
-
// cancellation and deadline expiry surface here. Parent abort propagates
|
|
191
|
-
// into the linked signal, so check parent first (499) before the linked
|
|
192
|
-
// signal (504).
|
|
193
187
|
if (signal.aborted) {
|
|
194
188
|
return formatErrorResponse(499, "client_closed_request", "CCA image request canceled by client");
|
|
195
189
|
}
|
|
196
190
|
if (linkedSignal.signal.aborted) {
|
|
197
191
|
return formatErrorResponse(504, "upstream_error", "CCA image generation timed out during authentication");
|
|
198
192
|
}
|
|
199
|
-
// Missing/revoked credential → 401 (re-login required); transient refresh/network → 502.
|
|
200
193
|
const errName = err instanceof Error ? err.name : "";
|
|
201
194
|
if (errName === "OAuthLoginRequiredError") {
|
|
202
195
|
return formatErrorResponse(401, "invalid_request_error", "Google Antigravity login required: run 'ocx login google-antigravity'");
|
|
203
196
|
}
|
|
204
197
|
return formatErrorResponse(502, "upstream_error", "CCA image generation failed: OAuth token refresh failed");
|
|
205
198
|
}
|
|
206
|
-
const
|
|
199
|
+
const token = snapshot.accessToken;
|
|
200
|
+
const project = snapshot.projectId;
|
|
207
201
|
if (!project) {
|
|
208
202
|
linkedSignal.cleanup();
|
|
209
203
|
return formatErrorResponse(
|
|
@@ -234,10 +228,8 @@ async function tryCcaImageGeneration(
|
|
|
234
228
|
let upstream: Response | undefined;
|
|
235
229
|
try {
|
|
236
230
|
try {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// connection failed, so never replay it on a peer host.
|
|
240
|
-
upstream = await fetch(`${baseUrl}/v1internal:generateContent`, {
|
|
231
|
+
const executor = providerTlsFetch("google-antigravity", provider, globalThis.fetch);
|
|
232
|
+
upstream = await executor(`${baseUrl}/v1internal:generateContent`, {
|
|
241
233
|
method: "POST",
|
|
242
234
|
headers: {
|
|
243
235
|
"Content-Type": "application/json",
|
|
@@ -249,23 +241,15 @@ async function tryCcaImageGeneration(
|
|
|
249
241
|
});
|
|
250
242
|
} catch (err) {
|
|
251
243
|
if (signal.aborted) return formatErrorResponse(499, "client_closed_request", "CCA image request canceled by client");
|
|
244
|
+
const rawMsg = err instanceof Error ? err.message : String(err);
|
|
245
|
+
const safeMsg = redactErrorMessage(sanitizeUpstreamErrorText(rawMsg));
|
|
252
246
|
if (err instanceof Error && err.name === "TimeoutError") {
|
|
253
|
-
return
|
|
247
|
+
return formatErrorResponse(504, "upstream_error", `CCA image generation timed out: ${safeMsg}`);
|
|
254
248
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
// and strip the internal base URL host from the surfaced message.
|
|
260
|
-
// 400, not 5xx: the POST is paid and non-idempotent. Codex retries every
|
|
261
|
-
// 5xx up to 5 attempts, which would duplicate generation after an ambiguous
|
|
262
|
-
// transport failure (the upstream may already have accepted the request).
|
|
263
|
-
const rawMsg = err instanceof Error ? err.message : String(err);
|
|
264
|
-
const safeMsg = sanitizeUpstreamErrorText(rawMsg).replace(
|
|
265
|
-
/https?:\/\/[^\s"'<>]+/gi,
|
|
266
|
-
"[upstream-url]",
|
|
267
|
-
);
|
|
268
|
-
return ccaAmbiguousImageTransportError(safeMsg);
|
|
249
|
+
if (provider.tlsProfile === "antigravity-browser") {
|
|
250
|
+
return formatErrorResponse(502, "upstream_error", `CCA image generation failed: ${safeMsg}`);
|
|
251
|
+
}
|
|
252
|
+
return ccaAmbiguousImageTransportError(safeMsg.replace(/https?:\/\/[^\s"'<>]+/gi, "[upstream-url]"));
|
|
269
253
|
}
|
|
270
254
|
|
|
271
255
|
// Stream the upstream body with a bounded reader so an oversized or malicious
|