@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.
- package/README.md +2 -2
- package/gui/dist/assets/index-CIDo4y4k.js +102 -0
- package/gui/dist/assets/index-DrSQdTRd.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +8 -5
- package/src/adapters/anthropic.ts +20 -6
- package/src/adapters/azure.ts +20 -4
- package/src/adapters/base.ts +3 -1
- package/src/adapters/command-code.ts +131 -17
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-transport.ts +4 -2
- package/src/adapters/cursor/native-exec-fs.ts +13 -12
- package/src/adapters/cursor/native-exec-network.ts +3 -5
- package/src/adapters/cursor/native-exec-policy.ts +47 -0
- package/src/adapters/cursor/native-exec-shell.ts +13 -25
- package/src/adapters/cursor/native-exec.ts +18 -10
- package/src/adapters/cursor/protobuf-events.ts +186 -9
- package/src/adapters/cursor/protobuf-request.ts +53 -18
- package/src/adapters/cursor/request-builder.ts +11 -3
- package/src/adapters/cursor/tool-definitions.ts +49 -2
- package/src/adapters/cursor/tool-result-normalize.ts +21 -8
- package/src/adapters/cursor/types.ts +11 -3
- package/src/adapters/cursor.ts +123 -0
- package/src/adapters/google-aistudio-parser.ts +49 -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 +242 -28
- package/src/adapters/kiro-tools.ts +20 -9
- package/src/adapters/openai-chat.ts +9 -0
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +112 -9
- package/src/chat/inbound.ts +15 -0
- package/src/claude/context-windows.ts +16 -9
- package/src/cli/doctor.ts +2 -2
- package/src/cli/index.ts +10 -3
- package/src/cli/provider.ts +6 -0
- package/src/cli/status.ts +23 -0
- package/src/codex/auth-api.ts +4 -2
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +12 -12
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/model-metadata.ts +39 -12
- package/src/codex/catalog/parsing.ts +38 -27
- package/src/codex/catalog/provider-fetch.ts +232 -133
- package/src/codex/catalog/sync.ts +1 -1
- package/src/codex/convergence.ts +5 -0
- package/src/codex/shim.ts +56 -3
- package/src/config/provider-validation.ts +37 -0
- package/src/config.ts +78 -2
- package/src/generated/compatibility-version.json +187 -119
- package/src/generated/model-metadata.ts +3 -0
- package/src/images/loop.ts +37 -6
- package/src/lib/azure-identity.ts +154 -0
- package/src/lib/debug.ts +42 -0
- package/src/lib/errors.ts +14 -0
- 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/oauth/aistudio-native-daemon.ts +62 -0
- package/src/oauth/aistudio-session-sync.ts +95 -0
- 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-aistudio-auth.ts +98 -0
- package/src/oauth/google-antigravity.ts +42 -5
- package/src/oauth/index.ts +15 -3
- package/src/oauth/key-providers.ts +8 -0
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/login-cli.ts +66 -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 +5 -1
- 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 +148 -50
- package/src/providers/registry.ts +27 -4
- package/src/providers/request-pacing.ts +33 -6
- package/src/providers/xai-transport.ts +21 -0
- package/src/responses/google-provider-options.ts +36 -0
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +11 -0
- package/src/responses/provider-opaque-metadata.ts +3 -3
- package/src/responses/schema.ts +37 -0
- package/src/responses/state.ts +94 -4
- package/src/router.ts +11 -2
- package/src/routing/account-pool/cooldown.ts +8 -0
- package/src/routing/account-pool/index.ts +1 -0
- package/src/server/aistudio-ws-hub.ts +295 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +2 -0
- package/src/server/images.ts +19 -35
- package/src/server/index.ts +94 -0
- package/src/server/management/agent-settings-routes.ts +205 -15
- package/src/server/management/combo-routes.ts +6 -0
- package/src/server/management/config-routes.ts +31 -5
- package/src/server/management/logs-usage-routes.ts +11 -5
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/oauth-account-routes.ts +38 -7
- package/src/server/management/provider-routes.ts +113 -15
- package/src/server/management/routing-profile-routes.ts +3 -0
- package/src/server/port-reclaim.ts +19 -1
- package/src/server/request-log-conversation.ts +12 -0
- package/src/server/request-log.ts +23 -1
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/compact.ts +30 -1
- package/src/server/responses/core.ts +363 -156
- package/src/server/responses/empty-completion-guard.ts +35 -6
- package/src/server/responses/fetch-helpers.ts +18 -5
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/v2-native-parent-override.ts +59 -0
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-undeclared-tool-guard.ts +90 -8
- package/src/server/ws-bridge.ts +2 -1
- package/src/service.ts +1 -1
- package/src/smoke/fingerprint-cache.ts +133 -0
- package/src/smoke/live-scenarios.ts +33 -0
- package/src/smoke/runner.ts +119 -0
- package/src/types/config.ts +16 -1
- package/src/types/provider.ts +18 -1
- package/src/types/request.ts +30 -0
- package/src/types/tools.ts +51 -0
- package/src/types.ts +6 -0
- package/src/usage/command-code-manifest.ts +116 -0
- package/src/usage/cost.ts +2 -2
- package/src/usage/expected-prices.ts +83 -0
- package/src/usage/log.ts +2 -2
- package/src/usage/summary.ts +34 -12
- package/src/web-search/gemini-executor.ts +6 -4
- package/src/web-search/index.ts +16 -8
- package/src/web-search/loop.ts +42 -6
- package/gui/dist/assets/index-BG43zwVe.js +0 -102
- 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
|
+
}
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -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
|
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
|
package/src/server/index.ts
CHANGED
|
@@ -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;
|