@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
@@ -0,0 +1,295 @@
1
+ /**
2
+ * In-process WebSocket Relay Hub for Google AI Studio Web / Build sessions.
3
+ */
4
+
5
+ export interface WsRelayRequest {
6
+ url: string;
7
+ method: string;
8
+ headers?: Record<string, string>;
9
+ body?: string;
10
+ }
11
+
12
+ export interface WsRelayStreamResult {
13
+ chunks: AsyncIterable<string>;
14
+ status?: number;
15
+ headers?: Record<string, string>;
16
+ }
17
+
18
+ export interface WebSocketLike {
19
+ send(data: string): void;
20
+ close(code?: number, reason?: string): void;
21
+ }
22
+
23
+ interface PendingStream {
24
+ pushChunk(chunk: string): void;
25
+ finish(): void;
26
+ fail(err: Error): void;
27
+ }
28
+
29
+ export interface AiStudioRelayHub {
30
+ hasActiveSessions(): boolean;
31
+ getActiveSessionCount(): number;
32
+ registerSession(id: string, ws: WebSocketLike): void;
33
+ unregisterSession(id: string): void;
34
+ reset(): void;
35
+ dispatchStream(req: WsRelayRequest, signal?: AbortSignal): Promise<WsRelayStreamResult>;
36
+ handleClientMessage(sessionId: string, raw: string): void;
37
+ }
38
+
39
+ export function createAiStudioRelayHub(): AiStudioRelayHub {
40
+ const sessions = new Map<string, WebSocketLike>();
41
+ const pendingRequests = new Map<string, PendingStream>();
42
+ const pendingBySession = new Map<string, Set<string>>();
43
+ let sessionRoundRobin = 0;
44
+
45
+ function getNextSession(): { id: string; ws: WebSocketLike } | undefined {
46
+ for (const [id, ws] of sessions.entries()) {
47
+ if ((ws as any).readyState !== undefined && (ws as any).readyState > 1) {
48
+ sessions.delete(id);
49
+ }
50
+ }
51
+ const arr = Array.from(sessions.values());
52
+ if (arr.length === 0) return undefined;
53
+ const id = Array.from(sessions.keys())[sessionRoundRobin % arr.length]!;
54
+ const ws = sessions.get(id)!;
55
+ sessionRoundRobin = (sessionRoundRobin + 1) % arr.length;
56
+ return { id, ws };
57
+ }
58
+
59
+ return {
60
+ hasActiveSessions() {
61
+ return sessions.size > 0;
62
+ },
63
+
64
+ getActiveSessionCount() {
65
+ return sessions.size;
66
+ },
67
+
68
+ registerSession(id: string, ws: WebSocketLike) {
69
+ sessions.set(id, ws);
70
+ console.log(`[AIStudioHub] registered session: ${id}, total: ${sessions.size}`);
71
+ },
72
+
73
+ unregisterSession(id: string) {
74
+ sessions.delete(id);
75
+ console.log(`[AIStudioHub] unregistered session: ${id}, remaining: ${sessions.size}`);
76
+ const requestIds = pendingBySession.get(id);
77
+ if (!requestIds) return;
78
+ for (const requestId of requestIds) {
79
+ pendingRequests.get(requestId)?.fail(new Error("Google AI Studio browser session disconnected"));
80
+ }
81
+ pendingBySession.delete(id);
82
+ },
83
+
84
+ reset() {
85
+ for (const pending of pendingRequests.values()) {
86
+ pending.fail(new Error("AI Studio relay hub reset"));
87
+ }
88
+ sessions.clear();
89
+ pendingRequests.clear();
90
+ pendingBySession.clear();
91
+ sessionRoundRobin = 0;
92
+ },
93
+
94
+ async dispatchStream(req: WsRelayRequest, signal?: AbortSignal): Promise<WsRelayStreamResult> {
95
+ const session = getNextSession();
96
+ if (!session) {
97
+ throw new Error("No active Google AI Studio browser session connected. Open the AI Studio bridge in your browser to start.");
98
+ }
99
+ const { id: sessionId, ws } = session;
100
+
101
+ const reqId = `req_${crypto.randomUUID()}`;
102
+ const chunkQueue: string[] = [];
103
+ let resolveNextChunk: ((value: IteratorResult<string>) => void) | null = null;
104
+ let rejectNextChunk: ((err: Error) => void) | null = null;
105
+ let isFinished = false;
106
+ let finishError: Error | null = null;
107
+
108
+ const pending: PendingStream = {
109
+ pushChunk(chunk: string) {
110
+ if (isFinished) return;
111
+ if (resolveNextChunk) {
112
+ const r = resolveNextChunk;
113
+ resolveNextChunk = null;
114
+ rejectNextChunk = null;
115
+ r({ value: chunk, done: false });
116
+ } else {
117
+ chunkQueue.push(chunk);
118
+ }
119
+ },
120
+ finish() {
121
+ if (isFinished) return;
122
+ isFinished = true;
123
+ pendingRequests.delete(reqId);
124
+ pendingBySession.get(sessionId)?.delete(reqId);
125
+ if (resolveNextChunk) {
126
+ const r = resolveNextChunk;
127
+ resolveNextChunk = null;
128
+ rejectNextChunk = null;
129
+ r({ value: undefined as any, done: true });
130
+ }
131
+ },
132
+ fail(err: Error) {
133
+ if (isFinished) return;
134
+ isFinished = true;
135
+ finishError = err;
136
+ pendingRequests.delete(reqId);
137
+ pendingBySession.get(sessionId)?.delete(reqId);
138
+ if (rejectNextChunk) {
139
+ const r = rejectNextChunk;
140
+ resolveNextChunk = null;
141
+ rejectNextChunk = null;
142
+ r(err);
143
+ }
144
+ },
145
+ };
146
+
147
+ pendingRequests.set(reqId, pending);
148
+ const requestIds = pendingBySession.get(sessionId) ?? new Set<string>();
149
+ requestIds.add(reqId);
150
+ pendingBySession.set(sessionId, requestIds);
151
+
152
+ if (signal) {
153
+ signal.addEventListener("abort", () => {
154
+ try {
155
+ ws.send(JSON.stringify({ id: reqId, type: "abort" }));
156
+ } catch (err) {
157
+ void err;
158
+ }
159
+ pending.fail(new Error("Request aborted by client"));
160
+ });
161
+ }
162
+
163
+ const msg = {
164
+ id: reqId,
165
+ type: "http_request",
166
+ payload: {
167
+ url: req.url,
168
+ method: req.method,
169
+ headers: req.headers ?? {},
170
+ body: req.body ?? "",
171
+ },
172
+ };
173
+
174
+ ws.send(JSON.stringify(msg));
175
+
176
+ const asyncIterable: AsyncIterable<string> = {
177
+ [Symbol.asyncIterator]() {
178
+ return {
179
+ async next(): Promise<IteratorResult<string>> {
180
+ if (chunkQueue.length > 0) {
181
+ return { value: chunkQueue.shift()!, done: false };
182
+ }
183
+ if (isFinished) {
184
+ if (finishError) throw finishError;
185
+ return { value: undefined as any, done: true };
186
+ }
187
+ return new Promise<IteratorResult<string>>((resolve, reject) => {
188
+ resolveNextChunk = resolve;
189
+ rejectNextChunk = reject;
190
+ });
191
+ },
192
+ };
193
+ },
194
+ };
195
+
196
+ return { chunks: asyncIterable };
197
+ },
198
+
199
+ handleClientMessage(sessionId: string, raw: string) {
200
+ let data: any;
201
+ try {
202
+ data = JSON.parse(raw);
203
+ } catch (err) {
204
+ console.log("[AIStudioHub] failed to parse JSON message:", err);
205
+ void err;
206
+ return;
207
+ }
208
+
209
+ const { id, type, payload } = data || {};
210
+ if (!id) {
211
+ console.log("[AIStudioHub] message missing id");
212
+ return;
213
+ }
214
+
215
+ const pending = pendingRequests.get(id);
216
+ if (!pending || !pendingBySession.get(sessionId)?.has(id)) {
217
+ console.log("[AIStudioHub] no pending request for id:", id, "session:", sessionId);
218
+ return;
219
+ }
220
+
221
+ if (type === "stream_chunk") {
222
+ if (payload?.data) {
223
+ pending.pushChunk(payload.data);
224
+ }
225
+ } else if (type === "stream_end" || type === "http_response") {
226
+ if (payload?.body) {
227
+ pending.pushChunk(payload.body);
228
+ }
229
+ pending.finish();
230
+ } else if (type === "error") {
231
+ console.log(`[AIStudioHub] client error from ${sessionId}:`, payload?.error);
232
+ pending.fail(new Error(payload?.error || "Upstream AI Studio error"));
233
+ }
234
+ },
235
+ };
236
+ }
237
+
238
+ export const globalAiStudioRelayHub = createAiStudioRelayHub();
239
+
240
+ export function getAiStudioUserScript(listenPort: number): string {
241
+ return `// ==UserScript==
242
+ // @name OpenCodex AI Studio Relay Bridge
243
+ // @namespace https://opencodex.dev/
244
+ // @version 1.0.0
245
+ // @description Relays requests from opencodex to Google AI Studio session
246
+ // @match https://aistudio.google.com/*
247
+ // @grant none
248
+ // @run-at document-idle
249
+ // ==/UserScript==
250
+
251
+ (function() {
252
+ 'use strict';
253
+ const WS_URL = "ws://127.0.0.1:${listenPort}/v1/ws/aistudio";
254
+ let ws;
255
+ function connect() {
256
+ try { ws = new WebSocket(WS_URL); } catch { setTimeout(connect, 3000); return; }
257
+ ws.onmessage = async (e) => {
258
+ try {
259
+ const msg = JSON.parse(e.data);
260
+ if (msg.type === "http_request") {
261
+ const { id, payload } = msg;
262
+ try {
263
+ const res = await fetch(payload.url, { method: payload.method, headers: payload.headers, body: payload.body, credentials: "include" });
264
+ if (payload.url.includes("streamGenerateContent") && res.body) {
265
+ const reader = res.body.getReader();
266
+ const decoder = new TextDecoder();
267
+ while (true) {
268
+ const { done, value } = await reader.read();
269
+ if (done) break;
270
+ ws.send(JSON.stringify({ id, type: "stream_chunk", payload: { data: decoder.decode(value, { stream: true }) } }));
271
+ }
272
+ ws.send(JSON.stringify({ id, type: "stream_end", payload: {} }));
273
+ } else {
274
+ const text = await res.text();
275
+ ws.send(JSON.stringify({ id, type: "http_response", payload: { body: text, status: res.status } }));
276
+ }
277
+ } catch (err) {
278
+ ws.send(JSON.stringify({ id, type: "error", payload: { error: String(err) } }));
279
+ }
280
+ }
281
+ } catch (err) {
282
+ void err;
283
+ }
284
+ };
285
+ ws.onclose = () => setTimeout(connect, 3000);
286
+ }
287
+ connect();
288
+ })();
289
+ `;
290
+ }
291
+
292
+ export function getAiStudioBridgeHtml(listenPort: number): string {
293
+ const extensionPath = typeof process !== "undefined" ? process.cwd() + "/integrations/aistudio-extension" : "integrations/aistudio-extension";
294
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Google AI Studio Bridge</title><style>body{font-family:system-ui,-apple-system,sans-serif;background:#121316;color:#e1e3e6;padding:2rem;max-width:680px;margin:auto;line-height:1.5}.card{background:#1e1f23;border:1px solid #2d2f34;border-radius:8px;padding:1.5rem;margin-bottom:1.5rem}.status{display:inline-block;padding:0.25rem 0.75rem;border-radius:4px;font-weight:500}.connected{background:#163820;color:#4ade80}.disconnected{background:#3b1717;color:#f87171}code{background:#2a2c31;padding:0.2rem 0.4rem;border-radius:4px}.btn{display:inline-block;padding:0.5rem 1rem;background:#3b82f6;color:#fff;text-decoration:none;border-radius:6px;font-weight:500;margin-top:0.5rem}pre{background:#16171a;padding:1rem;border-radius:6px;overflow-x:auto;font-size:0.85rem}</style></head><body><div class="card"><h2>Google AI Studio Bridge</h2><p>Status: <span id="status" class="status disconnected">Connecting...</span></p><p>This bridge connects <code>opencodex</code> with your active Google AI Studio / Google AI Pro session.</p></div><div class="card"><h3>🧩 Option 1: Unpacked Extension for Brave / Chrome (Zero Tab Overhead)</h3><p>Run entirely in the background without needing an active browser tab:</p><ol style="padding-left:1.2rem;margin:0.5rem 0"><li>Open <code>brave://extensions</code> (or <code>chrome://extensions</code>) and toggle <strong>Developer mode</strong> in the top-right.</li><li>Click <strong>Load unpacked</strong> and select this directory:<br><code id="ext-path">${extensionPath}</code></li></ol><button class="btn" style="cursor:pointer" onclick="navigator.clipboard.writeText(document.getElementById('ext-path').textContent);this.textContent='Copied!'">📋 Copy Extension Directory Path</button></div><div class="card"><h3>🌐 Option 2: Userscript (Tampermonkey) or Console</h3><p>Install via Tampermonkey/Violentmonkey or paste in Developer Tools Console:</p><p><a class="btn" href="/aistudio/bridge.user.js">⚡ Install Userscript</a> <a class="btn" style="background:#374151;margin-left:0.5rem" href="https://aistudio.google.com" target="_blank">Open aistudio.google.com ↗</a></p><pre style="margin-top:1rem"><code>const ws=new WebSocket('ws://127.0.0.1:${listenPort}/v1/ws/aistudio');ws.onmessage=async(e)=>{const{id,payload,type}=JSON.parse(e.data);if(type==='http_request'){try{const res=await fetch(payload.url,{method:payload.method,headers:payload.headers,body:payload.body,credentials:'include'});if(payload.url.includes('streamGenerateContent')&&res.body){const r=res.body.getReader(),d=new TextDecoder();while(true){const{done,value}=await r.read();if(done)break;ws.send(JSON.stringify({id,type:'stream_chunk',payload:{data:d.decode(value,{stream:true})}}))}ws.send(JSON.stringify({id,type:'stream_end',payload:{}}))}else{const body=await res.text();ws.send(JSON.stringify({id,type:'http_response',payload:{body,status:res.status}}))}}catch(err){ws.send(JSON.stringify({id,type:'error',payload:{error:String(err)}}))}}};console.log('opencodex AI Studio relay active!');</code></pre></div><script>let ws;const statusEl=document.getElementById("status");function connect(){const proto=location.protocol==="https:"?"wss:":"ws:";ws=new WebSocket(proto+"//"+location.host+"/v1/ws/aistudio");ws.onopen=()=>{statusEl.textContent="🟢 Connected (Relay Active)";statusEl.className="status connected"};ws.onclose=()=>{statusEl.textContent="🔴 Disconnected (Retrying...)";statusEl.className="status disconnected";setTimeout(connect,2000)}};connect()</script></body></html>`;
295
+ }
@@ -10,6 +10,7 @@ import {
10
10
  } from "../config";
11
11
  import {
12
12
  apiKeyTransportConfigError,
13
+ azureCredentialConfigError,
13
14
  booleanRecordConfigError,
14
15
  modelAdapterRecordConfigError,
15
16
  nonBlankStringArrayConfigError,
@@ -19,6 +20,7 @@ import {
19
20
  providerHeadersConfigError,
20
21
  reasoningSummaryDeliveryRecordConfigError,
21
22
  upstreamHttpVersionConfigError,
23
+ isAzureIdentityProvider,
22
24
  } from "../config/provider-validation";
23
25
  import { providerDestinationConfigError } from "../lib/destination-policy";
24
26
  import { redactSecretString } from "../lib/redact";
@@ -26,8 +28,10 @@ import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode
26
28
  import { providerConfigSeed } from "../providers/derive";
27
29
  import type { OcxConfig, OcxProviderConfig } from "../types";
28
30
  import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
31
+ import { modelAutoCompactTokenLimitsConfigError } from "../providers/auto-compact-budget";
29
32
  import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
30
33
  import { xaiResponsesOptInState } from "../providers/xai-responses-opt-in";
34
+ import { antigravityOAuthDestinationConfigError, getProviderTlsProfileStatus, providerTlsProfileConfigError } from "../lib/provider-tls-profile";
31
35
 
32
36
  let _corsOrigin = "http://localhost:10100";
33
37
  export function setCorsOrigin(port: number): void { _corsOrigin = `http://localhost:${port}`; }
@@ -565,6 +569,8 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
565
569
  if (contextOverlayError) return contextOverlayError;
566
570
  delete canonicalCandidate.contextWindow;
567
571
  delete canonicalCandidate.modelContextWindows;
572
+ // User-owned soft compaction policy; it does not alter the canonical transport seed.
573
+ delete canonicalCandidate.modelAutoCompactTokenLimits;
568
574
  const canonical = seed && sameCanonicalProviderSeed(canonicalCandidate, seed);
569
575
  if (!canonical) {
570
576
  return `provider ${name} must equal the canonical built-in provider seed`;
@@ -573,6 +579,12 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
573
579
  return `provider ${name} must not include codexAccountMode`;
574
580
  }
575
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}`;
576
588
  const baseUrlError = providerBaseUrlConfigError(typed.baseUrl);
577
589
  if (baseUrlError) return `provider ${name} ${baseUrlError}`;
578
590
  if (effectiveGoogleMode(name, typed) === "vertex" && typed.location !== undefined) {
@@ -605,8 +617,17 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
605
617
  }
606
618
  const apiKeyTransportError = apiKeyTransportConfigError(typed);
607
619
  if (apiKeyTransportError) return `provider ${name} ${apiKeyTransportError}`;
620
+ const azureCredentialError = azureCredentialConfigError(raw);
621
+ if (azureCredentialError) return `provider ${JSON.stringify(redactSecretString(name))} ${azureCredentialError}`;
608
622
  const maxInputError = positiveIntegerRecordConfigError(raw.modelMaxInputTokens, "modelMaxInputTokens");
609
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
+ }
610
631
  const reasoningSummariesError = booleanRecordConfigError(raw.modelSupportsReasoningSummaries, "modelSupportsReasoningSummaries");
611
632
  if (reasoningSummariesError) return `provider ${name} ${reasoningSummariesError}`;
612
633
  const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError(
@@ -689,6 +710,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
689
710
  adapter: provider.adapter,
690
711
  baseUrl: publicProviderBaseUrl(provider.baseUrl),
691
712
  hasApiKey: !!provider.apiKey,
713
+ hasAzureCredential: isAzureIdentityProvider(provider),
692
714
  hasHeaders: !!provider.headers && Object.keys(provider.headers).length > 0,
693
715
  };
694
716
  if (name === "xai") {
@@ -699,14 +721,17 @@ export function safeConfigDTO(config: OcxConfig): unknown {
699
721
  "disabled",
700
722
  "allowPrivateNetwork",
701
723
  "authMode",
724
+ "googleMode",
702
725
  "apiKeyTransport",
703
726
  "keyOptional",
704
727
  "freeTier",
705
728
  "liveModels",
706
729
  "requestPacing",
730
+ "tlsProfile",
707
731
  "models",
708
732
  "contextWindow",
709
733
  "modelContextWindows",
734
+ "modelAutoCompactTokenLimits",
710
735
  "defaultMaxOutputTokens",
711
736
  "modelMaxOutputTokens",
712
737
  "openRouterRouting",
@@ -728,6 +753,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
728
753
  ] as const) {
729
754
  copyIfDefined(dto, provider, key);
730
755
  }
756
+ dto.tlsProfileStatus = provider.tlsProfile === undefined ? "disabled" : getProviderTlsProfileStatus(name, provider);
731
757
  const modelCosts = sanitizeModelCostsForDisplay(provider.modelCosts);
732
758
  if (modelCosts) dto.modelCosts = modelCosts;
733
759
  // Resolve the note by DESTINATION, not by name. A preset saved under a custom name is
@@ -748,6 +774,9 @@ export function safeConfigDTO(config: OcxConfig): unknown {
748
774
  defaultProvider: config.defaultProvider,
749
775
  codexAutoStart: codexAutoStartEnabled(config),
750
776
  websockets: config.websockets,
777
+ // The GUI's browser-open toggle reads and writes this; absent means the
778
+ // historical auto-open behavior.
779
+ oauthOpenBrowser: config.oauthOpenBrowser !== false,
751
780
  providers,
752
781
  };
753
782
  }
@@ -9,6 +9,7 @@ import { FORWARD_HEADERS } from "../adapters/openai-responses";
9
9
  import {
10
10
  assertChatCompletionsRoutingBody,
11
11
  ChatCompletionsRequestError,
12
+ copyChatResponsesSessionHeaders,
12
13
  chatCompletionsToResponsesBody,
13
14
  } from "../chat/inbound";
14
15
  import {
@@ -201,6 +202,7 @@ async function handleChatCompletionsWithBudget(
201
202
  const value = req.headers.get(name);
202
203
  if (value) headers.set(name, value);
203
204
  }
205
+ copyChatResponsesSessionHeaders(req.headers, headers);
204
206
  // Prefer main ChatGPT auth so OpenAI-backed sidecars remain reachable on routed turns.
205
207
  if (!directRoute) {
206
208
  // This enrichment is optional for routed/non-main providers. If native main
@@ -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 token: string;
182
+ let snapshot: OAuthAccessSnapshot;
178
183
  try {
179
- // Race the OAuth refresh against the deadline signal. getValidAccessToken
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 project = getOAuthCredentialProjectId("google-antigravity");
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
- // Image generation is a paid, non-idempotent POST. A transport failure is
238
- // ambiguous: the upstream may have accepted the request before the
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 ccaAmbiguousImageTransportError("upstream request timed out");
247
+ return formatErrorResponse(504, "upstream_error", `CCA image generation timed out: ${safeMsg}`);
254
248
  }
255
- // Network/DNS/runtime errors may embed the request URL or headers verbatim
256
- // (e.g. "fetch failed: https://…/v1internal:generateContent"). The token
257
- // lives in an Authorization header, not in the URL, but sanitize defensively
258
- // so no upstream-rejected credential or query param can reach the client,
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
@@ -40,6 +40,7 @@ import {
40
40
  enforceAppOwnedMemoryBudget,
41
41
  resolveAppOwnedMemoryBudgetBytes,
42
42
  } from "../lib/app-owned-memory";
43
+ import { getAiStudioBridgeHtml, getAiStudioUserScript, globalAiStudioRelayHub } from "./aistudio-ws-hub";
43
44
  import {
44
45
  registerAppOwnedMemorySweepFallback,
45
46
  registerDefaultAppOwnedMemoryStores,
@@ -668,6 +669,12 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
668
669
  if (path === "/v1/realtime" || path === "/v1/live") {
669
670
  return req.headers.get("upgrade")?.toLowerCase() === "websocket";
670
671
  }
672
+ if (path === "/v1/ws/aistudio" || path === "/aistudio/ws") {
673
+ return req.headers.get("upgrade")?.toLowerCase() === "websocket";
674
+ }
675
+ if (path === "/v1/ws/aistudio/status") return req.method === "GET";
676
+ if (path === "/api/aistudio/session") return req.method === "POST";
677
+ if (path === "/aistudio/bridge" || path === "/aistudio/bridge.user.js") return req.method === "GET";
671
678
  return false;
672
679
  }
673
680
 
@@ -903,6 +910,73 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
903
910
  return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
904
911
  }
905
912
 
913
+ if (url.pathname === "/v1/ws/aistudio" || url.pathname === "/aistudio/ws") {
914
+ if (isDraining()) return drainingResponse(req, policy);
915
+ const admission = resolveApiAuth(req, policy);
916
+ if (!admission) {
917
+ return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
918
+ }
919
+ const origin = req.headers.get("Origin");
920
+ const isExtensionOrigin = origin?.startsWith("chrome-extension://");
921
+ if (!isAllowedRequestOrigin(req, policy) && origin !== "https://aistudio.google.com" && !isExtensionOrigin) {
922
+ return withCors(formatErrorResponse(403, "origin_rejected", "AI Studio relay WebSocket blocked: non-local Origin"), req, policy);
923
+ }
924
+ const sessionId = "aistudio_" + crypto.randomUUID().slice(0, 8);
925
+ if (requestServer.upgrade(req, {
926
+ data: { kind: "aistudio-relay", aistudioSessionId: sessionId },
927
+ })) return undefined as unknown as Response;
928
+ return withCors(formatErrorResponse(426, "upgrade_required", "AI Studio WebSocket upgrade failed"), req, policy);
929
+ }
930
+
931
+ if (url.pathname === "/v1/ws/aistudio/status" && req.method === "GET") {
932
+ return jsonResponse({
933
+ activeSessions: globalAiStudioRelayHub.getActiveSessionCount(),
934
+ hasActiveSessions: globalAiStudioRelayHub.hasActiveSessions(),
935
+ });
936
+ }
937
+
938
+ if (url.pathname === "/api/aistudio/session" && req.method === "POST") {
939
+ const admission = resolveApiAuth(req, policy);
940
+ if (!admission) {
941
+ return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
942
+ }
943
+ const origin = req.headers.get("Origin");
944
+ const isExtensionOrigin = origin?.startsWith("chrome-extension://");
945
+ if (!isAllowedRequestOrigin(req, policy) && origin !== "https://aistudio.google.com" && !isExtensionOrigin) {
946
+ return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin request blocked"), req, policy);
947
+ }
948
+ try {
949
+ const bodyJson = (await req.json()) as any;
950
+ const { saveAiStudioSession, saveAiStudioSessionFromToken } = await import("../oauth/aistudio-session-sync");
951
+ if (bodyJson.token && typeof bodyJson.token === "string") {
952
+ saveAiStudioSessionFromToken(bodyJson.token);
953
+ } else if (Array.isArray(bodyJson.cookies)) {
954
+ saveAiStudioSession({
955
+ selectedProject: bodyJson.selectedProject || "",
956
+ windowId: bodyJson.windowId || "",
957
+ cookies: bodyJson.cookies,
958
+ });
959
+ } else {
960
+ return withCors(jsonResponse({ error: "invalid session payload" }, 400), req, policy);
961
+ }
962
+ return withCors(jsonResponse({ ok: true, message: "AI Studio session updated successfully" }), req, policy);
963
+ } catch (err) {
964
+ return withCors(jsonResponse({ error: String(err) }, 400), req, policy);
965
+ }
966
+ }
967
+
968
+ if (url.pathname === "/aistudio/bridge" && req.method === "GET") {
969
+ const listenPort = (server.port ?? config.port) || 10100;
970
+ const bridgeHtml = getAiStudioBridgeHtml(listenPort);
971
+ return new Response(bridgeHtml, { headers: { "Content-Type": "text/html; charset=utf-8" } });
972
+ }
973
+
974
+ if (url.pathname === "/aistudio/bridge.user.js" && req.method === "GET") {
975
+ const listenPort = (server.port ?? config.port) || 10100;
976
+ const userScript = getAiStudioUserScript(listenPort);
977
+ return new Response(userScript, { headers: { "Content-Type": "application/javascript; charset=utf-8" } });
978
+ }
979
+
906
980
  if (url.pathname === "/healthz" && req.method === "GET") {
907
981
  // service/pid/port let CLI liveness reject foreign 200s and verify pid identity.
908
982
  const healthPort = server.port ?? listenPort;
@@ -1564,6 +1638,10 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1564
1638
  // Text frames. response.processed is a no-op ack. close() aborts the upstream (RC2 parity).
1565
1639
  // Live sideband sockets (kind=live-sideband) are a transparent bidirectional relay instead.
1566
1640
  open(ws: ServerWebSocket<WsData>) {
1641
+ if (ws.data.kind === "aistudio-relay" && ws.data.aistudioSessionId) {
1642
+ globalAiStudioRelayHub.registerSession(ws.data.aistudioSessionId, ws);
1643
+ return;
1644
+ }
1567
1645
  if (ws.data.kind === "live-sideband") {
1568
1646
  if (!ws.data.liveTurnAdmissionLease) {
1569
1647
  closeLiveSideband(ws, 1013, "server busy");
@@ -1580,6 +1658,18 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1580
1658
  registerCodexWebSocket(ws);
1581
1659
  },
1582
1660
  message(ws: ServerWebSocket<WsData>, raw: string | Buffer) {
1661
+ if (ws.data.kind === "aistudio-relay" && ws.data.aistudioSessionId) {
1662
+ const text = typeof raw === "string"
1663
+ ? raw
1664
+ : Buffer.isBuffer(raw)
1665
+ ? raw.toString("utf-8")
1666
+ : new TextDecoder().decode(raw as any);
1667
+ globalAiStudioRelayHub.handleClientMessage(
1668
+ ws.data.aistudioSessionId,
1669
+ text
1670
+ );
1671
+ return;
1672
+ }
1583
1673
  if (ws.data.kind === "live-sideband") {
1584
1674
  if (ws.data.liveClosing) return;
1585
1675
  const rawBytes = webSocketFrameBytes(raw);
@@ -1758,6 +1848,10 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1758
1848
  })();
1759
1849
  },
1760
1850
  close(ws: ServerWebSocket<WsData>) {
1851
+ if (ws.data.kind === "aistudio-relay" && ws.data.aistudioSessionId) {
1852
+ globalAiStudioRelayHub.unregisterSession(ws.data.aistudioSessionId);
1853
+ return;
1854
+ }
1761
1855
  if (ws.data.kind === "live-sideband") {
1762
1856
  closeLiveSideband(ws);
1763
1857
  return;