@yansigit/opencodex 2.32.0 → 2.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +2 -2
  2. package/gui/dist/assets/index-CIDo4y4k.js +102 -0
  3. package/gui/dist/assets/index-DrSQdTRd.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +8 -5
  6. package/src/adapters/anthropic.ts +20 -6
  7. package/src/adapters/azure.ts +20 -4
  8. package/src/adapters/base.ts +3 -1
  9. package/src/adapters/command-code.ts +131 -17
  10. package/src/adapters/cursor/envelope-echo.ts +162 -0
  11. package/src/adapters/cursor/live-transport.ts +4 -2
  12. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  13. package/src/adapters/cursor/native-exec-network.ts +3 -5
  14. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  15. package/src/adapters/cursor/native-exec-shell.ts +13 -25
  16. package/src/adapters/cursor/native-exec.ts +18 -10
  17. package/src/adapters/cursor/protobuf-events.ts +186 -9
  18. package/src/adapters/cursor/protobuf-request.ts +53 -18
  19. package/src/adapters/cursor/request-builder.ts +11 -3
  20. package/src/adapters/cursor/tool-definitions.ts +49 -2
  21. package/src/adapters/cursor/tool-result-normalize.ts +21 -8
  22. package/src/adapters/cursor/types.ts +11 -3
  23. package/src/adapters/cursor.ts +123 -0
  24. package/src/adapters/google-aistudio-parser.ts +49 -0
  25. package/src/adapters/google-antigravity-replay.ts +2 -2
  26. package/src/adapters/google-antigravity-wire.ts +7 -0
  27. package/src/adapters/google-errors.ts +6 -2
  28. package/src/adapters/google-http.ts +30 -7
  29. package/src/adapters/google-truncation.ts +5 -0
  30. package/src/adapters/google-wire-compiler.ts +38 -6
  31. package/src/adapters/google.ts +242 -28
  32. package/src/adapters/kiro-tools.ts +20 -9
  33. package/src/adapters/openai-chat.ts +9 -0
  34. package/src/adapters/openai-responses.ts +2 -1
  35. package/src/bridge.ts +112 -9
  36. package/src/chat/inbound.ts +15 -0
  37. package/src/claude/context-windows.ts +16 -9
  38. package/src/cli/doctor.ts +2 -2
  39. package/src/cli/index.ts +10 -3
  40. package/src/cli/provider.ts +6 -0
  41. package/src/cli/status.ts +23 -0
  42. package/src/codex/auth-api.ts +4 -2
  43. package/src/codex/autostart-health.ts +16 -0
  44. package/src/codex/catalog/aggregation.ts +12 -12
  45. package/src/codex/catalog/effort.ts +18 -3
  46. package/src/codex/catalog/metadata.ts +27 -1
  47. package/src/codex/catalog/model-metadata.ts +39 -12
  48. package/src/codex/catalog/parsing.ts +38 -27
  49. package/src/codex/catalog/provider-fetch.ts +232 -133
  50. package/src/codex/catalog/sync.ts +1 -1
  51. package/src/codex/convergence.ts +5 -0
  52. package/src/codex/shim.ts +56 -3
  53. package/src/config/provider-validation.ts +37 -0
  54. package/src/config.ts +78 -2
  55. package/src/generated/compatibility-version.json +187 -119
  56. package/src/generated/model-metadata.ts +3 -0
  57. package/src/images/loop.ts +37 -6
  58. package/src/lib/azure-identity.ts +154 -0
  59. package/src/lib/debug.ts +42 -0
  60. package/src/lib/errors.ts +14 -0
  61. package/src/lib/provider-outbound.ts +45 -33
  62. package/src/lib/provider-tls-profile.ts +309 -0
  63. package/src/lib/proxy-env.ts +49 -0
  64. package/src/lib/redact.ts +10 -1
  65. package/src/oauth/aistudio-native-daemon.ts +62 -0
  66. package/src/oauth/aistudio-session-sync.ts +95 -0
  67. package/src/oauth/antigravity-routing.ts +282 -236
  68. package/src/oauth/callback-server.ts +22 -2
  69. package/src/oauth/command-code.ts +5 -16
  70. package/src/oauth/google-aistudio-auth.ts +98 -0
  71. package/src/oauth/google-antigravity.ts +42 -5
  72. package/src/oauth/index.ts +15 -3
  73. package/src/oauth/key-providers.ts +8 -0
  74. package/src/oauth/kimi.ts +9 -1
  75. package/src/oauth/login-cli.ts +66 -1
  76. package/src/oauth/open-browser-choice.ts +26 -0
  77. package/src/oauth/store.ts +6 -0
  78. package/src/providers/antigravity-quota.ts +3 -1
  79. package/src/providers/api-keys.ts +2 -1
  80. package/src/providers/auto-compact-budget.ts +65 -0
  81. package/src/providers/derive.ts +5 -1
  82. package/src/providers/key-failover.ts +5 -1
  83. package/src/providers/openai-tiers.ts +5 -0
  84. package/src/providers/provider-id-rewrite.ts +1 -0
  85. package/src/providers/quota.ts +148 -50
  86. package/src/providers/registry.ts +27 -4
  87. package/src/providers/request-pacing.ts +33 -6
  88. package/src/providers/xai-transport.ts +21 -0
  89. package/src/responses/google-provider-options.ts +36 -0
  90. package/src/responses/namespace-tool-compat.ts +84 -4
  91. package/src/responses/parser.ts +11 -0
  92. package/src/responses/provider-opaque-metadata.ts +3 -3
  93. package/src/responses/schema.ts +37 -0
  94. package/src/responses/state.ts +94 -4
  95. package/src/router.ts +11 -2
  96. package/src/routing/account-pool/cooldown.ts +8 -0
  97. package/src/routing/account-pool/index.ts +1 -0
  98. package/src/server/aistudio-ws-hub.ts +295 -0
  99. package/src/server/auth-cors.ts +29 -0
  100. package/src/server/chat-completions.ts +2 -0
  101. package/src/server/images.ts +19 -35
  102. package/src/server/index.ts +94 -0
  103. package/src/server/management/agent-settings-routes.ts +205 -15
  104. package/src/server/management/combo-routes.ts +6 -0
  105. package/src/server/management/config-routes.ts +31 -5
  106. package/src/server/management/logs-usage-routes.ts +11 -5
  107. package/src/server/management/model-rows.ts +4 -0
  108. package/src/server/management/oauth-account-routes.ts +38 -7
  109. package/src/server/management/provider-routes.ts +113 -15
  110. package/src/server/management/routing-profile-routes.ts +3 -0
  111. package/src/server/port-reclaim.ts +19 -1
  112. package/src/server/request-log-conversation.ts +12 -0
  113. package/src/server/request-log.ts +23 -1
  114. package/src/server/responses/agent-task-recovery.ts +1 -1
  115. package/src/server/responses/compact.ts +30 -1
  116. package/src/server/responses/core.ts +363 -156
  117. package/src/server/responses/empty-completion-guard.ts +35 -6
  118. package/src/server/responses/fetch-helpers.ts +18 -5
  119. package/src/server/responses/policy-fallback.ts +1 -1
  120. package/src/server/responses/v2-native-parent-override.ts +59 -0
  121. package/src/server/responses/ws-upstream.ts +75 -2
  122. package/src/server/responses-undeclared-tool-guard.ts +90 -8
  123. package/src/server/ws-bridge.ts +2 -1
  124. package/src/service.ts +1 -1
  125. package/src/smoke/fingerprint-cache.ts +133 -0
  126. package/src/smoke/live-scenarios.ts +33 -0
  127. package/src/smoke/runner.ts +119 -0
  128. package/src/types/config.ts +16 -1
  129. package/src/types/provider.ts +18 -1
  130. package/src/types/request.ts +30 -0
  131. package/src/types/tools.ts +51 -0
  132. package/src/types.ts +6 -0
  133. package/src/usage/command-code-manifest.ts +116 -0
  134. package/src/usage/cost.ts +2 -2
  135. package/src/usage/expected-prices.ts +83 -0
  136. package/src/usage/log.ts +2 -2
  137. package/src/usage/summary.ts +34 -12
  138. package/src/web-search/gemini-executor.ts +6 -4
  139. package/src/web-search/index.ts +16 -8
  140. package/src/web-search/loop.ts +42 -6
  141. package/gui/dist/assets/index-BG43zwVe.js +0 -102
  142. package/gui/dist/assets/index-CiSI-jrP.css +0 -1
@@ -27,6 +27,9 @@ const PROVIDER_ALIASES: Record<string, string> = {
27
27
  "google-antigravity": "google",
28
28
  "antigravity": "google",
29
29
  "gemini-antigravity": "google",
30
+ "google-aistudio": "google",
31
+ "aistudio": "google",
32
+ "gemini-aistudio": "google",
30
33
  "deepseek": "deepseek",
31
34
  "moonshot": "moonshot",
32
35
  "zhipu-bigmodel": "zai",
@@ -18,7 +18,7 @@ import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderContinuatio
18
18
  import { namespacedToolName, toolChoiceToolPredicate } from "../types";
19
19
  import { cloneProviderOpaqueToolCallMetadata } from "../responses/provider-opaque-metadata";
20
20
  import type { AttemptRecoveryKind } from "../usage/log";
21
- import { bridgeToResponsesSSE } from "../bridge";
21
+ import { bridgeToResponsesSSE, diagnoseAdapterEvent, type BridgeDiagnosticContext } from "../bridge";
22
22
  import { clearableDeadline, idleDeadline } from "../lib/abort";
23
23
  import { readBoundedResponseBody } from "../lib/bounded-body";
24
24
  import { fetchWithResetRetry, prepareSameTarget429Wait } from "../lib/upstream-retry";
@@ -35,6 +35,7 @@ import { submitVideoJob } from "./xai-video-client";
35
35
  import { downloadVideoToArtifact, createImageBudget, pruneArtifacts } from "./artifacts";
36
36
  import { IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "./synthetic-tool";
37
37
  import type { ImageBridgePlan, VideoBridgePlan } from "./types";
38
+ import { OcxRequestValidationError } from "../lib/errors";
38
39
 
39
40
  const SSE_HEADERS = {
40
41
  "Content-Type": "text/event-stream",
@@ -209,8 +210,14 @@ function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent[]
209
210
  return parts;
210
211
  }
211
212
 
212
- function jsonError(status: number, message: string): Response {
213
- return new Response(JSON.stringify({ error: { message, type: "upstream_error", code: null } }), {
213
+ function jsonError(status: number, message: string, errorType = "upstream_error", code: string | null = null): Response {
214
+ return new Response(JSON.stringify({
215
+ error: {
216
+ message,
217
+ type: errorType,
218
+ code,
219
+ },
220
+ }), {
214
221
  status,
215
222
  headers: { "Content-Type": "application/json" },
216
223
  });
@@ -219,7 +226,12 @@ function jsonError(status: number, message: string): Response {
219
226
  /** Hard provider/parse failure inside an iteration. The eager first iteration converts it to a
220
227
  * non-2xx jsonError; later (already-streaming) iterations surface it as an in-stream error event. */
221
228
  class LoopError extends Error {
222
- constructor(readonly status: number, message: string) {
229
+ constructor(
230
+ readonly status: number,
231
+ message: string,
232
+ readonly errorType?: string,
233
+ readonly code?: string,
234
+ ) {
223
235
  super(message);
224
236
  this.name = "LoopError";
225
237
  }
@@ -245,6 +257,8 @@ export interface ImageBridgeDeps {
245
257
  onAttemptSend?: (recovery?: AttemptRecoveryKind) => void;
246
258
  /** Called after each upstream request is built (parity with web-search / normal path). */
247
259
  onRequestBuilt?: (request: AdapterRequest) => void;
260
+ /** Validate the final adapter before every cached replay or request build. */
261
+ validateAdapter?: (parsed: OcxParsedRequest, adapter: ProviderAdapter) => void;
248
262
  abortSignal?: AbortSignal;
249
263
  onFirstOutput?: () => void;
250
264
  /** Max image-generation rounds before forcing a final answer. Defaults to 3; clamped to [0, 10]. */
@@ -270,6 +284,8 @@ export interface ImageBridgeDeps {
270
284
  onCompletedResponse?: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => void;
271
285
  /** WebSocket Responses path only — leave response id empty for protocol compatibility. */
272
286
  forceEmptyResponseId?: boolean;
287
+ /** Internal, opt-in structural stream diagnostics shared with the final bridge. */
288
+ diagnostic?: BridgeDiagnosticContext;
273
289
  }
274
290
 
275
291
  /**
@@ -487,6 +503,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
487
503
  * header deadline. The caller owns same-target 429 replays and key rotation around it.
488
504
  */
489
505
  const fetchOnce = async (requestAdapter: ProviderAdapter, recovery?: AttemptRecoveryKind): Promise<IterationResponse> => {
506
+ deps.validateAdapter?.(iterParsed, requestAdapter);
490
507
  let request: AdapterRequest;
491
508
  if (cachedRequest !== undefined && cachedAdapter === requestAdapter) {
492
509
  request = cachedRequest;
@@ -609,6 +626,9 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
609
626
  return prepared;
610
627
  } catch (error) {
611
628
  if (isTranslatorBudgetExceededError(error)) throw error;
629
+ if (error instanceof OcxRequestValidationError) {
630
+ throw new LoopError(error.status, error.message, "invalid_request_error", "invalid_request_error");
631
+ }
612
632
  if (headerDeadline.didExpire()) {
613
633
  throw new LoopError(504, `Provider response-header timeout after ${connectTimeoutMs}ms during image-bridge`);
614
634
  }
@@ -638,6 +658,10 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
638
658
  inactivityTimeoutMs: stallTimeoutMs,
639
659
  translatorBudget,
640
660
  })) {
661
+ if (deps.diagnostic) {
662
+ deps.diagnostic.adapterName = prepared.responseAdapter.name;
663
+ diagnoseAdapterEvent(deps.diagnostic, event);
664
+ }
641
665
  if (event.type === "heartbeat") yield event;
642
666
  else events.push(event);
643
667
  }
@@ -674,7 +698,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
674
698
  firstPrepared = await prepareIterationDrained(maxRounds <= 0);
675
699
  } catch (e) {
676
700
  if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
677
- if (e instanceof LoopError) return jsonError(e.status, e.message);
701
+ if (e instanceof LoopError) return jsonError(e.status, e.message, e.errorType, e.code ?? null);
678
702
  throw e;
679
703
  }
680
704
  }
@@ -920,7 +944,13 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
920
944
  yield {
921
945
  type: "error",
922
946
  message: e instanceof LoopError ? e.message : (e instanceof Error ? e.message : String(e)),
923
- ...(e instanceof LoopError ? { status: e.status } : {}),
947
+ ...(e instanceof LoopError ? {
948
+ status: e.status,
949
+ ...(e.errorType !== undefined || e.status === 400
950
+ ? { errorType: e.errorType ?? "upstream_error" }
951
+ : {}),
952
+ ...(e.code !== undefined ? { code: e.code } : {}),
953
+ } : {}),
924
954
  ...(hiddenUsage ? { usage: hiddenUsage } : {}),
925
955
  };
926
956
  }
@@ -949,6 +979,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
949
979
  onUsage: (usage: OcxUsage | undefined) => deps.onUsage?.(usage),
950
980
  } : {}),
951
981
  ...(deps.onCompletedResponse ? { onCompletedResponse: deps.onCompletedResponse } : {}),
982
+ ...(deps.diagnostic ? { diagnostic: deps.diagnostic } : {}),
952
983
  },
953
984
  );
954
985
  return new Response(sse, { headers: SSE_HEADERS });
@@ -0,0 +1,154 @@
1
+ import type { OcxProviderConfig } from "../types";
2
+
3
+ export const AZURE_IDENTITY_SCOPE = "https://cognitiveservices.azure.com/.default";
4
+ export const AZURE_IDENTITY_UNAVAILABLE_ERROR = "Azure identity credential unavailable";
5
+
6
+ export interface AzureTokenCredential {
7
+ getToken(scope: string): Promise<{ token?: string } | null>;
8
+ }
9
+
10
+ export type AzureCredentialFactory = (options?: { managedIdentityClientId?: string }) =>
11
+ AzureTokenCredential | Promise<AzureTokenCredential>;
12
+ export type AzureIdentityModuleLoader = () => Promise<{
13
+ DefaultAzureCredential: new (options?: { managedIdentityClientId?: string }) => AzureTokenCredential;
14
+ }>;
15
+ export type AzureLoggerModuleLoader = () => Promise<{
16
+ AzureLogger: { log: (...args: unknown[]) => void };
17
+ }>;
18
+
19
+ const credentials = new Map<string, AzureTokenCredential>();
20
+ const inflight = new Map<string, Promise<AzureTokenCredential>>();
21
+ const invalidatedInflight = new Set<Promise<AzureTokenCredential>>();
22
+ let testFactory: AzureCredentialFactory | undefined;
23
+ let testModuleLoader: AzureIdentityModuleLoader | undefined;
24
+ let testLoggerModuleLoader: AzureLoggerModuleLoader | undefined;
25
+
26
+ async function loadAzureLoggerModule(): Promise<{
27
+ AzureLogger: { log: (...args: unknown[]) => void };
28
+ }> {
29
+ const packageName: string = "@azure/logger";
30
+ return await import(packageName) as unknown as {
31
+ AzureLogger: { log: (...args: unknown[]) => void };
32
+ };
33
+ }
34
+
35
+ async function suppressAzureSdkLogging(): Promise<void> {
36
+ const module = await (testLoggerModuleLoader ?? loadAzureLoggerModule)();
37
+ module.AzureLogger.log = () => {};
38
+ }
39
+
40
+ async function loadAzureIdentityModule(): Promise<{
41
+ DefaultAzureCredential: new (options?: { managedIdentityClientId?: string }) => AzureTokenCredential;
42
+ }> {
43
+ // Keep the package out of the static module graph until Task 3 installs it.
44
+ const packageName: string = "@azure/identity";
45
+ return await import(packageName) as unknown as {
46
+ DefaultAzureCredential: new (options?: { managedIdentityClientId?: string }) => AzureTokenCredential;
47
+ };
48
+ }
49
+
50
+ async function loadDefaultAzureCredential(options?: { managedIdentityClientId?: string }): Promise<AzureTokenCredential> {
51
+ await suppressAzureSdkLogging();
52
+ const module = await (testModuleLoader ?? loadAzureIdentityModule)();
53
+ return options === undefined
54
+ ? new module.DefaultAzureCredential()
55
+ : new module.DefaultAzureCredential(options);
56
+ }
57
+
58
+ function clientIdFor(provider: OcxProviderConfig): string {
59
+ return provider.azureCredential?.managedIdentityClientId?.trim() ?? "";
60
+ }
61
+
62
+ async function credentialFor(clientId: string): Promise<AzureTokenCredential> {
63
+ const cached = credentials.get(clientId);
64
+ if (cached) return cached;
65
+ const running = inflight.get(clientId);
66
+ if (running) return running;
67
+ const generation = cacheGeneration;
68
+ let promise!: Promise<AzureTokenCredential>;
69
+ promise = Promise.resolve((testFactory ?? loadDefaultAzureCredential)(clientId ? { managedIdentityClientId: clientId } : undefined))
70
+ .then(credential => {
71
+ if (cacheGeneration === generation && !invalidatedInflight.has(promise)) {
72
+ credentials.set(clientId, credential);
73
+ }
74
+ return credential;
75
+ })
76
+ .finally(() => {
77
+ invalidatedInflight.delete(promise);
78
+ if (inflight.get(clientId) === promise) inflight.delete(clientId);
79
+ });
80
+ inflight.set(clientId, promise);
81
+ return promise;
82
+ }
83
+
84
+ export function azureIdentityClientId(provider: Pick<OcxProviderConfig, "azureCredential">): string | undefined {
85
+ if (provider.azureCredential?.type !== "default-azure-credential") return undefined;
86
+ return provider.azureCredential.managedIdentityClientId?.trim() ?? "";
87
+ }
88
+
89
+ export function invalidateAzureCredentialCache(clientId: string): void {
90
+ const key = clientId.trim();
91
+ const running = inflight.get(key);
92
+ if (running) invalidatedInflight.add(running);
93
+ credentials.delete(key);
94
+ inflight.delete(key);
95
+ }
96
+
97
+ export function __azureCredentialCacheStateForTests(): {
98
+ credentials: number;
99
+ inflight: number;
100
+ invalidatedInflight: number;
101
+ } {
102
+ return {
103
+ credentials: credentials.size,
104
+ inflight: inflight.size,
105
+ invalidatedInflight: invalidatedInflight.size,
106
+ };
107
+ }
108
+
109
+ export async function getAzureAccessToken(provider: OcxProviderConfig): Promise<string> {
110
+ try {
111
+ const credential = await credentialFor(clientIdFor(provider));
112
+ const result = await credential.getToken(AZURE_IDENTITY_SCOPE);
113
+ const token = result?.token?.trim();
114
+ if (!token) throw new Error(AZURE_IDENTITY_UNAVAILABLE_ERROR);
115
+ return token;
116
+ } catch {
117
+ throw new Error(AZURE_IDENTITY_UNAVAILABLE_ERROR);
118
+ }
119
+ }
120
+
121
+ export function setAzureCredentialFactoryForTests(factory: AzureCredentialFactory): void {
122
+ cacheGeneration++;
123
+ testFactory = factory;
124
+ testModuleLoader = undefined;
125
+ testLoggerModuleLoader = undefined;
126
+ credentials.clear();
127
+ inflight.clear();
128
+ invalidatedInflight.clear();
129
+ }
130
+
131
+ export function __resetAzureCredentialCache(): void {
132
+ cacheGeneration++;
133
+ testFactory = undefined;
134
+ testModuleLoader = undefined;
135
+ testLoggerModuleLoader = undefined;
136
+ credentials.clear();
137
+ inflight.clear();
138
+ invalidatedInflight.clear();
139
+ }
140
+
141
+ let cacheGeneration = 0;
142
+
143
+ export function setAzureIdentityModuleLoaderForTests(loader: AzureIdentityModuleLoader): void {
144
+ cacheGeneration++;
145
+ testFactory = undefined;
146
+ testModuleLoader = loader;
147
+ credentials.clear();
148
+ inflight.clear();
149
+ invalidatedInflight.clear();
150
+ }
151
+
152
+ export function setAzureLoggerModuleLoaderForTests(loader: AzureLoggerModuleLoader): void {
153
+ testLoggerModuleLoader = loader;
154
+ }
package/src/lib/debug.ts CHANGED
@@ -1,7 +1,10 @@
1
+ import { createHmac, randomBytes } from "node:crypto";
1
2
  import { appendDebugLogLine } from "./debug-log-buffer";
2
3
  import { isDebugEnabled } from "./debug-settings";
3
4
  import { redactSecrets } from "./redact";
4
5
 
6
+ let debugFingerprintKey: Uint8Array | undefined;
7
+
5
8
  function emitDebugLine(line: string): void {
6
9
  if (!isDebugEnabled()) return;
7
10
  try {
@@ -29,3 +32,42 @@ export function debugProviderDiagnostic(adapter: string, event: string, details:
29
32
  /* diagnostics must never affect request handling */
30
33
  }
31
34
  }
35
+
36
+ /** Process-local, content-free correlation aid for opt-in provider diagnostics. */
37
+ export function debugFingerprint(value: string | Uint8Array): string | undefined {
38
+ if (!isDebugEnabled()) return undefined;
39
+ try {
40
+ debugFingerprintKey ??= randomBytes(32);
41
+ return createHmac("sha256", debugFingerprintKey).update(value).digest("hex");
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ }
46
+
47
+ export interface DebugStreamDiagnosticContext {
48
+ requestId: string;
49
+ adapterName: string;
50
+ attempt?: number;
51
+ recovery?: string;
52
+ }
53
+
54
+ export type DebugStreamDiagnosticStage = "adapter" | "bridge";
55
+
56
+ /** Emit one structural line for an adapter/bridge event without retaining its content. */
57
+ export function debugStreamDiagnostic(
58
+ context: DebugStreamDiagnosticContext,
59
+ stage: DebugStreamDiagnosticStage,
60
+ sequence: number,
61
+ eventType: string,
62
+ details?: Record<string, unknown>,
63
+ ): void {
64
+ debugProviderDiagnostic(context.adapterName, "stream", {
65
+ stage,
66
+ sequence,
67
+ eventType,
68
+ ...(context.requestId !== undefined ? { requestId: context.requestId } : {}),
69
+ ...(context.attempt !== undefined ? { attempt: context.attempt } : {}),
70
+ ...(context.recovery !== undefined ? { recovery: context.recovery } : {}),
71
+ ...details,
72
+ });
73
+ }
package/src/lib/errors.ts CHANGED
@@ -4,6 +4,16 @@ export interface OcxErrorPayload {
4
4
  code: string | null;
5
5
  }
6
6
 
7
+ /** A local request validation failure that must remain a client-visible HTTP 400. */
8
+ export class OcxRequestValidationError extends Error {
9
+ readonly status = 400;
10
+
11
+ constructor(message: string) {
12
+ super(message);
13
+ this.name = "OcxRequestValidationError";
14
+ }
15
+ }
16
+
7
17
  /** OpenAI / Codex hard block for high-risk cybersecurity activity (HTTP 400 or mid-stream). */
8
18
  export const CYBER_POLICY_ERROR_CODE = "cyber_policy";
9
19
 
@@ -180,6 +190,10 @@ export function classifyError(status: number, type: string, message: string): Oc
180
190
  }
181
191
  if (
182
192
  text.includes("insufficient_quota") ||
193
+ text.includes("insufficient credits") ||
194
+ text.includes("purchase more credits") ||
195
+ text.includes("run out of credits") ||
196
+ text.includes("out of credits") ||
183
197
  text.includes("exceeded your current quota") ||
184
198
  text.includes("quota exhausted") ||
185
199
  text.includes("account quota exceeded") ||
@@ -7,12 +7,14 @@ import {
7
7
  resolvePublicAddresses,
8
8
  } from "./destination-policy";
9
9
  import { pinnedHttpGet, pinnedHttpPost } from "./pinned-http";
10
- import { outboundProxyConfigured } from "./proxy-env";
10
+ import { noProxyMatches, outboundProxyConfigured, proxyForUrl } from "./proxy-env";
11
11
  import { publicProviderBaseUrl } from "./provider-url";
12
+ import { antigravityOAuthDestinationConfigError, isCanonicalAntigravityUrl, providerTlsFetch } from "./provider-tls-profile";
13
+ import { waitForProviderRequestSlot } from "../providers/request-pacing";
12
14
 
13
15
  type ProviderGetInit = Omit<RequestInit, "body" | "method" | "redirect">;
14
16
  type ProviderPostInit = ProviderGetInit & { body: string };
15
- type ProviderOutboundConfig = Pick<OcxProviderConfig, "baseUrl" | "allowPrivateNetwork"> & {
17
+ type ProviderOutboundConfig = Pick<OcxProviderConfig, "baseUrl" | "allowPrivateNetwork"> & Partial<Pick<OcxProviderConfig, "adapter" | "authMode" | "googleMode" | "tlsProfile">> & {
16
18
  fetch?: typeof globalThis.fetch;
17
19
  };
18
20
  export interface ProviderOutboundDependencies {
@@ -40,36 +42,26 @@ function normalizeProxyHostname(hostname: string): string {
40
42
  : normalized;
41
43
  }
42
44
 
43
- function noProxyMatches(url: URL): boolean {
44
- const raw = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
45
- const hostname = normalizeProxyHostname(url.hostname);
46
- const port = url.port || (url.protocol === "https:" ? "443" : "80");
47
- for (const rawEntry of raw.split(",")) {
48
- let entry = rawEntry.trim().toLowerCase();
49
- if (!entry) continue;
50
- if (entry === "*") return true;
51
- entry = entry.replace(/^https?:\/\//, "").split("/", 1)[0]!;
52
-
53
- let entryHost = entry;
54
- let entryPort = "";
55
- const bracketed = /^\[([^\]]+)](?::(\d+))?$/.exec(entry);
56
- if (bracketed) {
57
- entryHost = bracketed[1]!;
58
- entryPort = bracketed[2] ?? "";
59
- } else if ((entry.match(/:/g)?.length ?? 0) === 1) {
60
- const separator = entry.lastIndexOf(":");
61
- const possiblePort = entry.slice(separator + 1);
62
- if (/^\d+$/.test(possiblePort)) {
63
- entryHost = entry.slice(0, separator);
64
- entryPort = possiblePort;
65
- }
66
- }
67
- if (entryPort && entryPort !== port) continue;
68
- entryHost = normalizeProxyHostname(entryHost.replace(/^\*?\./, ""));
69
- if (!entryHost) continue;
70
- if (hostname === entryHost || hostname.endsWith(`.${entryHost}`)) return true;
45
+ function antigravityProfileFetch(
46
+ name: string,
47
+ provider: ProviderOutboundConfig,
48
+ url: string,
49
+ ): typeof globalThis.fetch | undefined {
50
+ if (name !== "google-antigravity"
51
+ || provider.fetch
52
+ || provider.tlsProfile !== "antigravity-browser"
53
+ || provider.adapter !== "google"
54
+ || provider.authMode !== "oauth"
55
+ || provider.googleMode !== "cloud-code-assist"
56
+ || !isCanonicalAntigravityUrl(provider.baseUrl)
57
+ || !isCanonicalAntigravityUrl(url)) {
58
+ return undefined;
71
59
  }
72
- return false;
60
+ const nativeFetch = providerTlsFetch(name, provider as Parameters<typeof providerTlsFetch>[1], globalThis.fetch);
61
+ return (async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit): Promise<Response> => {
62
+ await waitForProviderRequestSlot(name, provider as OcxProviderConfig, undefined, init?.signal ?? undefined);
63
+ return nativeFetch(input, init);
64
+ }) as typeof globalThis.fetch;
73
65
  }
74
66
 
75
67
  let proxyBoundaryWarned = false;
@@ -112,11 +104,31 @@ async function providerOutboundRequest(
112
104
  init: ProviderGetInit | ProviderPostInit,
113
105
  dependencies: ProviderOutboundDependencies = {},
114
106
  ): Promise<Response> {
107
+ const antigravityBaseError = antigravityOAuthDestinationConfigError(name, provider);
108
+ if (antigravityBaseError) throw new ProviderOutboundPolicyError(`provider ${name} ${antigravityBaseError}`);
109
+ if (name === "google-antigravity" && !isCanonicalAntigravityUrl(url)) {
110
+ throw new ProviderOutboundPolicyError("provider google-antigravity requires a canonical Antigravity HTTPS destination for OAuth");
111
+ }
115
112
  const postUrl = method === "POST" ? new URL(url) : undefined;
116
113
  if (postUrl?.protocol !== undefined && postUrl.protocol !== "https:") {
117
114
  throw new ProviderOutboundPolicyError("provider POST URL must use HTTPS");
118
115
  }
119
- if (provider.fetch) {
116
+ const profiledFetch = antigravityProfileFetch(name, provider, url);
117
+ const fetchOverride = provider.fetch ?? profiledFetch;
118
+ if (profiledFetch) {
119
+ // Keep the profiled executor behind the same pre-dispatch DNS boundary as
120
+ // the pinned path. A proxy owns peer selection, so match the existing
121
+ // providerOutbound proxy boundary and deliberately skip local resolution.
122
+ if (!proxyForUrl(url)) {
123
+ const resolveAddresses = dependencies.resolveAddresses ?? resolvePublicAddresses;
124
+ await resolveAddresses(url, {
125
+ context: "provider URL",
126
+ allowPrivateNetwork: false,
127
+ });
128
+ }
129
+ return fetchOverride!(url, { ...init, method, redirect: "manual" });
130
+ }
131
+ if (fetchOverride) {
120
132
  // A caller-owned executor cannot be peer-pinned here. This branch keeps literal/config
121
133
  // checks and redirect blocking, but does not provide the resolved-address guarantees of
122
134
  // the built-in transport. Main-request migration must define that executor contract first.
@@ -135,7 +147,7 @@ async function providerOutboundRequest(
135
147
  });
136
148
  if (destinationError) throw new ProviderOutboundPolicyError(destinationError);
137
149
  }
138
- return provider.fetch(url, { ...init, method, redirect: "manual" });
150
+ return fetchOverride(url, { ...init, method, redirect: "manual" });
139
151
  }
140
152
  const parsed = postUrl ?? new URL(url);
141
153
  const proxyConfigured = configuredProxyFor();