sneakoscope 10.7.0 → 10.7.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 +27 -5
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/install-helpers-codex-lb-config.js +11 -8
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/agents/agent-role-config.js +6 -2
- package/dist/core/agents/sks-model-pin-hygiene.js +150 -0
- package/dist/core/codex/codex-config-guard.js +18 -13
- package/dist/core/codex-app/codex-app-ui-state-snapshot.js +7 -1
- package/dist/core/codex-lb/bridge-contracts.js +5 -0
- package/dist/core/codex-lb/combined-catalog/builder.js +4 -2
- package/dist/core/codex-lb/combined-catalog/latest-generation.js +7 -34
- package/dist/core/codex-lb/combined-catalog/normalize.js +3 -1
- package/dist/core/codex-lb/desktop-bridge/codex-lb-turn-state.js +35 -0
- package/dist/core/codex-lb/desktop-bridge/compaction-adapter.js +3 -0
- package/dist/core/codex-lb/desktop-bridge/header-policy.js +10 -4
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +92 -30
- package/dist/core/codex-lb/desktop-bridge/provider-handoff.js +79 -0
- package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +57 -13
- package/dist/core/codex-lb/desktop-bridge/security.js +101 -55
- package/dist/core/codex-lb/desktop-bridge/server.js +10 -2
- package/dist/core/codex-lb/desktop-bridge/types.js +3 -0
- package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +3 -0
- package/dist/core/codex-lb/desktop-controller-v3/catalog.js +13 -0
- package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +0 -9
- package/dist/core/codex-lb/desktop-service.js +10 -13
- package/dist/core/codex-lb/request-route-resolver.js +1 -1
- package/dist/core/codex-runtime/codex-desktop-config-policy.js +0 -1
- package/dist/core/hooks-runtime/jev-spawn-routing.js +1 -5
- package/dist/core/hooks-runtime/parent-orchestration-gate.js +6 -8
- package/dist/core/hooks-runtime.js +13 -9
- package/dist/core/init/skills.js +22 -3
- package/dist/core/provider/provider-context.js +4 -1
- package/dist/core/search/context.js +2 -2
- package/dist/core/update/update-migration-state.js +10 -40
- package/dist/core/version.js +1 -1
- package/package.json +2 -2
|
@@ -2,16 +2,19 @@ import http, {} from 'node:http';
|
|
|
2
2
|
import https from 'node:https';
|
|
3
3
|
import zlib from 'node:zlib';
|
|
4
4
|
import { pipeline } from 'node:stream/promises';
|
|
5
|
-
import { BRIDGE_OFFICIAL_ROUTE_ID } from '../bridge-contracts.js';
|
|
5
|
+
import { BRIDGE_OFFICIAL_ROUTE_ID, BRIDGE_REMOTE_COMPACTION } from '../bridge-contracts.js';
|
|
6
6
|
import { buildOfficialPassthroughHeaders, buildProviderUpstreamHeaders, rewriteResponseHeaders } from './header-policy.js';
|
|
7
7
|
import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
|
|
8
|
-
import { OPENROUTER_ONLY_REFUSAL_CODES
|
|
9
|
-
import { ensureDesktopBridgeRemoteTarget, isNoRouteUpstreamError, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader } from './security.js';
|
|
8
|
+
import { OPENROUTER_ONLY_REFUSAL_CODES } from './exclusive-provider-guard.js';
|
|
9
|
+
import { ensureDesktopBridgeRemoteTarget, isNoRouteUpstreamError, isResponsesFamilyPath, isUnreachableUpstreamError, pinnedPublicModel, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader, threadRouteMove } from './security.js';
|
|
10
|
+
import { admitCodexLbTurnState, forgetCodexLbTurnStates, rememberCodexLbTurnState } from './codex-lb-turn-state.js';
|
|
11
|
+
import { ProviderHandoffRefusedError, assertProviderHandoffAllowed, providerHandoffRefusalBody, stripProviderBoundItems, } from './provider-handoff.js';
|
|
10
12
|
import { desktopBridgeListenOrigin } from './state.js';
|
|
11
13
|
import { buildCompactionSummaryRequest, compactEndpointResult, compactionCreatedEvent, compactionFailedEvent, compactionResponseEvents, expandBridgeCompactionItems, newCompactionResponseId, parseProviderSummary, requestsRemoteCompaction, safeUpstreamErrorId, sseEvent, summaryFailure, summaryWithinBounds, } from './compaction-adapter.js';
|
|
12
|
-
import { isAdvisableEventStream, modelCapacityAdvisingSse } from './capacity-retry-advice.js';
|
|
13
|
-
import { DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES, DesktopBridgeError, DesktopBridgeRequestBodyTooLargeError } from './types.js';
|
|
14
|
+
import { MODEL_CAPACITY_ERROR_CODE, MODEL_CAPACITY_RETRY_AFTER_SECONDS, isAdvisableEventStream, modelCapacityAdvisingSse } from './capacity-retry-advice.js';
|
|
15
|
+
import { DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES, DesktopBridgeError, DesktopBridgeRequestBodyTooLargeError, isDesktopBridgeRoutingConflict } from './types.js';
|
|
14
16
|
const MAX_UPSTREAM_ERROR_BODY_BYTES = 1024 * 1024;
|
|
17
|
+
const NATIVE_COMPACTION_IDLE_FLOOR_MS = 300_000;
|
|
15
18
|
function decodeBridgeRequestBody(body, encoding, maximum) {
|
|
16
19
|
const options = { maxOutputLength: maximum };
|
|
17
20
|
try {
|
|
@@ -61,11 +64,10 @@ async function reencodeLikeClient(plain, encoding) {
|
|
|
61
64
|
return new Promise((resolve) => compress(plain, (error, result) => resolve(error ? null : result)));
|
|
62
65
|
}
|
|
63
66
|
const RESPONSES_PATHS = ['/backend-api/codex/responses', '/api/v1/responses', '/v1/responses'];
|
|
64
|
-
function modelBodyKind(req, pathname
|
|
67
|
+
function modelBodyKind(req, pathname) {
|
|
65
68
|
if (RESPONSES_PATHS.includes(pathname))
|
|
66
69
|
return req.method === 'POST' ? 'responses' : null;
|
|
67
|
-
return
|
|
68
|
-
&& RESPONSES_PATHS.some((prefix) => pathname.startsWith(`${prefix}/`)) ? 'sub-endpoint' : null;
|
|
70
|
+
return req.method === 'POST' && RESPONSES_PATHS.some((prefix) => pathname.startsWith(`${prefix}/`)) ? 'sub-endpoint' : null;
|
|
69
71
|
}
|
|
70
72
|
function isCompactEndpoint(req, pathname) {
|
|
71
73
|
return req.method === 'POST' && RESPONSES_PATHS.some((prefix) => pathname === `${prefix}/compact`);
|
|
@@ -105,7 +107,7 @@ export async function prepareDesktopBridgeRequest(req, config) {
|
|
|
105
107
|
let contentEncodingStripped = false;
|
|
106
108
|
let encoding = '';
|
|
107
109
|
let encodedBody = null;
|
|
108
|
-
const bodyKind = modelBodyKind(req, pathname
|
|
110
|
+
const bodyKind = modelBodyKind(req, pathname);
|
|
109
111
|
if (bodyKind) {
|
|
110
112
|
const maximum = config.maxRequestBodyBytes ?? DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES;
|
|
111
113
|
body = await readBoundedBody(req, maximum);
|
|
@@ -131,12 +133,19 @@ export async function prepareDesktopBridgeRequest(req, config) {
|
|
|
131
133
|
body = decoded;
|
|
132
134
|
}
|
|
133
135
|
const headerModel = singleBridgeHeader(req.headers, 'x-sks-model');
|
|
134
|
-
const model = typeof payload?.model === 'string' ? payload.model : headerModel;
|
|
135
136
|
const sessionIdentity = resolveCodexSessionIdentity(req.headers, payload);
|
|
136
|
-
const
|
|
137
|
+
const model = typeof payload?.model === 'string'
|
|
138
|
+
? payload.model
|
|
139
|
+
: headerModel || (isResponsesFamilyPath(pathname) ? pinnedPublicModel(sessionIdentity.thread_id, config) : null);
|
|
140
|
+
const routeRequest = {
|
|
137
141
|
public_model: String(model || ''), session_id: sessionIdentity.thread_id,
|
|
138
142
|
pathname, transport: 'http', headers: req.headers, identity: sessionIdentity,
|
|
139
|
-
}
|
|
143
|
+
};
|
|
144
|
+
let move = null;
|
|
145
|
+
const route = await resolveAndBindDesktopBridgeRouteContext(routeRequest, config, (resolved) => {
|
|
146
|
+
move = threadRouteMove(routeRequest, resolved, config);
|
|
147
|
+
assertProviderHandoffAllowed(payload, move);
|
|
148
|
+
});
|
|
140
149
|
let rewrite = false;
|
|
141
150
|
if (payload && (bodyKind === 'responses' || typeof payload.model === 'string') && payload.model !== route.upstream_model) {
|
|
142
151
|
payload.model = route.upstream_model;
|
|
@@ -144,14 +153,15 @@ export async function prepareDesktopBridgeRequest(req, config) {
|
|
|
144
153
|
}
|
|
145
154
|
if (expandBridgeCompactionItems(payload))
|
|
146
155
|
rewrite = true;
|
|
156
|
+
if (route.session_pin?.handoff_from && stripProviderBoundItems(payload, Boolean(move)))
|
|
157
|
+
rewrite = true;
|
|
147
158
|
if (payload && rewrite)
|
|
148
159
|
body = Buffer.from(JSON.stringify(payload));
|
|
149
160
|
const compaction = bodyKind === 'responses' && requestsRemoteCompaction(payload)
|
|
150
161
|
? 'trigger'
|
|
151
162
|
: bodyKind === 'sub-endpoint' && isCompactEndpoint(req, pathname) ? 'endpoint' : null;
|
|
152
163
|
let plainBody = null;
|
|
153
|
-
|
|
154
|
-
if (encodedBody && body && !answeredByBridge && keepsClientEncoding(route)) {
|
|
164
|
+
if (encodedBody && body && !answeredByBridge(compaction, route) && keepsClientEncoding(route)) {
|
|
155
165
|
const upstreamBody = rewrite ? await reencodeLikeClient(body, encoding) : encodedBody;
|
|
156
166
|
if (upstreamBody) {
|
|
157
167
|
plainBody = body;
|
|
@@ -160,7 +170,10 @@ export async function prepareDesktopBridgeRequest(req, config) {
|
|
|
160
170
|
}
|
|
161
171
|
}
|
|
162
172
|
const credential = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID ? null : await resolveCredential(config, route);
|
|
163
|
-
return { body, route, credential, contentEncodingStripped, plainBody, compaction, payload };
|
|
173
|
+
return { body, route, credential, contentEncodingStripped, plainBody, compaction, payload, threadId: sessionIdentity.thread_id, move };
|
|
174
|
+
}
|
|
175
|
+
function answeredByBridge(compaction, route) {
|
|
176
|
+
return Boolean(compaction) && BRIDGE_REMOTE_COMPACTION[route.provider_id] === 'bridge_summary';
|
|
164
177
|
}
|
|
165
178
|
function connectTimeout(request, config) {
|
|
166
179
|
request.once('socket', (socket) => {
|
|
@@ -191,8 +204,12 @@ function writeHttpBridgeError(res, error, req) {
|
|
|
191
204
|
res.destroy(error instanceof Error ? error : undefined);
|
|
192
205
|
return;
|
|
193
206
|
}
|
|
194
|
-
|
|
195
|
-
|
|
207
|
+
if (error instanceof ProviderHandoffRefusedError) {
|
|
208
|
+
res.writeHead(400, { 'content-type': 'application/json', 'cache-control': 'no-store', connection: 'close' });
|
|
209
|
+
res.end(providerHandoffRefusalBody(error));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
res.writeHead(isDesktopBridgeRoutingConflict(code) || OPENROUTER_ONLY_REFUSAL_CODES.has(code) ? 409 : 502, {
|
|
196
213
|
'content-type': 'application/json', 'cache-control': 'no-store', connection: 'close',
|
|
197
214
|
});
|
|
198
215
|
res.end(JSON.stringify({ error: { type: 'sks_bridge_error', code, message: code } }));
|
|
@@ -229,19 +246,32 @@ function redactedUpstreamClientMessage(statusCode, transient) {
|
|
|
229
246
|
const UNREADABLE_BODY_HINT = /json|pars(?:e|ing)|decod|encod|compress|utf-?8/i;
|
|
230
247
|
async function readRedactedUpstreamError(response) {
|
|
231
248
|
let total = 0;
|
|
249
|
+
let complete = true;
|
|
232
250
|
const chunks = [];
|
|
233
251
|
for await (const raw of response) {
|
|
234
252
|
const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
235
253
|
total += chunk.length;
|
|
236
254
|
if (total > MAX_UPSTREAM_ERROR_BODY_BYTES) {
|
|
237
255
|
response.destroy();
|
|
256
|
+
complete = false;
|
|
238
257
|
break;
|
|
239
258
|
}
|
|
240
259
|
chunks.push(chunk);
|
|
241
260
|
}
|
|
242
261
|
let upstreamCode = null;
|
|
243
262
|
let upstreamType = null;
|
|
244
|
-
const
|
|
263
|
+
const raw = Buffer.concat(chunks);
|
|
264
|
+
const encoding = String(response.headers['content-encoding'] || '').trim().toLowerCase();
|
|
265
|
+
let decoded = complete ? raw : null;
|
|
266
|
+
if (decoded && encoding && encoding !== 'identity') {
|
|
267
|
+
try {
|
|
268
|
+
decoded = decodeBridgeRequestBody(decoded, encoding, 4 * MAX_UPSTREAM_ERROR_BODY_BYTES);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
decoded = null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const text = (decoded ?? raw).toString('utf8');
|
|
245
275
|
try {
|
|
246
276
|
const parsed = JSON.parse(text);
|
|
247
277
|
upstreamCode = safeUpstreamErrorId(parsed?.error?.code) ?? safeUpstreamErrorId(parsed?.detail);
|
|
@@ -249,7 +279,21 @@ async function readRedactedUpstreamError(response) {
|
|
|
249
279
|
}
|
|
250
280
|
catch {
|
|
251
281
|
}
|
|
252
|
-
return {
|
|
282
|
+
return {
|
|
283
|
+
upstreamCode,
|
|
284
|
+
upstreamType,
|
|
285
|
+
unreadableBody: UNREADABLE_BODY_HINT.test(text.slice(0, 4096)),
|
|
286
|
+
body: decoded,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function scrubUpstreamErrorBody(body, credential) {
|
|
290
|
+
let text = body.toString('utf8');
|
|
291
|
+
if (credential)
|
|
292
|
+
text = text.split(credential).join('[REDACTED]');
|
|
293
|
+
text = text
|
|
294
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/-]{8,}=*/gi, 'Bearer [REDACTED]')
|
|
295
|
+
.replace(/\bsk-[A-Za-z0-9_-]{16,}/g, '[REDACTED]');
|
|
296
|
+
return Buffer.from(text, 'utf8');
|
|
253
297
|
}
|
|
254
298
|
function buildRedactedUpstreamErrorBody(statusCode, upstreamCode, upstreamType, transient) {
|
|
255
299
|
return Buffer.from(JSON.stringify({
|
|
@@ -284,6 +328,7 @@ export function destroyDesktopBridgeUpstreamAgents() {
|
|
|
284
328
|
agent.destroy();
|
|
285
329
|
upstreamAgents.clear();
|
|
286
330
|
encodedBodyRefusals.clear();
|
|
331
|
+
forgetCodexLbTurnStates();
|
|
287
332
|
}
|
|
288
333
|
const STALE_CONNECTION_ERROR_CODES = new Set(['ECONNRESET', 'EPIPE', 'ECONNABORTED']);
|
|
289
334
|
function isStalePooledSocketFailure(request, error) {
|
|
@@ -319,7 +364,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
319
364
|
if (!official && !request.credential)
|
|
320
365
|
throw new DesktopBridgeError('bridge_provider_credential_invalid');
|
|
321
366
|
await ensureDesktopBridgeRemoteTarget(remote, config.remoteLookup);
|
|
322
|
-
if (!official && request.compaction && request.payload) {
|
|
367
|
+
if (!official && request.compaction && answeredByBridge(request.compaction, request.route) && request.payload) {
|
|
323
368
|
await answerCompactionWithProviderSummary(req, res, config, {
|
|
324
369
|
kind: request.compaction,
|
|
325
370
|
payload: request.payload,
|
|
@@ -341,6 +386,12 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
341
386
|
: buildProviderUpstreamHeaders(req.headers, {
|
|
342
387
|
providerId: provider.provider_id, authTransport: provider.auth_transport, credential: request.credential,
|
|
343
388
|
}, target.host);
|
|
389
|
+
const codexLb = provider?.provider_id === 'codex-lb';
|
|
390
|
+
const threadId = request.threadId ?? null;
|
|
391
|
+
if (request.move)
|
|
392
|
+
delete headers['x-codex-turn-state'];
|
|
393
|
+
else if (codexLb)
|
|
394
|
+
admitCodexLbTurnState(headers, threadId, request.credential.fingerprint);
|
|
344
395
|
if (request.body)
|
|
345
396
|
headers['content-length'] = String(request.body.length);
|
|
346
397
|
else
|
|
@@ -348,6 +399,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
348
399
|
if (request.contentEncodingStripped)
|
|
349
400
|
delete headers['content-encoding'];
|
|
350
401
|
const agent = upstreamAgent(remote.secure, `${remote.address}:${remote.port}`, config.idleTimeoutMs);
|
|
402
|
+
const idleTimeoutMs = request.compaction ? Math.max(config.idleTimeoutMs, NATIVE_COMPACTION_IDLE_FLOOR_MS) : config.idleTimeoutMs;
|
|
351
403
|
const replayable = Buffer.isBuffer(request.body);
|
|
352
404
|
let body = request.body;
|
|
353
405
|
let plainFallbackActive = false;
|
|
@@ -371,7 +423,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
371
423
|
...(remote.tlsServername ? { servername: remote.tlsServername } : {}),
|
|
372
424
|
});
|
|
373
425
|
connectTimeout(upstream, config);
|
|
374
|
-
upstream.setTimeout(
|
|
426
|
+
upstream.setTimeout(idleTimeoutMs, () => upstream.destroy(new DesktopBridgeError('bridge_upstream_idle_timeout')));
|
|
375
427
|
req.once('aborted', abort);
|
|
376
428
|
res.once('close', abort);
|
|
377
429
|
upstream.once('error', (error) => {
|
|
@@ -390,6 +442,8 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
390
442
|
});
|
|
391
443
|
upstream.once('response', (response) => {
|
|
392
444
|
const statusCode = response.statusCode || 502;
|
|
445
|
+
if (codexLb)
|
|
446
|
+
rememberCodexLbTurnState(threadId, request.credential.fingerprint, response.headers);
|
|
393
447
|
if (plainFallbackActive && statusCode < 400 && !official && !encodedBodyRefusals.has(provider.provider_id)) {
|
|
394
448
|
encodedBodyRefusals.add(provider.provider_id);
|
|
395
449
|
logHttpRejection({ code: 'bridge_upstream_encoded_body_refused', transport: 'http', ...(req.method === undefined ? {} : { method: req.method }), ...(req.url === undefined ? {} : { url: req.url }), provider_id: provider.provider_id });
|
|
@@ -425,7 +479,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
425
479
|
return;
|
|
426
480
|
}
|
|
427
481
|
if (statusCode >= 400) {
|
|
428
|
-
void readRedactedUpstreamError(response).then(({ upstreamCode, upstreamType, unreadableBody }) => {
|
|
482
|
+
void readRedactedUpstreamError(response).then(({ upstreamCode, upstreamType, unreadableBody, body: upstreamBody }) => {
|
|
429
483
|
if (budget.plainFallback && (statusCode === 415
|
|
430
484
|
|| (unreadableBody && !upstreamType && !upstreamCode && (statusCode === 400 || statusCode === 422)))) {
|
|
431
485
|
responseStarted = true;
|
|
@@ -440,10 +494,13 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
440
494
|
: 'bridge_upstream_transient_mislabel')));
|
|
441
495
|
return;
|
|
442
496
|
}
|
|
443
|
-
const
|
|
444
|
-
const
|
|
497
|
+
const verbatim = codexLb && upstreamBody !== null;
|
|
498
|
+
const clientStatus = verbatim ? statusCode : transientMislabel ? 503 : statusCode;
|
|
499
|
+
const body = verbatim
|
|
500
|
+
? scrubUpstreamErrorBody(upstreamBody, request.credential.value)
|
|
501
|
+
: buildRedactedUpstreamErrorBody(statusCode, upstreamCode, upstreamType, transientMislabel);
|
|
445
502
|
logHttpRejection({
|
|
446
|
-
code: transientMislabel
|
|
503
|
+
code: transientMislabel && !verbatim
|
|
447
504
|
? `bridge_upstream_status_${statusCode}_translated_503`
|
|
448
505
|
: upstreamCode ? `bridge_upstream_status_${statusCode}:${upstreamCode}` : `bridge_upstream_status_${statusCode}`,
|
|
449
506
|
transport: 'http',
|
|
@@ -457,10 +514,10 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
457
514
|
const responseHeaders = rewriteResponseHeaders(response.headers, upstreamBaseUrl, authenticatedLocalBaseUrl);
|
|
458
515
|
responseHeaders['content-length'] = String(body.length);
|
|
459
516
|
delete responseHeaders['transfer-encoding'];
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
517
|
+
delete responseHeaders['content-encoding'];
|
|
518
|
+
if (verbatim && upstreamCode === MODEL_CAPACITY_ERROR_CODE && responseHeaders['retry-after'] === undefined) {
|
|
519
|
+
responseHeaders['retry-after'] = String(MODEL_CAPACITY_RETRY_AFTER_SECONDS);
|
|
520
|
+
}
|
|
464
521
|
res.writeHead(clientStatus, responseHeaders);
|
|
465
522
|
res.end(body, () => finish());
|
|
466
523
|
}).catch(finish);
|
|
@@ -638,6 +695,10 @@ export async function requestProviderCompactionSummary(config, input, body, onAc
|
|
|
638
695
|
}
|
|
639
696
|
}
|
|
640
697
|
}
|
|
698
|
+
function singleRetryAfter(value) {
|
|
699
|
+
const text = String(Array.isArray(value) ? value[0] ?? '' : value ?? '').trim();
|
|
700
|
+
return /^[A-Za-z0-9 ,:.-]{1,64}$/.test(text) ? text : null;
|
|
701
|
+
}
|
|
641
702
|
function upstreamErrorIds(body) {
|
|
642
703
|
try {
|
|
643
704
|
const parsed = JSON.parse(body.toString('utf8'));
|
|
@@ -683,9 +744,10 @@ async function answerCompactionWithProviderSummary(req, res, config, input) {
|
|
|
683
744
|
const transient = isTransientUpstreamFailure(call.status, upstreamType, upstreamCode);
|
|
684
745
|
const clientStatus = transient ? 503 : call.status;
|
|
685
746
|
const errorBody = buildRedactedUpstreamErrorBody(call.status, upstreamCode, upstreamType, transient);
|
|
747
|
+
const retryAfter = singleRetryAfter(call.headers['retry-after']);
|
|
686
748
|
res.writeHead(clientStatus, {
|
|
687
749
|
'content-type': 'application/json', 'cache-control': 'no-store', 'content-length': String(errorBody.length),
|
|
688
|
-
...(
|
|
750
|
+
...(retryAfter ? { 'retry-after': retryAfter } : {}),
|
|
689
751
|
});
|
|
690
752
|
res.end(errorBody);
|
|
691
753
|
return;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { DesktopBridgeError } from './types.js';
|
|
2
|
+
import { isNativeCompactionItem } from './compaction-adapter.js';
|
|
3
|
+
export const PROVIDER_HANDOFF_NATIVE_COMPACTION_REFUSED = 'bridge_thread_handoff_after_native_compaction';
|
|
4
|
+
const ROUTE_TARGET_NAMES = {
|
|
5
|
+
openai: 'your ChatGPT account',
|
|
6
|
+
'codex-lb': 'Codex-LB',
|
|
7
|
+
openrouter: 'OpenRouter',
|
|
8
|
+
};
|
|
9
|
+
function isObject(value) {
|
|
10
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function targetName(id) {
|
|
13
|
+
return ROUTE_TARGET_NAMES[id] ?? id;
|
|
14
|
+
}
|
|
15
|
+
export class ProviderHandoffRefusedError extends DesktopBridgeError {
|
|
16
|
+
move;
|
|
17
|
+
constructor(move) {
|
|
18
|
+
super(PROVIDER_HANDOFF_NATIVE_COMPACTION_REFUSED);
|
|
19
|
+
this.move = move;
|
|
20
|
+
}
|
|
21
|
+
userMessage() {
|
|
22
|
+
return `This thread was compacted on ${targetName(this.move.from)}, and that compaction can only be read there. `
|
|
23
|
+
+ `Switch back to the model you were using, or start a new thread to continue on ${targetName(this.move.to)}.`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function carriesNativeCompaction(payload) {
|
|
27
|
+
return Boolean(payload && Array.isArray(payload.input) && payload.input.some(isNativeCompactionItem));
|
|
28
|
+
}
|
|
29
|
+
export function assertProviderHandoffAllowed(payload, move) {
|
|
30
|
+
if (!move || move.from === move.to || !carriesNativeCompaction(payload))
|
|
31
|
+
return;
|
|
32
|
+
throw new ProviderHandoffRefusedError(move);
|
|
33
|
+
}
|
|
34
|
+
export function providerHandoffRefusalBody(error) {
|
|
35
|
+
return JSON.stringify({
|
|
36
|
+
error: { type: 'invalid_request_error', code: 'invalid_prompt', message: error.userMessage(), param: error.code },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function providerHandoffRefusedEvent(error, responseId, publicModel) {
|
|
40
|
+
return {
|
|
41
|
+
type: 'response.failed',
|
|
42
|
+
response: {
|
|
43
|
+
id: responseId,
|
|
44
|
+
object: 'response',
|
|
45
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
46
|
+
model: publicModel,
|
|
47
|
+
status: 'failed',
|
|
48
|
+
error: { code: 'invalid_prompt', message: error.userMessage() },
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function stripProviderBoundItems(payload, moving) {
|
|
53
|
+
if (!payload)
|
|
54
|
+
return false;
|
|
55
|
+
let changed = false;
|
|
56
|
+
if (Array.isArray(payload.input)) {
|
|
57
|
+
const input = [];
|
|
58
|
+
for (const item of payload.input) {
|
|
59
|
+
if (isObject(item) && item.type === 'reasoning') {
|
|
60
|
+
changed = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (isObject(item) && 'encrypted_content' in item && !isNativeCompactionItem(item)) {
|
|
64
|
+
const { encrypted_content: _foreign, ...rest } = item;
|
|
65
|
+
input.push(rest);
|
|
66
|
+
changed = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
input.push(item);
|
|
70
|
+
}
|
|
71
|
+
if (changed)
|
|
72
|
+
payload.input = input;
|
|
73
|
+
}
|
|
74
|
+
if (moving && payload.previous_response_id !== undefined) {
|
|
75
|
+
delete payload.previous_response_id;
|
|
76
|
+
changed = true;
|
|
77
|
+
}
|
|
78
|
+
return changed;
|
|
79
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import net from 'node:net';
|
|
2
2
|
import tls from 'node:tls';
|
|
3
3
|
import WebSocket, { WebSocketServer } from 'ws';
|
|
4
|
-
import { BRIDGE_OFFICIAL_ROUTE_ID } from '../bridge-contracts.js';
|
|
4
|
+
import { BRIDGE_OFFICIAL_ROUTE_ID, BRIDGE_REMOTE_COMPACTION } from '../bridge-contracts.js';
|
|
5
5
|
import { buildOfficialPassthroughWebSocketHeaders, buildProviderWebSocketHeaders } from './header-policy.js';
|
|
6
6
|
import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
|
|
7
|
-
import { assertDesktopBridgeRouteContext, ensureDesktopBridgeRemoteTarget, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode } from './security.js';
|
|
7
|
+
import { assertDesktopBridgeRouteContext, ensureDesktopBridgeRemoteTarget, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, threadRouteMove } from './security.js';
|
|
8
|
+
import { admitCodexLbTurnState } from './codex-lb-turn-state.js';
|
|
9
|
+
import { ProviderHandoffRefusedError, assertProviderHandoffAllowed, providerHandoffRefusedEvent, stripProviderBoundItems, } from './provider-handoff.js';
|
|
8
10
|
import { DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES, DesktopBridgeError } from './types.js';
|
|
9
11
|
import { buildCompactionSummaryRequest, compactionCreatedEvent, compactionFailedEvent, compactionResponseEvents, expandBridgeCompactionItems, newCompactionResponseId, parseProviderSummary, requestsRemoteCompaction, ResponsesContinuationChain, summaryFailure, summaryWithinBounds, } from './compaction-adapter.js';
|
|
10
12
|
import { requestProviderCompactionSummary } from './http-forward.js';
|
|
@@ -45,6 +47,7 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
45
47
|
let upstream = null;
|
|
46
48
|
let upstreamSocket = null;
|
|
47
49
|
let bound = null;
|
|
50
|
+
let boundByMove = false;
|
|
48
51
|
let boundBaseUrl = null;
|
|
49
52
|
let boundCredentialGeneration = null;
|
|
50
53
|
let boundCredentialFingerprint = null;
|
|
@@ -126,26 +129,44 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
126
129
|
}
|
|
127
130
|
identity = bound ? withLineage(identity, nextIdentity) : nextIdentity;
|
|
128
131
|
const request = routeRequest(model);
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
const planned = assertDesktopBridgeRouteContext(request, config);
|
|
133
|
+
assertProviderHandoffAllowed(message.create, threadRouteMove(request, planned, config));
|
|
134
|
+
assertBinding(planned);
|
|
135
|
+
let move = null;
|
|
136
|
+
const route = await resolveAndBindDesktopBridgeRouteContext(request, config, (resolved) => {
|
|
137
|
+
move = threadRouteMove(request, resolved, config);
|
|
138
|
+
assertProviderHandoffAllowed(message.create, move);
|
|
139
|
+
});
|
|
131
140
|
if (stopped || client.readyState !== WebSocket.OPEN)
|
|
132
141
|
return;
|
|
133
142
|
assertBinding(route);
|
|
134
143
|
routedModel = route.public_model;
|
|
135
144
|
if (!bound) {
|
|
136
145
|
bound = route;
|
|
146
|
+
boundByMove = Boolean(move);
|
|
137
147
|
await connect(route);
|
|
138
148
|
}
|
|
139
149
|
const create = { ...message.create, model: route.upstream_model };
|
|
140
|
-
|
|
141
|
-
if (route.
|
|
150
|
+
let rewritten = expandBridgeCompactionItems(create);
|
|
151
|
+
if (route.session_pin?.handoff_from && stripProviderBoundItems(create, Boolean(move)))
|
|
152
|
+
rewritten = true;
|
|
153
|
+
if (BRIDGE_REMOTE_COMPACTION[route.provider_id] === 'bridge_summary' && requestsRemoteCompaction(create)) {
|
|
142
154
|
message.compaction = create;
|
|
143
155
|
return;
|
|
144
156
|
}
|
|
145
|
-
if (route.upstream_model !== model ||
|
|
157
|
+
if (route.upstream_model !== model || rewritten)
|
|
146
158
|
message.data = Buffer.from(JSON.stringify(create));
|
|
147
159
|
message.routed = create;
|
|
148
160
|
}
|
|
161
|
+
function refuseMove(message, error) {
|
|
162
|
+
const model = typeof message.create?.model === 'string' ? message.create.model : '';
|
|
163
|
+
logRejection({ code: error.code, transport: 'websocket', ...(req.url ? { url: req.url } : {}) });
|
|
164
|
+
relay(client, Buffer.from(JSON.stringify(providerHandoffRefusedEvent(error, newCompactionResponseId(), model))), false);
|
|
165
|
+
const index = pending.indexOf(message);
|
|
166
|
+
if (index >= 0)
|
|
167
|
+
pending.splice(index, 1);
|
|
168
|
+
pendingBytes -= message.originalBytes;
|
|
169
|
+
}
|
|
149
170
|
async function answerCompaction(create) {
|
|
150
171
|
const route = bound;
|
|
151
172
|
const provider = config.providers[route.provider_id];
|
|
@@ -192,6 +213,7 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
192
213
|
}
|
|
193
214
|
async function connect(route) {
|
|
194
215
|
const official = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID;
|
|
216
|
+
const summarizes = BRIDGE_REMOTE_COMPACTION[route.provider_id] === 'bridge_summary';
|
|
195
217
|
const provider = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID ? null : config.providers[route.provider_id];
|
|
196
218
|
const remote = official ? config.officialRemote : provider?.remote;
|
|
197
219
|
if (!remote)
|
|
@@ -209,6 +231,10 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
209
231
|
const target = resolveDesktopBridgeTarget(req.url, remote);
|
|
210
232
|
const outgoing = official ? buildOfficialPassthroughWebSocketHeaders(req.headers, target.host)
|
|
211
233
|
: buildProviderWebSocketHeaders(req.headers, { providerId: provider.provider_id, authTransport: provider.auth_transport, credential: credential }, target.host);
|
|
234
|
+
if (boundByMove)
|
|
235
|
+
delete outgoing['x-codex-turn-state'];
|
|
236
|
+
else if (provider?.provider_id === 'codex-lb')
|
|
237
|
+
admitCodexLbTurnState(outgoing, identity.thread_id, credential.fingerprint);
|
|
212
238
|
const headers = {};
|
|
213
239
|
for (const [name, value] of Object.entries(outgoing)) {
|
|
214
240
|
if (name.startsWith('sec-websocket-') || name === 'connection' || name === 'upgrade' || name === 'content-length')
|
|
@@ -256,7 +282,8 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
256
282
|
frame = advised;
|
|
257
283
|
logRejection({ code: 'bridge_upstream_model_capacity_retry_advised', transport: 'websocket', provider_id: route.provider_id, public_model: routedModel ?? route.public_model, ...(req.url ? { url: req.url } : {}) });
|
|
258
284
|
}
|
|
259
|
-
|
|
285
|
+
if (summarizes)
|
|
286
|
+
chain.received(frame);
|
|
260
287
|
try {
|
|
261
288
|
relay(client, frame, binary);
|
|
262
289
|
}
|
|
@@ -289,24 +316,41 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
289
316
|
return;
|
|
290
317
|
processing = true;
|
|
291
318
|
try {
|
|
292
|
-
|
|
319
|
+
while (!bound) {
|
|
293
320
|
const firstCreate = pending.find((message) => message.create);
|
|
294
321
|
if (!firstCreate)
|
|
295
322
|
return;
|
|
296
|
-
|
|
323
|
+
try {
|
|
324
|
+
await resolveCreate(firstCreate);
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
if (!(error instanceof ProviderHandoffRefusedError))
|
|
328
|
+
throw error;
|
|
329
|
+
refuseMove(firstCreate, error);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
297
332
|
firstCreate.create = null;
|
|
298
333
|
}
|
|
299
334
|
while (!stopped && client.readyState === WebSocket.OPEN && pending.length) {
|
|
300
335
|
const message = pending[0];
|
|
301
|
-
if (message.create)
|
|
302
|
-
|
|
336
|
+
if (message.create) {
|
|
337
|
+
try {
|
|
338
|
+
await resolveCreate(message);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
if (!(error instanceof ProviderHandoffRefusedError))
|
|
342
|
+
throw error;
|
|
343
|
+
refuseMove(message, error);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
303
347
|
if (stopped || client.readyState !== WebSocket.OPEN)
|
|
304
348
|
return;
|
|
305
349
|
if (message.compaction)
|
|
306
350
|
await answerCompaction(message.compaction);
|
|
307
351
|
else {
|
|
308
352
|
relay(upstream, message.data, message.binary);
|
|
309
|
-
if (message.routed && bound
|
|
353
|
+
if (message.routed && bound && BRIDGE_REMOTE_COMPACTION[bound.provider_id] === 'bridge_summary')
|
|
310
354
|
chain.sent(message.routed, message.data);
|
|
311
355
|
}
|
|
312
356
|
pending.shift();
|