sneakoscope 10.7.1 → 10.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/command-manifest-lite.js +1 -1
  5. package/dist/cli/install-helpers-codex-lb-config.js +44 -12
  6. package/dist/commands/bridge.js +14 -1
  7. package/dist/commands/codex-app.js +3 -3
  8. package/dist/commands/codex.js +5 -1
  9. package/dist/commands/doctor-console.js +2 -0
  10. package/dist/commands/doctor.js +8 -9
  11. package/dist/config/skills-manifest.json +1 -1
  12. package/dist/core/codex-app/codex-context-window.js +23 -8
  13. package/dist/core/codex-app/menubar/constants.js +0 -1
  14. package/dist/core/codex-control/model-call-concurrency.js +6 -5
  15. package/dist/core/codex-lb/combined-catalog/persistence.js +4 -0
  16. package/dist/core/codex-lb/combined-catalog/storage.js +4 -1
  17. package/dist/core/codex-lb/desktop-bridge/capacity-retry-advice.js +35 -11
  18. package/dist/core/codex-lb/desktop-bridge/http-forward.js +48 -8
  19. package/dist/core/codex-lb/desktop-bridge/models-endpoint.js +62 -0
  20. package/dist/core/codex-lb/desktop-bridge/provider-handoff.js +13 -4
  21. package/dist/core/codex-lb/desktop-bridge/provider-request-compat.js +89 -0
  22. package/dist/core/codex-lb/desktop-bridge/rejection-log.js +61 -0
  23. package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +52 -4
  24. package/dist/core/codex-lb/desktop-bridge/server.js +5 -0
  25. package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +2 -6
  26. package/dist/core/codex-lb/desktop-bridge-migration.js +14 -2
  27. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +114 -5
  28. package/dist/core/codex-lb/desktop-controller-v3/model-refresh.js +140 -0
  29. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +5 -5
  30. package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +14 -2
  31. package/dist/core/codex-lb/desktop-controller-v3/status.js +2 -3
  32. package/dist/core/codex-lb/desktop-controller-v3.js +3 -1
  33. package/dist/core/codex-lb/desktop-service.js +41 -4
  34. package/dist/core/codex-lb/model-refresh-after.js +16 -0
  35. package/dist/core/commands/basic-cli.js +5 -0
  36. package/dist/core/commands/menubar-command.js +5 -1
  37. package/dist/core/doctor/desktop-bridge-catalog-repair.js +20 -17
  38. package/dist/core/subagents/model-tiers.js +19 -3
  39. package/dist/core/update/update-migration-state/desktop-bridge-catalog-repair-stage.js +4 -3
  40. package/dist/core/version.js +1 -1
  41. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +0 -1
  42. package/dist/native/sks-menubar/Sources/OverviewSummary.swift +32 -0
  43. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +15 -5
  44. package/dist/native/sks-menubar/Sources/SidebarItem.swift +0 -2
  45. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +3 -0
  46. package/package.json +1 -1
  47. package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +0 -49
@@ -0,0 +1,62 @@
1
+ export const DESKTOP_BRIDGE_MODELS_PATHS = ['/backend-api/codex/models', '/api/v1/models', '/v1/models'];
2
+ export function desktopBridgeModelsEtag(generation) {
3
+ return `sksb-${generation.slice(0, 16)}`;
4
+ }
5
+ export function isDesktopBridgeModelsRequest(req, pathname) {
6
+ return (req.method === 'GET' || req.method === 'HEAD') && DESKTOP_BRIDGE_MODELS_PATHS.includes(pathname);
7
+ }
8
+ function versionParts(value) {
9
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(String(value ?? '').trim());
10
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
11
+ }
12
+ function newerThan(left, right) {
13
+ for (let index = 0; index < 3; index += 1) {
14
+ if (left[index] !== right[index])
15
+ return left[index] > right[index];
16
+ }
17
+ return false;
18
+ }
19
+ export function modelsForClient(catalog, clientVersion) {
20
+ const client = versionParts(clientVersion);
21
+ if (!client)
22
+ return [...catalog.models];
23
+ return catalog.models.filter((row) => {
24
+ const minimum = versionParts(row.minimal_client_version);
25
+ return !minimum || !newerThan(minimum, client);
26
+ });
27
+ }
28
+ export function answerDesktopBridgeModels(req, res, catalog) {
29
+ const etag = desktopBridgeModelsEtag(catalog.generation);
30
+ const headers = { etag, 'cache-control': 'no-store', 'content-type': 'application/json' };
31
+ const ifNoneMatch = String(req.headers['if-none-match'] || '').split(',').map((value) => value.trim());
32
+ if (ifNoneMatch.includes(etag)) {
33
+ res.writeHead(304, headers);
34
+ res.end();
35
+ return;
36
+ }
37
+ const clientVersion = new URL(String(req.url || '/'), 'http://bridge.invalid').searchParams.get('client_version');
38
+ const body = Buffer.from(JSON.stringify({ models: modelsForClient(catalog, clientVersion) }));
39
+ res.writeHead(200, { ...headers, 'content-length': String(body.length) });
40
+ res.end(req.method === 'HEAD' ? undefined : body);
41
+ }
42
+ function isRecord(value) {
43
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
44
+ }
45
+ export function frameWithModelsEtag(frame, etag) {
46
+ if (!frame.includes('"codex.response.metadata"'))
47
+ return null;
48
+ let event;
49
+ try {
50
+ event = JSON.parse(frame.toString('utf8'));
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ if (!isRecord(event) || event.type !== 'codex.response.metadata')
56
+ return null;
57
+ const headers = isRecord(event.headers) ? event.headers : {};
58
+ const others = Object.entries(headers).filter(([name]) => name.toLowerCase() !== 'x-models-etag');
59
+ if (headers['x-models-etag'] === etag && others.length === Object.keys(headers).length - 1)
60
+ return null;
61
+ return Buffer.from(JSON.stringify({ ...event, headers: { ...Object.fromEntries(others), 'x-models-etag': etag } }));
62
+ }
@@ -60,10 +60,19 @@ export function stripProviderBoundItems(payload, moving) {
60
60
  changed = true;
61
61
  continue;
62
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;
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);
67
76
  continue;
68
77
  }
69
78
  input.push(item);
@@ -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
  }
@@ -3,14 +3,16 @@ import tls from 'node:tls';
3
3
  import WebSocket, { WebSocketServer } from 'ws';
4
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';
6
+ import { createDesktopBridgeRejectionLogger, desktopBridgeRequestShape } from './rejection-log.js';
7
+ import { adaptRequestForRoute, logRequestAdaptation, providerExtraInclude } from './provider-request-compat.js';
7
8
  import { assertDesktopBridgeRouteContext, ensureDesktopBridgeRemoteTarget, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, threadRouteMove } from './security.js';
8
9
  import { admitCodexLbTurnState } from './codex-lb-turn-state.js';
9
10
  import { ProviderHandoffRefusedError, assertProviderHandoffAllowed, providerHandoffRefusedEvent, stripProviderBoundItems, } from './provider-handoff.js';
10
11
  import { DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES, DesktopBridgeError } from './types.js';
11
- 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';
12
13
  import { requestProviderCompactionSummary } from './http-forward.js';
13
14
  import { modelCapacityAdvisedFrame } from './capacity-retry-advice.js';
15
+ import { desktopBridgeModelsEtag, frameWithModelsEtag } from './models-endpoint.js';
14
16
  const MAX_PENDING_MESSAGES = 256;
15
17
  const CLOSE_GRACE_MS = 1_000;
16
18
  const INITIAL_CREATE_TIMEOUT_MS = 60 * 60_000;
@@ -22,6 +24,25 @@ export function isResponsesWebSocketRequest(req) {
22
24
  function bytes(data) {
23
25
  return Buffer.isBuffer(data) ? data : Array.isArray(data) ? Buffer.concat(data) : Buffer.from(data);
24
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
+ }
25
46
  function withLineage(identity, next) {
26
47
  return {
27
48
  ...identity,
@@ -34,6 +55,7 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
34
55
  const upgradeIdentity = resolveCodexSessionIdentity(req.headers);
35
56
  const maxBytes = config.maxRequestBodyBytes ?? DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES;
36
57
  const server = new WebSocketServer({ noServer: true, clientTracking: false, perMessageDeflate: false, maxPayload: maxBytes });
58
+ const modelsEtag = config.modelsCatalog ? desktopBridgeModelsEtag(config.modelsCatalog.generation) : null;
37
59
  try {
38
60
  server.handleUpgrade(req, socket, head, (client) => {
39
61
  bridge(client);
@@ -52,6 +74,7 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
52
74
  let boundCredentialGeneration = null;
53
75
  let boundCredentialFingerprint = null;
54
76
  let routedModel = null;
77
+ let routedShape = null;
55
78
  let stopped = false;
56
79
  let processing = false;
57
80
  let pendingBytes = 0;
@@ -150,12 +173,19 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
150
173
  let rewritten = expandBridgeCompactionItems(create);
151
174
  if (route.session_pin?.handoff_from && stripProviderBoundItems(create, Boolean(move)))
152
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
+ }
153
181
  if (BRIDGE_REMOTE_COMPACTION[route.provider_id] === 'bridge_summary' && requestsRemoteCompaction(create)) {
154
182
  message.compaction = create;
155
183
  return;
156
184
  }
157
- if (route.upstream_model !== model || rewritten)
185
+ if (route.upstream_model !== model || rewritten) {
158
186
  message.data = Buffer.from(JSON.stringify(create));
187
+ message.binary = false;
188
+ }
159
189
  message.routed = create;
160
190
  }
161
191
  function refuseMove(message, error) {
@@ -181,7 +211,9 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
181
211
  send(compactionFailedEvent(responseId, route.public_model, 'bridge_compaction_history_unavailable'));
182
212
  return;
183
213
  }
184
- const body = Buffer.from(JSON.stringify(buildCompactionSummaryRequest({ ...create, input }, route.upstream_model)));
214
+ const summaryRequest = buildCompactionSummaryRequest({ ...create, input }, route.upstream_model);
215
+ adaptRequestForRoute(summaryRequest, route, providerExtraInclude(config, route));
216
+ const body = Buffer.from(JSON.stringify(summaryRequest));
185
217
  let accepted = false;
186
218
  try {
187
219
  const call = await requestProviderCompactionSummary(config, { provider, credential, clientHeaders: req.headers, requestUrl: req.url || '/' }, body, () => {
@@ -282,6 +314,20 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
282
314
  frame = advised;
283
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 } : {}) });
284
316
  }
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
+ }
285
331
  if (summarizes)
286
332
  chain.received(frame);
287
333
  try {
@@ -350,6 +396,8 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
350
396
  await answerCompaction(message.compaction);
351
397
  else {
352
398
  relay(upstream, message.data, message.binary);
399
+ if (message.routed)
400
+ routedShape = desktopBridgeRequestShape(message.routed);
353
401
  if (message.routed && bound && BRIDGE_REMOTE_COMPACTION[bound.provider_id] === 'bridge_summary')
354
402
  chain.sent(message.routed, message.data);
355
403
  }
@@ -11,6 +11,7 @@ import { ProviderHandoffRefusedError, providerHandoffRefusalBody } from './provi
11
11
  import { PACKAGE_VERSION } from '../../version.js';
12
12
  import { DESKTOP_BRIDGE_CLIENT_PATH_PREFIX, DESKTOP_BRIDGE_DIAGNOSTIC_HEALTH_PATH, DESKTOP_BRIDGE_DIAGNOSTIC_PATH, DESKTOP_BRIDGE_DIAGNOSTIC_PROTOCOL, DESKTOP_BRIDGE_IMAGEGEN_PATH } from './types.js';
13
13
  import { handleDesktopBridgeImagegen } from './imagegen-endpoint.js';
14
+ import { answerDesktopBridgeModels, isDesktopBridgeModelsRequest } from './models-endpoint.js';
14
15
  import { forwardWebSocket, safeEndUpgradeSocket } from './websocket-forward.js';
15
16
  function authenticateDesktopBridgeClient(req, input) {
16
17
  const raw = String(req.url || '/');
@@ -270,6 +271,10 @@ export async function startPreparedDesktopBridge(input, options = {}) {
270
271
  return;
271
272
  }
272
273
  assertAllowedPath(authenticated.pathname, input.allowedPathPrefixes);
274
+ if (input.modelsCatalog && isDesktopBridgeModelsRequest(req, authenticated.pathname)) {
275
+ answerDesktopBridgeModels(req, res, input.modelsCatalog);
276
+ return;
277
+ }
273
278
  const prepared = await prepareDesktopBridgeRequest(req, input);
274
279
  await forwardHttp(req, res, input, prepared, `${desktopBridgeListenOrigin(input)}${authenticated.clientBasePath}`);
275
280
  }
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER, DESKTOP_BRIDGE_MANAGED_MARKER, DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER } from '../../../cli/install-helpers-codex-lb-config.js';
2
+ import { DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER, DESKTOP_BRIDGE_MANAGED_MARKER } from '../../../cli/install-helpers-codex-lb-config.js';
3
3
  import { escapeRegExp } from '../../text/regex.js';
4
4
  import { CANONICAL_OPENROUTER_ENDPOINT, HISTORICAL_DESKTOP_BRIDGE_MARKER, HISTORICAL_DESKTOP_COMPAT_MARKER, HISTORICAL_MODEL_CATALOG_MARKER, HISTORICAL_OAUTH_SELECTION_MARKER, HISTORICAL_OPENAI_ROUTING_MARKER, HISTORICAL_PROVIDER_MODE_MARKER, HISTORICAL_PROVIDER_SELECTION_MARKER, hasTomlTable, hasTopLevelMarker, isHistoricalProviderMode, secretFreeProviderEndpoint, tomlTableBoolean, tomlTableContainsCustomHeader, tomlTableString, topLevelTomlString } from './historical-toml.js';
5
5
  export function inspectHistoricalDesktopBridgeIntent(configText) {
@@ -122,11 +122,7 @@ export function inspectHistoricalDesktopBridgeIntent(configText) {
122
122
  }
123
123
  export function prepareHistoricalConfigForDesktopBridgeWriter(text, state) {
124
124
  const source = String(text || '');
125
- if ([
126
- DESKTOP_BRIDGE_MANAGED_MARKER,
127
- DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER,
128
- DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER
129
- ].every((marker) => hasTopLevelMarker(source, marker)))
125
+ if ([DESKTOP_BRIDGE_MANAGED_MARKER, DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER].every((marker) => hasTopLevelMarker(source, marker)))
130
126
  return source;
131
127
  const historicalMarkers = historicalManagedMarkers();
132
128
  const hasHistoricalMarker = historicalMarkers.some((marker) => hasTopLevelMarker(source, marker));
@@ -55,7 +55,8 @@ async function migrateDesktopBridgeConfigUnlocked(input) {
55
55
  const migrationInputConfig = prepareHistoricalConfigForDesktopBridgeWriter(currentConfig, historicalState);
56
56
  nextConfig = upsertDesktopBridgeManagedConfig(migrationInputConfig, {
57
57
  bridgeBaseUrl: input.bridgeBaseUrl,
58
- combinedCatalogPath
58
+ combinedCatalogPath,
59
+ bindModelCatalog: input.bindModelCatalog !== false
59
60
  });
60
61
  validateMetadataUpdates(input.metadataUpdates || [], { home, configPath, authPath });
61
62
  }
@@ -74,6 +75,17 @@ async function migrateDesktopBridgeConfigUnlocked(input) {
74
75
  error: message
75
76
  };
76
77
  }
78
+ if (input.configWrite === 'forbid' && nextConfig !== currentConfig) {
79
+ return {
80
+ ...baseResult,
81
+ ok: false,
82
+ status: 'blocked',
83
+ managed_runtime: null,
84
+ receipt_path: null,
85
+ auth_semantic_identity_preserved: true,
86
+ blockers: ['desktop_bridge_config_write_deferred']
87
+ };
88
+ }
77
89
  const metadataUpdates = normalizedMetadataUpdates(input.metadataUpdates || []);
78
90
  const metadataAlreadyCurrent = await Promise.all(metadataUpdates.map(async (update) => {
79
91
  const currentSha = await fileSha256OrMissing(update.path);
@@ -82,7 +94,7 @@ async function migrateDesktopBridgeConfigUnlocked(input) {
82
94
  const hasUnifiedMarkers = [
83
95
  DESKTOP_BRIDGE_MANAGED_MARKER,
84
96
  DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER,
85
- DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER
97
+ ...(input.bindModelCatalog === false ? [] : [DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER])
86
98
  ].every((marker) => hasTopLevelMarker(currentConfig, marker));
87
99
  if (nextConfig === currentConfig && hasUnifiedMarkers && metadataAlreadyCurrent.every(Boolean)) {
88
100
  const authAfter = await captureCodexAuthSnapshot({ home, authPath });
@@ -1,5 +1,5 @@
1
1
  import { readText, writeJsonAtomic } from '../../fsx.js';
2
- import { repairManagedCodexLbClientSelection } from '../../../cli/install-helpers-codex-lb-config.js';
2
+ import { codexReadsBridgeModels, hasUserOwnedModelCatalog, repairManagedCodexLbClientSelection } from '../../../cli/install-helpers-codex-lb-config.js';
3
3
  import { safeWriteCodexConfigToml } from '../../codex-runtime/codex-desktop-config-policy.js';
4
4
  import { readTopLevelTomlString } from '../../codex-app/codex-model-catalog.js';
5
5
  import { codexRestartBlockers, maybeRestartRunningCodexApp } from '../../codex-app/codex-app-restart-policy.js';
@@ -18,6 +18,7 @@ import { sha256Stable } from '../route-index.js';
18
18
  import { bridgeBaseUrl, commandResult, controllerEnv, controllerPaths, nowIso, providerCode, providerRegistrySnapshot, resolveRawCredentials, resolveValidatedCredentials, safeCode, serializedSettings, stringArray, timeoutMs, unique } from './shared.js';
19
19
  import { desktopBridgeStatusV3 } from './status.js';
20
20
  import { openRouterOnlyExposure, readControllerOpenRouterOnlyState, withoutCodexLbCatalog } from './openrouter-only-catalog.js';
21
+ import { MODEL_REFRESH_RECOVERY_COMMAND, alignCodexModelSource, bridgeServesModels, listedModelSlugs, modelListDiff, modelRefreshLine, readModelRefreshState, refreshedRecently, writeModelRefreshState } from './model-refresh.js';
21
22
  export async function syncCatalog(options) {
22
23
  const result = await syncCatalogInternal(options);
23
24
  const status = await desktopBridgeStatusV3(options);
@@ -36,10 +37,11 @@ async function repairCodexLbClientSelection(configPath) {
36
37
  }
37
38
  export async function syncCatalogInternal(options, behavior = {}) {
38
39
  const paths = controllerPaths(options);
39
- const repair = await repairCodexLbClientSelection(paths.configPath);
40
+ const repair = behavior.configWrites === false ? null : await repairCodexLbClientSelection(paths.configPath);
40
41
  if (repair)
41
42
  return blockedSync(inspectHistoricalDesktopBridgeIntent(''), [repair]);
42
- const historicalIntent = inspectHistoricalDesktopBridgeIntent(await readText(paths.configPath, ''));
43
+ const configText = await readText(paths.configPath, '');
44
+ const historicalIntent = inspectHistoricalDesktopBridgeIntent(configText);
43
45
  if (historicalIntent.blockers.length > 0)
44
46
  return blockedSync(historicalIntent, historicalIntent.blockers);
45
47
  const rawCredentials = await resolveRawCredentials(options, paths);
@@ -68,6 +70,10 @@ export async function syncCatalogInternal(options, behavior = {}) {
68
70
  ? openRouterOnlyExposure(selection, openRouterOnly, await readText(paths.configPath, ''))
69
71
  : selection;
70
72
  const build = buildCombinedBridgeCatalog(catalogRegistry(registry), { catalogs, created_at: nowIso(options), selection: exposure });
73
+ const lost = providersLostToFailedFetch(await readActiveCombinedBridgeCatalog(paths.catalogPath, paths.routeIndexPath), catalogs, catalogRegistry(registry));
74
+ if (lost.length > 0) {
75
+ return { ...blockedSync(historicalIntent, lost.map((providerId) => `${providerCode(providerId)}_catalog_refresh_failed`)), build, active_generation_preserved: true };
76
+ }
71
77
  if (!selectionState.stored)
72
78
  await writeBridgeModelSelection(paths.home, selection).catch(() => undefined);
73
79
  await writeJsonAtomic(bridgeAvailableModelsPath(paths.home), {
@@ -139,6 +145,8 @@ export async function syncCatalogInternal(options, behavior = {}) {
139
145
  receiptDir: paths.receiptDir,
140
146
  bridgeBaseUrl: managedBridgeBaseUrl,
141
147
  combinedCatalogPath: staging.catalog_path,
148
+ bindModelCatalog: !codexReadsBridgeModels(configText) && !hasUserOwnedModelCatalog(configText),
149
+ configWrite: behavior.configWrites === false ? 'forbid' : 'allow',
142
150
  newCatalogGeneration: build.catalog.generation,
143
151
  metadataUpdates: [
144
152
  {
@@ -316,6 +324,20 @@ function emptyProviderCatalog(providerId, state, checkedAt, blockers) {
316
324
  warnings: []
317
325
  };
318
326
  }
327
+ function providersLostToFailedFetch(active, catalogs, registry) {
328
+ if (!active.ok)
329
+ return [];
330
+ return ['codex-lb', 'openrouter'].filter((providerId) => {
331
+ const blockers = catalogs[providerId].blockers || [];
332
+ return registry.profiles[providerId].enabled
333
+ && catalogs[providerId].state === 'failed'
334
+ && !blockers.some(refusesCredential)
335
+ && active.catalog.models.some((model) => model.provider_id === providerId);
336
+ });
337
+ }
338
+ function refusesCredential(blocker) {
339
+ return /unauthorized|credential_missing|key_missing|invalid_key|_http_40[13]\b/.test(blocker);
340
+ }
319
341
  function validationFailureState(blockers) {
320
342
  return blockers.some((blocker) => /(?:401|403|auth|rejected|invalid_key|unauthorized)/i.test(blocker))
321
343
  ? 'rejected' : 'unavailable';
@@ -362,12 +384,14 @@ export async function selectExposedModels(publicIds, options) {
362
384
  updated_at: nowIso(options),
363
385
  openrouter: { mode: 'selected', public_ids: requested }
364
386
  });
365
- const sync = await syncCatalogInternal(options);
387
+ const sync = await syncAsRefresh(options, 'models.select');
366
388
  const status = await desktopBridgeStatusV3(options);
367
389
  const activation = sync.activation && typeof sync.activation === 'object'
368
390
  ? sync.activation
369
391
  : {};
370
- const changed = before.selection.openrouter.public_ids.join('\0') !== requested.join('\0');
392
+ const refresh = sync.model_refresh;
393
+ const changed = before.selection.openrouter.public_ids.join('\0') !== requested.join('\0')
394
+ && (refresh.codex_model_source !== 'bridge_models' || refresh.restart_codex_required);
371
395
  const restart = sync.ok === true
372
396
  ? await maybeRestartRunningCodexApp({
373
397
  env: controllerEnv(options),
@@ -387,3 +411,88 @@ function seedSelectionFromConfiguredModel(selection, configText) {
387
411
  return selection;
388
412
  return { ...selection, openrouter: { mode: 'selected', public_ids: [configured] } };
389
413
  }
414
+ export async function refreshModelCatalog(options, input) {
415
+ const paths = controllerPaths(options);
416
+ const refreshedAt = nowIso(options);
417
+ const base = {
418
+ schema: 'sks.bridge-model-refresh.v1',
419
+ reason: input.reason,
420
+ refreshed_at: refreshedAt,
421
+ added: [],
422
+ removed: [],
423
+ restart_codex_required: false
424
+ };
425
+ try {
426
+ const registryGeneration = (await resolveBridgeProviderRegistry({
427
+ home: paths.home,
428
+ credentials: await resolveRawCredentials(options, paths)
429
+ })).generation;
430
+ const recent = input.force ? null : await readModelRefreshState(paths.home);
431
+ if (recent && refreshedRecently(recent, registryGeneration, Date.parse(refreshedAt))) {
432
+ return {
433
+ ...base, ok: true, skipped: true, generation: recent.generation, total: recent.total, codex_model_source: null,
434
+ line: `Model list refreshed recently: ${recent.total} models generation ${recent.generation.slice(0, 8)}`,
435
+ warnings: ['model_refresh_skipped_recent'], blockers: [], sync: null
436
+ };
437
+ }
438
+ const before = await readActiveCombinedBridgeCatalog(paths.catalogPath, paths.routeIndexPath);
439
+ const sync = await syncCatalogInternal(options, { ...(input.behavior || {}), configWrites: input.allowConfigWrites });
440
+ if (sync.ok !== true)
441
+ return refreshFailed(base, stringArray(sync.activation?.blockers), sync);
442
+ const build = sync.build;
443
+ const activation = sync.activation;
444
+ const diff = modelListDiff(listedModelSlugs(before.ok ? before.catalog : null), listedModelSlugs(build.catalog));
445
+ const source = await alignCodexModelSource({
446
+ home: paths.home,
447
+ configPath: paths.configPath,
448
+ authPath: paths.authPath,
449
+ catalog: build.catalog,
450
+ catalogPath: typeof activation.catalog_path === 'string' ? activation.catalog_path : null,
451
+ allowConfigWrites: input.allowConfigWrites,
452
+ selfCheck: options.modelsSelfCheckImpl || bridgeServesModels
453
+ });
454
+ const restart = source.changed || (source.source === 'model_catalog_json' && diff.added.length + diff.removed.length > 0);
455
+ const total = listedModelSlugs(build.catalog).length;
456
+ await writeModelRefreshState(paths.home, {
457
+ refreshed_at: refreshedAt,
458
+ provider_registry_generation: registryGeneration,
459
+ generation: build.catalog.generation,
460
+ total
461
+ }).catch(() => undefined);
462
+ return {
463
+ ...base, ok: true, skipped: false, generation: build.catalog.generation, total, added: diff.added, removed: diff.removed,
464
+ codex_model_source: source.source, restart_codex_required: restart,
465
+ line: modelRefreshLine({ total, added: diff.added.length, removed: diff.removed.length, generation: build.catalog.generation, restartCodex: restart }),
466
+ warnings: source.warnings, blockers: [], sync
467
+ };
468
+ }
469
+ catch (error) {
470
+ return refreshFailed(base, [safeCode(error, 'model_refresh_failed')], null);
471
+ }
472
+ }
473
+ function refreshFailed(base, blockers, sync) {
474
+ return {
475
+ ...base, ok: false, skipped: false, generation: null, total: 0, codex_model_source: null,
476
+ line: `Model list refresh failed; the previous list stays active. Run \`${MODEL_REFRESH_RECOVERY_COMMAND}\` to retry.`,
477
+ warnings: [`model_refresh_failed: run \`${MODEL_REFRESH_RECOVERY_COMMAND}\``],
478
+ blockers: unique(blockers.length > 0 ? blockers : ['model_refresh_failed']),
479
+ sync
480
+ };
481
+ }
482
+ export async function refreshModels(request, options) {
483
+ const refresh = await refreshModelCatalog(options, {
484
+ reason: request.reason || 'cli',
485
+ force: request.force !== false,
486
+ allowConfigWrites: request.allow_config_writes !== false
487
+ });
488
+ const { sync, ...model_refresh } = refresh;
489
+ const status = await desktopBridgeStatusV3(options);
490
+ return commandResult('models.refresh', refresh.ok, status, { model_refresh, catalog_sync: sync }, refresh.blockers, options);
491
+ }
492
+ export async function syncAsRefresh(options, reason, behavior = {}) {
493
+ const { sync, ...model_refresh } = await refreshModelCatalog(options, { reason, force: true, allowConfigWrites: true, behavior });
494
+ return {
495
+ ...(sync ?? blockedSync(null, model_refresh.blockers)),
496
+ model_refresh
497
+ };
498
+ }