sneakoscope 10.7.0 → 10.7.2
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/command-manifest-lite.js +1 -1
- package/dist/cli/install-helpers-codex-lb-config.js +55 -20
- package/dist/commands/bridge.js +14 -1
- package/dist/commands/codex.js +5 -1
- package/dist/commands/doctor-console.js +2 -0
- package/dist/commands/doctor.js +8 -9
- 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-app/menubar/constants.js +0 -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/combined-catalog/persistence.js +4 -0
- package/dist/core/codex-lb/combined-catalog/storage.js +4 -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 +138 -36
- package/dist/core/codex-lb/desktop-bridge/models-endpoint.js +62 -0
- package/dist/core/codex-lb/desktop-bridge/provider-handoff.js +88 -0
- package/dist/core/codex-lb/desktop-bridge/provider-request-compat.js +89 -0
- package/dist/core/codex-lb/desktop-bridge/rejection-log.js +61 -0
- package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +108 -16
- package/dist/core/codex-lb/desktop-bridge/security.js +101 -55
- package/dist/core/codex-lb/desktop-bridge/server.js +15 -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-bridge-migration/historical-config.js +2 -6
- package/dist/core/codex-lb/desktop-bridge-migration.js +14 -2
- package/dist/core/codex-lb/desktop-controller-v3/catalog.js +125 -3
- package/dist/core/codex-lb/desktop-controller-v3/model-refresh.js +140 -0
- package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +5 -5
- package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +14 -11
- package/dist/core/codex-lb/desktop-controller-v3/status.js +2 -3
- package/dist/core/codex-lb/desktop-controller-v3.js +3 -1
- package/dist/core/codex-lb/desktop-service.js +51 -17
- package/dist/core/codex-lb/model-refresh-after.js +16 -0
- 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/commands/basic-cli.js +5 -0
- package/dist/core/commands/menubar-command.js +5 -1
- package/dist/core/doctor/desktop-bridge-catalog-repair.js +20 -17
- 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/subagents/model-tiers.js +19 -3
- package/dist/core/update/update-migration-state/desktop-bridge-catalog-repair-stage.js +4 -3
- package/dist/core/update/update-migration-state.js +10 -40
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +0 -1
- package/dist/native/sks-menubar/Sources/OverviewSummary.swift +32 -0
- package/dist/native/sks-menubar/Sources/OverviewViewController.swift +15 -5
- package/dist/native/sks-menubar/Sources/SidebarItem.swift +0 -2
- package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +3 -0
- package/package.json +2 -2
- package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +0 -49
|
@@ -0,0 +1,88 @@
|
|
|
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)) {
|
|
64
|
+
let next = item;
|
|
65
|
+
if ('encrypted_content' in next && !isNativeCompactionItem(next)) {
|
|
66
|
+
const { encrypted_content: _foreign, ...rest } = next;
|
|
67
|
+
next = rest;
|
|
68
|
+
changed = true;
|
|
69
|
+
}
|
|
70
|
+
if (next.type === 'function_call' && 'encrypted_function_args' in next) {
|
|
71
|
+
const { encrypted_function_args: _foreign, ...rest } = next;
|
|
72
|
+
next = rest;
|
|
73
|
+
changed = true;
|
|
74
|
+
}
|
|
75
|
+
input.push(next);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
input.push(item);
|
|
79
|
+
}
|
|
80
|
+
if (changed)
|
|
81
|
+
payload.input = input;
|
|
82
|
+
}
|
|
83
|
+
if (moving && payload.previous_response_id !== undefined) {
|
|
84
|
+
delete payload.previous_response_id;
|
|
85
|
+
changed = true;
|
|
86
|
+
}
|
|
87
|
+
return changed;
|
|
88
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { BRIDGE_OFFICIAL_ROUTE_ID } from '../bridge-contracts.js';
|
|
2
|
+
import { PACKAGE_VERSION } from '../../version.js';
|
|
3
|
+
import { DESKTOP_BRIDGE_LOG_SCHEMA, REJECTION_LOG_SUMMARY_INTERVAL_MS } from './rejection-log.js';
|
|
4
|
+
export const PROVIDER_INCLUDE_ALLOWLIST = {
|
|
5
|
+
[BRIDGE_OFFICIAL_ROUTE_ID]: null,
|
|
6
|
+
'codex-lb': [
|
|
7
|
+
'code_interpreter_call.outputs',
|
|
8
|
+
'computer_call_output.output.image_url',
|
|
9
|
+
'file_search_call.results',
|
|
10
|
+
'message.input_image.image_url',
|
|
11
|
+
'message.output_text.logprobs',
|
|
12
|
+
'reasoning.encrypted_content',
|
|
13
|
+
'web_search_call.action.sources',
|
|
14
|
+
],
|
|
15
|
+
openrouter: [
|
|
16
|
+
'code_interpreter_call.outputs',
|
|
17
|
+
'computer_call_output.output.image_url',
|
|
18
|
+
'file_search_call.results',
|
|
19
|
+
'message.input_image.image_url',
|
|
20
|
+
'reasoning.encrypted_content',
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
export const REQUEST_COMPAT_INCLUDE_VALUE = /^[a-z_.]{1,64}$/;
|
|
24
|
+
export const REQUEST_COMPAT_MAX_EXTRA_INCLUDE = 8;
|
|
25
|
+
export function adaptRequestForRoute(payload, route, extraInclude = []) {
|
|
26
|
+
if (!payload || route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID)
|
|
27
|
+
return [];
|
|
28
|
+
const changes = [];
|
|
29
|
+
const allowlist = PROVIDER_INCLUDE_ALLOWLIST[route.provider_id];
|
|
30
|
+
if (allowlist && Array.isArray(payload.include)) {
|
|
31
|
+
const allowed = new Set([...allowlist, ...extraInclude]);
|
|
32
|
+
const kept = [];
|
|
33
|
+
for (const value of payload.include) {
|
|
34
|
+
if (typeof value === 'string' && allowed.has(value))
|
|
35
|
+
kept.push(value);
|
|
36
|
+
else
|
|
37
|
+
changes.push({ field: 'include', value: String(value) });
|
|
38
|
+
}
|
|
39
|
+
if (kept.length !== payload.include.length)
|
|
40
|
+
payload.include = kept;
|
|
41
|
+
}
|
|
42
|
+
return changes;
|
|
43
|
+
}
|
|
44
|
+
export function providerExtraInclude(config, route) {
|
|
45
|
+
if (route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID)
|
|
46
|
+
return [];
|
|
47
|
+
const values = config.providers[route.provider_id]?.request_compat?.extra_include;
|
|
48
|
+
return Array.isArray(values) ? values.filter((value) => REQUEST_COMPAT_INCLUDE_VALUE.test(value)) : [];
|
|
49
|
+
}
|
|
50
|
+
const adaptationCounts = new WeakMap();
|
|
51
|
+
function loggedValue(value) {
|
|
52
|
+
return /^[A-Za-z0-9_.:-]{1,64}$/.test(value) ? value : '<other>';
|
|
53
|
+
}
|
|
54
|
+
export function logRequestAdaptation(config, route, changes, transport, options = {}) {
|
|
55
|
+
const write = options.write ?? ((line) => process.stdout.write(line));
|
|
56
|
+
const now = options.now?.() ?? Date.now();
|
|
57
|
+
let counts = adaptationCounts.get(config);
|
|
58
|
+
if (!counts) {
|
|
59
|
+
counts = new Map();
|
|
60
|
+
adaptationCounts.set(config, counts);
|
|
61
|
+
}
|
|
62
|
+
for (const change of changes) {
|
|
63
|
+
const value = loggedValue(change.value);
|
|
64
|
+
const key = `${route.provider_id}\0${change.field}\0${value}`;
|
|
65
|
+
const entry = counts.get(key) ?? { count: 0, loggedAtMs: Number.NEGATIVE_INFINITY };
|
|
66
|
+
entry.count += 1;
|
|
67
|
+
counts.set(key, entry);
|
|
68
|
+
if (entry.count > 1 && now - entry.loggedAtMs < REJECTION_LOG_SUMMARY_INTERVAL_MS)
|
|
69
|
+
continue;
|
|
70
|
+
entry.loggedAtMs = now;
|
|
71
|
+
try {
|
|
72
|
+
write(`${JSON.stringify({
|
|
73
|
+
schema: DESKTOP_BRIDGE_LOG_SCHEMA,
|
|
74
|
+
sks_version: PACKAGE_VERSION,
|
|
75
|
+
at: new Date(now).toISOString(),
|
|
76
|
+
secret_fields_redacted: true,
|
|
77
|
+
event: 'sks.desktop_bridge.request_adapted',
|
|
78
|
+
code: 'bridge_request_include_dropped',
|
|
79
|
+
transport,
|
|
80
|
+
provider_id: route.provider_id,
|
|
81
|
+
field: change.field,
|
|
82
|
+
value,
|
|
83
|
+
count: entry.count,
|
|
84
|
+
})}\n`);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -2,6 +2,58 @@ import { PACKAGE_VERSION } from '../../version.js';
|
|
|
2
2
|
export const DESKTOP_BRIDGE_LOG_SCHEMA = 'sks.desktop-bridge-log.v2';
|
|
3
3
|
export const REJECTION_LOG_BURST = 5;
|
|
4
4
|
export const REJECTION_LOG_SUMMARY_INTERVAL_MS = 60_000;
|
|
5
|
+
const SHAPE_TOKEN = /^[A-Za-z0-9_.[\]-]{1,64}$/;
|
|
6
|
+
const MAX_SHAPE_KEYS = 64;
|
|
7
|
+
const MAX_SHAPE_INCLUDE = 16;
|
|
8
|
+
const MAX_SHAPE_INPUT_TYPES = 32;
|
|
9
|
+
function shapeToken(value) {
|
|
10
|
+
return typeof value === 'string' && SHAPE_TOKEN.test(value) ? value : '<other>';
|
|
11
|
+
}
|
|
12
|
+
export function desktopBridgeRequestShape(payload) {
|
|
13
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
14
|
+
return null;
|
|
15
|
+
const request = payload;
|
|
16
|
+
const inputTypes = {};
|
|
17
|
+
if (Array.isArray(request.input)) {
|
|
18
|
+
for (const item of request.input) {
|
|
19
|
+
const type = item && typeof item === 'object' && !Array.isArray(item) ? shapeToken(item.type) : '<other>';
|
|
20
|
+
if (inputTypes[type] === undefined && Object.keys(inputTypes).length >= MAX_SHAPE_INPUT_TYPES)
|
|
21
|
+
continue;
|
|
22
|
+
inputTypes[type] = (inputTypes[type] ?? 0) + 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else if (typeof request.input === 'string')
|
|
26
|
+
inputTypes['<text>'] = 1;
|
|
27
|
+
return {
|
|
28
|
+
keys: Object.keys(request).map(shapeToken).sort().slice(0, MAX_SHAPE_KEYS),
|
|
29
|
+
include: Array.isArray(request.include) ? request.include.slice(0, MAX_SHAPE_INCLUDE).map(shapeToken) : null,
|
|
30
|
+
input_types: inputTypes,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function safeRequestShape(shape) {
|
|
34
|
+
if (!shape || !Array.isArray(shape.keys))
|
|
35
|
+
return null;
|
|
36
|
+
const inputTypes = {};
|
|
37
|
+
for (const [type, count] of Object.entries(shape.input_types ?? {}).slice(0, MAX_SHAPE_INPUT_TYPES)) {
|
|
38
|
+
if (Number.isSafeInteger(count) && count > 0)
|
|
39
|
+
inputTypes[shapeToken(type)] = count;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
keys: shape.keys.slice(0, MAX_SHAPE_KEYS).map(shapeToken),
|
|
43
|
+
include: Array.isArray(shape.include) ? shape.include.slice(0, MAX_SHAPE_INCLUDE).map(shapeToken) : null,
|
|
44
|
+
input_types: inputTypes,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function safeUpstreamId(value) {
|
|
48
|
+
const text = String(value ?? '').trim();
|
|
49
|
+
return /^[A-Za-z0-9_.:-]{1,64}$/.test(text) ? text : null;
|
|
50
|
+
}
|
|
51
|
+
export function safeUpstreamParam(value) {
|
|
52
|
+
if (typeof value !== 'string')
|
|
53
|
+
return null;
|
|
54
|
+
const text = value.slice(0, 64);
|
|
55
|
+
return /^[A-Za-z0-9_.[\]-]+$/.test(text) ? text : null;
|
|
56
|
+
}
|
|
5
57
|
function safeCatalogId(value) {
|
|
6
58
|
const text = String(value ?? '').replace(/[\r\n\0]/g, '').trim().slice(0, 128);
|
|
7
59
|
return text.length > 0 ? text : null;
|
|
@@ -69,6 +121,10 @@ export function createDesktopBridgeRejectionLogger(options = {}) {
|
|
|
69
121
|
const method = safeMethod(event.method);
|
|
70
122
|
const providerId = safeCatalogId(event.provider_id);
|
|
71
123
|
const publicModel = safeCatalogId(event.public_model);
|
|
124
|
+
const upstreamCode = safeUpstreamId(event.upstream_code);
|
|
125
|
+
const upstreamType = safeUpstreamId(event.upstream_type);
|
|
126
|
+
const upstreamParam = safeUpstreamParam(event.upstream_param);
|
|
127
|
+
const requestShape = safeRequestShape(event.request_shape);
|
|
72
128
|
emit({
|
|
73
129
|
event: 'sks.desktop_bridge.rejected',
|
|
74
130
|
code,
|
|
@@ -81,6 +137,11 @@ export function createDesktopBridgeRejectionLogger(options = {}) {
|
|
|
81
137
|
...(Number.isSafeInteger(event.max_request_body_bytes) && event.max_request_body_bytes > 0 ? { max_request_body_bytes: event.max_request_body_bytes } : {}),
|
|
82
138
|
...(Number.isSafeInteger(event.request_body_bytes) && event.request_body_bytes >= 0 ? { request_body_bytes: event.request_body_bytes } : {}),
|
|
83
139
|
...(event.request_body_stage === 'encoded' || event.request_body_stage === 'decoded' ? { request_body_stage: event.request_body_stage } : {}),
|
|
140
|
+
...(Number.isInteger(event.upstream_status) && event.upstream_status >= 100 && event.upstream_status <= 599 ? { upstream_status: event.upstream_status } : {}),
|
|
141
|
+
...(upstreamCode ? { upstream_code: upstreamCode } : {}),
|
|
142
|
+
...(upstreamType ? { upstream_type: upstreamType } : {}),
|
|
143
|
+
...(upstreamParam ? { upstream_param: upstreamParam } : {}),
|
|
144
|
+
...(requestShape ? { request_shape: requestShape } : {}),
|
|
84
145
|
});
|
|
85
146
|
};
|
|
86
147
|
}
|
|
@@ -1,14 +1,18 @@
|
|
|
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
|
-
import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
|
|
7
|
-
import {
|
|
6
|
+
import { createDesktopBridgeRejectionLogger, desktopBridgeRequestShape } from './rejection-log.js';
|
|
7
|
+
import { adaptRequestForRoute, logRequestAdaptation, providerExtraInclude } from './provider-request-compat.js';
|
|
8
|
+
import { assertDesktopBridgeRouteContext, ensureDesktopBridgeRemoteTarget, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, threadRouteMove } from './security.js';
|
|
9
|
+
import { admitCodexLbTurnState } from './codex-lb-turn-state.js';
|
|
10
|
+
import { ProviderHandoffRefusedError, assertProviderHandoffAllowed, providerHandoffRefusedEvent, stripProviderBoundItems, } from './provider-handoff.js';
|
|
8
11
|
import { DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES, DesktopBridgeError } from './types.js';
|
|
9
|
-
import { buildCompactionSummaryRequest, compactionCreatedEvent, compactionFailedEvent, compactionResponseEvents, expandBridgeCompactionItems, newCompactionResponseId, parseProviderSummary, requestsRemoteCompaction, ResponsesContinuationChain, summaryFailure, summaryWithinBounds, } from './compaction-adapter.js';
|
|
12
|
+
import { buildCompactionSummaryRequest, compactionCreatedEvent, compactionFailedEvent, compactionResponseEvents, expandBridgeCompactionItems, newCompactionResponseId, parseProviderSummary, requestsRemoteCompaction, ResponsesContinuationChain, safeUpstreamErrorId, summaryFailure, summaryWithinBounds, } from './compaction-adapter.js';
|
|
10
13
|
import { requestProviderCompactionSummary } from './http-forward.js';
|
|
11
14
|
import { modelCapacityAdvisedFrame } from './capacity-retry-advice.js';
|
|
15
|
+
import { desktopBridgeModelsEtag, frameWithModelsEtag } from './models-endpoint.js';
|
|
12
16
|
const MAX_PENDING_MESSAGES = 256;
|
|
13
17
|
const CLOSE_GRACE_MS = 1_000;
|
|
14
18
|
const INITIAL_CREATE_TIMEOUT_MS = 60 * 60_000;
|
|
@@ -20,6 +24,25 @@ export function isResponsesWebSocketRequest(req) {
|
|
|
20
24
|
function bytes(data) {
|
|
21
25
|
return Buffer.isBuffer(data) ? data : Array.isArray(data) ? Buffer.concat(data) : Buffer.from(data);
|
|
22
26
|
}
|
|
27
|
+
function isRecord(value) {
|
|
28
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
function upstreamFailureEvent(frame) {
|
|
31
|
+
if (!frame.includes('"response.failed"') && !frame.includes('"type":"error"') && !frame.includes('"type": "error"'))
|
|
32
|
+
return null;
|
|
33
|
+
let event;
|
|
34
|
+
try {
|
|
35
|
+
event = JSON.parse(frame.toString('utf8'));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (!isRecord(event) || (event.type !== 'error' && event.type !== 'response.failed'))
|
|
41
|
+
return null;
|
|
42
|
+
const error = event.type === 'error' ? event.error : isRecord(event.response) ? event.response.error : null;
|
|
43
|
+
const record = isRecord(error) ? error : {};
|
|
44
|
+
return { status: typeof event.status === 'number' ? event.status : undefined, code: record.code, type: record.type, param: record.param };
|
|
45
|
+
}
|
|
23
46
|
function withLineage(identity, next) {
|
|
24
47
|
return {
|
|
25
48
|
...identity,
|
|
@@ -32,6 +55,7 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
32
55
|
const upgradeIdentity = resolveCodexSessionIdentity(req.headers);
|
|
33
56
|
const maxBytes = config.maxRequestBodyBytes ?? DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES;
|
|
34
57
|
const server = new WebSocketServer({ noServer: true, clientTracking: false, perMessageDeflate: false, maxPayload: maxBytes });
|
|
58
|
+
const modelsEtag = config.modelsCatalog ? desktopBridgeModelsEtag(config.modelsCatalog.generation) : null;
|
|
35
59
|
try {
|
|
36
60
|
server.handleUpgrade(req, socket, head, (client) => {
|
|
37
61
|
bridge(client);
|
|
@@ -45,10 +69,12 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
45
69
|
let upstream = null;
|
|
46
70
|
let upstreamSocket = null;
|
|
47
71
|
let bound = null;
|
|
72
|
+
let boundByMove = false;
|
|
48
73
|
let boundBaseUrl = null;
|
|
49
74
|
let boundCredentialGeneration = null;
|
|
50
75
|
let boundCredentialFingerprint = null;
|
|
51
76
|
let routedModel = null;
|
|
77
|
+
let routedShape = null;
|
|
52
78
|
let stopped = false;
|
|
53
79
|
let processing = false;
|
|
54
80
|
let pendingBytes = 0;
|
|
@@ -126,26 +152,51 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
126
152
|
}
|
|
127
153
|
identity = bound ? withLineage(identity, nextIdentity) : nextIdentity;
|
|
128
154
|
const request = routeRequest(model);
|
|
129
|
-
|
|
130
|
-
|
|
155
|
+
const planned = assertDesktopBridgeRouteContext(request, config);
|
|
156
|
+
assertProviderHandoffAllowed(message.create, threadRouteMove(request, planned, config));
|
|
157
|
+
assertBinding(planned);
|
|
158
|
+
let move = null;
|
|
159
|
+
const route = await resolveAndBindDesktopBridgeRouteContext(request, config, (resolved) => {
|
|
160
|
+
move = threadRouteMove(request, resolved, config);
|
|
161
|
+
assertProviderHandoffAllowed(message.create, move);
|
|
162
|
+
});
|
|
131
163
|
if (stopped || client.readyState !== WebSocket.OPEN)
|
|
132
164
|
return;
|
|
133
165
|
assertBinding(route);
|
|
134
166
|
routedModel = route.public_model;
|
|
135
167
|
if (!bound) {
|
|
136
168
|
bound = route;
|
|
169
|
+
boundByMove = Boolean(move);
|
|
137
170
|
await connect(route);
|
|
138
171
|
}
|
|
139
172
|
const create = { ...message.create, model: route.upstream_model };
|
|
140
|
-
|
|
141
|
-
if (route.
|
|
173
|
+
let rewritten = expandBridgeCompactionItems(create);
|
|
174
|
+
if (route.session_pin?.handoff_from && stripProviderBoundItems(create, Boolean(move)))
|
|
175
|
+
rewritten = true;
|
|
176
|
+
const adapted = adaptRequestForRoute(create, route, providerExtraInclude(config, route));
|
|
177
|
+
if (adapted.length) {
|
|
178
|
+
rewritten = true;
|
|
179
|
+
logRequestAdaptation(config, route, adapted, 'websocket');
|
|
180
|
+
}
|
|
181
|
+
if (BRIDGE_REMOTE_COMPACTION[route.provider_id] === 'bridge_summary' && requestsRemoteCompaction(create)) {
|
|
142
182
|
message.compaction = create;
|
|
143
183
|
return;
|
|
144
184
|
}
|
|
145
|
-
if (route.upstream_model !== model ||
|
|
185
|
+
if (route.upstream_model !== model || rewritten) {
|
|
146
186
|
message.data = Buffer.from(JSON.stringify(create));
|
|
187
|
+
message.binary = false;
|
|
188
|
+
}
|
|
147
189
|
message.routed = create;
|
|
148
190
|
}
|
|
191
|
+
function refuseMove(message, error) {
|
|
192
|
+
const model = typeof message.create?.model === 'string' ? message.create.model : '';
|
|
193
|
+
logRejection({ code: error.code, transport: 'websocket', ...(req.url ? { url: req.url } : {}) });
|
|
194
|
+
relay(client, Buffer.from(JSON.stringify(providerHandoffRefusedEvent(error, newCompactionResponseId(), model))), false);
|
|
195
|
+
const index = pending.indexOf(message);
|
|
196
|
+
if (index >= 0)
|
|
197
|
+
pending.splice(index, 1);
|
|
198
|
+
pendingBytes -= message.originalBytes;
|
|
199
|
+
}
|
|
149
200
|
async function answerCompaction(create) {
|
|
150
201
|
const route = bound;
|
|
151
202
|
const provider = config.providers[route.provider_id];
|
|
@@ -160,7 +211,9 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
160
211
|
send(compactionFailedEvent(responseId, route.public_model, 'bridge_compaction_history_unavailable'));
|
|
161
212
|
return;
|
|
162
213
|
}
|
|
163
|
-
const
|
|
214
|
+
const summaryRequest = buildCompactionSummaryRequest({ ...create, input }, route.upstream_model);
|
|
215
|
+
adaptRequestForRoute(summaryRequest, route, providerExtraInclude(config, route));
|
|
216
|
+
const body = Buffer.from(JSON.stringify(summaryRequest));
|
|
164
217
|
let accepted = false;
|
|
165
218
|
try {
|
|
166
219
|
const call = await requestProviderCompactionSummary(config, { provider, credential, clientHeaders: req.headers, requestUrl: req.url || '/' }, body, () => {
|
|
@@ -192,6 +245,7 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
192
245
|
}
|
|
193
246
|
async function connect(route) {
|
|
194
247
|
const official = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID;
|
|
248
|
+
const summarizes = BRIDGE_REMOTE_COMPACTION[route.provider_id] === 'bridge_summary';
|
|
195
249
|
const provider = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID ? null : config.providers[route.provider_id];
|
|
196
250
|
const remote = official ? config.officialRemote : provider?.remote;
|
|
197
251
|
if (!remote)
|
|
@@ -209,6 +263,10 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
209
263
|
const target = resolveDesktopBridgeTarget(req.url, remote);
|
|
210
264
|
const outgoing = official ? buildOfficialPassthroughWebSocketHeaders(req.headers, target.host)
|
|
211
265
|
: buildProviderWebSocketHeaders(req.headers, { providerId: provider.provider_id, authTransport: provider.auth_transport, credential: credential }, target.host);
|
|
266
|
+
if (boundByMove)
|
|
267
|
+
delete outgoing['x-codex-turn-state'];
|
|
268
|
+
else if (provider?.provider_id === 'codex-lb')
|
|
269
|
+
admitCodexLbTurnState(outgoing, identity.thread_id, credential.fingerprint);
|
|
212
270
|
const headers = {};
|
|
213
271
|
for (const [name, value] of Object.entries(outgoing)) {
|
|
214
272
|
if (name.startsWith('sec-websocket-') || name === 'connection' || name === 'upgrade' || name === 'content-length')
|
|
@@ -256,7 +314,22 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
256
314
|
frame = advised;
|
|
257
315
|
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
316
|
}
|
|
259
|
-
|
|
317
|
+
const advertised = modelsEtag && !binary ? frameWithModelsEtag(frame, modelsEtag) : null;
|
|
318
|
+
if (advertised)
|
|
319
|
+
frame = advertised;
|
|
320
|
+
const failure = binary ? null : upstreamFailureEvent(frame);
|
|
321
|
+
if (failure) {
|
|
322
|
+
logRejection({
|
|
323
|
+
code: 'bridge_upstream_request_rejected', transport: 'websocket',
|
|
324
|
+
...(failure.status === undefined ? {} : { status: failure.status, upstream_status: failure.status }),
|
|
325
|
+
provider_id: route.provider_id, public_model: routedModel ?? route.public_model,
|
|
326
|
+
upstream_code: safeUpstreamErrorId(failure.code), upstream_type: safeUpstreamErrorId(failure.type),
|
|
327
|
+
upstream_param: typeof failure.param === 'string' ? failure.param : null,
|
|
328
|
+
request_shape: routedShape, ...(req.url ? { url: req.url } : {}),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (summarizes)
|
|
332
|
+
chain.received(frame);
|
|
260
333
|
try {
|
|
261
334
|
relay(client, frame, binary);
|
|
262
335
|
}
|
|
@@ -289,24 +362,43 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
289
362
|
return;
|
|
290
363
|
processing = true;
|
|
291
364
|
try {
|
|
292
|
-
|
|
365
|
+
while (!bound) {
|
|
293
366
|
const firstCreate = pending.find((message) => message.create);
|
|
294
367
|
if (!firstCreate)
|
|
295
368
|
return;
|
|
296
|
-
|
|
369
|
+
try {
|
|
370
|
+
await resolveCreate(firstCreate);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
if (!(error instanceof ProviderHandoffRefusedError))
|
|
374
|
+
throw error;
|
|
375
|
+
refuseMove(firstCreate, error);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
297
378
|
firstCreate.create = null;
|
|
298
379
|
}
|
|
299
380
|
while (!stopped && client.readyState === WebSocket.OPEN && pending.length) {
|
|
300
381
|
const message = pending[0];
|
|
301
|
-
if (message.create)
|
|
302
|
-
|
|
382
|
+
if (message.create) {
|
|
383
|
+
try {
|
|
384
|
+
await resolveCreate(message);
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
if (!(error instanceof ProviderHandoffRefusedError))
|
|
388
|
+
throw error;
|
|
389
|
+
refuseMove(message, error);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
303
393
|
if (stopped || client.readyState !== WebSocket.OPEN)
|
|
304
394
|
return;
|
|
305
395
|
if (message.compaction)
|
|
306
396
|
await answerCompaction(message.compaction);
|
|
307
397
|
else {
|
|
308
398
|
relay(upstream, message.data, message.binary);
|
|
309
|
-
if (message.routed
|
|
399
|
+
if (message.routed)
|
|
400
|
+
routedShape = desktopBridgeRequestShape(message.routed);
|
|
401
|
+
if (message.routed && bound && BRIDGE_REMOTE_COMPACTION[bound.provider_id] === 'bridge_summary')
|
|
310
402
|
chain.sent(message.routed, message.data);
|
|
311
403
|
}
|
|
312
404
|
pending.shift();
|