sneakoscope 10.7.1 → 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.
Files changed (43) 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.js +5 -1
  8. package/dist/commands/doctor-console.js +2 -0
  9. package/dist/commands/doctor.js +8 -9
  10. package/dist/config/skills-manifest.json +1 -1
  11. package/dist/core/codex-app/menubar/constants.js +0 -1
  12. package/dist/core/codex-lb/combined-catalog/persistence.js +4 -0
  13. package/dist/core/codex-lb/combined-catalog/storage.js +4 -1
  14. package/dist/core/codex-lb/desktop-bridge/http-forward.js +48 -8
  15. package/dist/core/codex-lb/desktop-bridge/models-endpoint.js +62 -0
  16. package/dist/core/codex-lb/desktop-bridge/provider-handoff.js +13 -4
  17. package/dist/core/codex-lb/desktop-bridge/provider-request-compat.js +89 -0
  18. package/dist/core/codex-lb/desktop-bridge/rejection-log.js +61 -0
  19. package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +52 -4
  20. package/dist/core/codex-lb/desktop-bridge/server.js +5 -0
  21. package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +2 -6
  22. package/dist/core/codex-lb/desktop-bridge-migration.js +14 -2
  23. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +114 -5
  24. package/dist/core/codex-lb/desktop-controller-v3/model-refresh.js +140 -0
  25. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +5 -5
  26. package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +14 -2
  27. package/dist/core/codex-lb/desktop-controller-v3/status.js +2 -3
  28. package/dist/core/codex-lb/desktop-controller-v3.js +3 -1
  29. package/dist/core/codex-lb/desktop-service.js +41 -4
  30. package/dist/core/codex-lb/model-refresh-after.js +16 -0
  31. package/dist/core/commands/basic-cli.js +5 -0
  32. package/dist/core/commands/menubar-command.js +5 -1
  33. package/dist/core/doctor/desktop-bridge-catalog-repair.js +20 -17
  34. package/dist/core/subagents/model-tiers.js +19 -3
  35. package/dist/core/update/update-migration-state/desktop-bridge-catalog-repair-stage.js +4 -3
  36. package/dist/core/version.js +1 -1
  37. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +0 -1
  38. package/dist/native/sks-menubar/Sources/OverviewSummary.swift +32 -0
  39. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +15 -5
  40. package/dist/native/sks-menubar/Sources/SidebarItem.swift +0 -2
  41. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +3 -0
  42. package/package.json +1 -1
  43. package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +0 -49
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
  Sneakoscope Codex (`sks`) is an open-source local tool for the Codex CLI and Codex desktop app. It adds a macOS control center, bounded agent workflows, repository context, and evidence checks for SKS releases.
18
18
  <!-- END SKS SEARCH VISIBILITY MARKETING -->
19
19
 
20
- Current package: **SKS 10.7.1**, prepared for npm publication.
20
+ Current package: **SKS 10.7.2**, prepared for npm publication.
21
21
  [Latest release notes](https://github.com/mandarange/Sneakoscope-Codex/releases/latest)
22
22
  · [npm package](https://www.npmjs.com/package/sneakoscope)
23
23
  · [Changelog](CHANGELOG.md). Install the latest stable release from npm.
@@ -259,7 +259,7 @@ dependencies = [
259
259
 
260
260
  [[package]]
261
261
  name = "sks-core"
262
- version = "10.7.1"
262
+ version = "10.7.2"
263
263
  dependencies = [
264
264
  "globset",
265
265
  "grep-matcher",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "10.7.1"
3
+ version = "10.7.2"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -273,7 +273,7 @@ const COMMAND_HELP_LITE = {
273
273
  },
274
274
  doctor: {
275
275
  usage: 'sks doctor [--fix] [--local-only] [--json] [--install-scope global|project]',
276
- description: 'Check and repair SKS generated files, while blocking setup if another Codex harness is detected.'
276
+ description: 'Check and repair SKS generated files, while blocking setup if another Codex harness is detected. Every run refreshes the Desktop Bridge model list; without --fix it writes only SKS\'s own catalog files, never ~/.codex/config.toml.'
277
277
  },
278
278
  git: {
279
279
  usage: 'sks git policy|install|status|doctor|precommit|publish-plan|summary [--json]',
@@ -1,6 +1,38 @@
1
1
  import path from 'node:path';
2
2
  import { ensureTrailingNewline, upsertTopLevelTomlString } from '../core/codex-runtime/codex-desktop-config-policy.js';
3
3
  import { escapeRegExp } from '../core/text/regex.js';
4
+ export function codexReadsBridgeModels(text) {
5
+ const source = String(text || '');
6
+ return topLevelHasLine(source, DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER)
7
+ && Boolean(topLevelTomlString(source, 'openai_base_url'))
8
+ && !topLevelTomlString(source, 'model_catalog_json');
9
+ }
10
+ export function hasUserOwnedModelCatalog(text) {
11
+ const source = String(text || '');
12
+ return Boolean(topLevelTomlString(source, 'model_catalog_json'))
13
+ && !topLevelHasLine(source, DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER);
14
+ }
15
+ export function setDesktopBridgeManagedModelCatalog(text, combinedCatalogPath) {
16
+ const source = String(text || '');
17
+ if (!topLevelHasLine(source, DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER) || hasUserOwnedModelCatalog(source))
18
+ return source;
19
+ if (combinedCatalogPath === null) {
20
+ if (!topLevelTomlString(source, 'model_catalog_json'))
21
+ return source;
22
+ return ensureTrailingNewline(removeTopLevelLine(removeTopLevelTomlKey(source, 'model_catalog_json'), DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER));
23
+ }
24
+ assertCombinedCatalogPath(combinedCatalogPath);
25
+ if (topLevelTomlString(source, 'model_catalog_json') === combinedCatalogPath)
26
+ return source;
27
+ const next = upsertTopLevelTomlString(source, 'model_catalog_json', combinedCatalogPath);
28
+ return ensureTrailingNewline(addTopLevelMarkerBeforeKey(next, 'model_catalog_json', DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER));
29
+ }
30
+ function assertCombinedCatalogPath(combinedCatalogPath) {
31
+ if (!combinedCatalogPath || !path.isAbsolute(combinedCatalogPath)
32
+ || path.basename(combinedCatalogPath) !== 'sks-bridge-catalog.json') {
33
+ throw new Error('desktop_bridge_combined_catalog_path_invalid');
34
+ }
35
+ }
4
36
  export const DESKTOP_BRIDGE_MANAGED_MARKER = '# sks-desktop-bridge-managed';
5
37
  export const DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER = '# sks-desktop-bridge-managed-base-url';
6
38
  export const DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER = '# sks-desktop-bridge-managed-model-catalog';
@@ -18,12 +50,8 @@ export function hasCodexLbProviderTable(text) {
18
50
  export function upsertDesktopBridgeManagedConfig(text, input) {
19
51
  const bridgeBaseUrl = normalizeManagedBridgeBaseUrl(input.bridgeBaseUrl);
20
52
  const combinedCatalogPath = String(input.combinedCatalogPath || '').trim();
21
- if (!combinedCatalogPath || !path.isAbsolute(combinedCatalogPath)) {
22
- throw new Error('desktop_bridge_combined_catalog_path_invalid');
23
- }
24
- if (path.basename(combinedCatalogPath) !== 'sks-bridge-catalog.json') {
25
- throw new Error('desktop_bridge_combined_catalog_path_invalid');
26
- }
53
+ assertCombinedCatalogPath(combinedCatalogPath);
54
+ const bindModelCatalog = input.bindModelCatalog !== false;
27
55
  const orphanCleanup = removeDesktopBridgeOrphanManagedMarkers(String(text || ''));
28
56
  let next = orphanCleanup.text;
29
57
  const selectedProvider = topLevelTomlString(next, 'model_provider');
@@ -39,17 +67,20 @@ export function upsertDesktopBridgeManagedConfig(text, input) {
39
67
  }
40
68
  const existingCatalog = topLevelTomlString(next, 'model_catalog_json');
41
69
  const catalogOwned = topLevelHasLine(next, DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER);
42
- if (existingCatalog && !catalogOwned) {
70
+ if (existingCatalog && !catalogOwned && bindModelCatalog) {
43
71
  throw new Error('historical_user_owned_config_conflict:model_catalog_json');
44
72
  }
73
+ const userCatalog = existingCatalog && !catalogOwned ? existingCatalog : null;
45
74
  const keepCodexLb = selectedProvider === 'codex-lb' && hasCodexLbProviderTable(next);
46
- next = removeManagedBridgeTopLevelBindings(next);
75
+ next = removeManagedBridgeTopLevelBindings(next, { keepModelCatalog: userCatalog !== null });
47
76
  next = upsertTopLevelTomlString(next, 'model_provider', keepCodexLb ? 'codex-lb' : 'openai');
48
77
  next = addTopLevelMarkerBeforeKey(next, 'model_provider', DESKTOP_BRIDGE_MANAGED_MARKER);
49
78
  next = upsertTopLevelTomlString(next, 'openai_base_url', bridgeBaseUrl);
50
79
  next = addTopLevelMarkerBeforeKey(next, 'openai_base_url', DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER);
51
- next = upsertTopLevelTomlString(next, 'model_catalog_json', combinedCatalogPath);
52
- next = addTopLevelMarkerBeforeKey(next, 'model_catalog_json', DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER);
80
+ if (bindModelCatalog) {
81
+ next = upsertTopLevelTomlString(next, 'model_catalog_json', combinedCatalogPath);
82
+ next = addTopLevelMarkerBeforeKey(next, 'model_catalog_json', DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER);
83
+ }
53
84
  return ensureTrailingNewline(next);
54
85
  }
55
86
  export function removeDesktopBridgeManagedConfig(text) {
@@ -135,11 +166,12 @@ function removeTopLevelTomlKey(text, key) {
135
166
  .replace(/^\n+/, '')
136
167
  .replace(/\n{3,}/g, '\n\n');
137
168
  }
138
- function removeManagedBridgeTopLevelBindings(text) {
169
+ function removeManagedBridgeTopLevelBindings(text, options = {}) {
139
170
  let next = String(text || '');
140
171
  next = removeTopLevelTomlKey(next, 'model_provider');
141
172
  next = removeTopLevelTomlKey(next, 'openai_base_url');
142
- next = removeTopLevelTomlKey(next, 'model_catalog_json');
173
+ if (!options.keepModelCatalog)
174
+ next = removeTopLevelTomlKey(next, 'model_catalog_json');
143
175
  for (const marker of [
144
176
  DESKTOP_BRIDGE_MANAGED_MARKER,
145
177
  DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER,
@@ -31,6 +31,7 @@ export function usage(command = 'bridge') {
31
31
  ` sks ${command} catalog sync|status [--json]`,
32
32
  ` sks ${command} models list [--json]`,
33
33
  ` sks ${command} models select --set <public-id,...> [--json]`,
34
+ ` sks ${command} models refresh [--json]`,
34
35
  ` sks ${command} route list [--json]`,
35
36
  ` sks ${command} route set-default <codex-lb|openrouter> [--json]`,
36
37
  ` sks ${command} route official-models <passthrough|gateway|auto> [--json]`,
@@ -86,7 +87,7 @@ export async function executeBridgeCommand(args = [], options = {}) {
86
87
  : await (options.facade || await loadBridgeCommandFacade()).execute(parsed.request);
87
88
  const output = parsed.request.operation === 'verify'
88
89
  ? verificationOutput(raw, parsed.request.level, parsed.strict)
89
- : ordinaryOutput(raw, parsed.label);
90
+ : ordinaryOutput(raw, parsed.request.operation === 'models.refresh' ? modelRefreshLine(raw) ?? parsed.label : parsed.label);
90
91
  const sanitized = sanitizeBridgeValue(output, parsed.secrets);
91
92
  const decorated = mergeMetadata(sanitized, options.metadata);
92
93
  return {
@@ -104,6 +105,10 @@ export async function executeBridgeCommand(args = [], options = {}) {
104
105
  };
105
106
  }
106
107
  }
108
+ function modelRefreshLine(raw) {
109
+ const result = raw?.result;
110
+ return typeof result?.model_refresh?.line === 'string' ? result.model_refresh.line : null;
111
+ }
107
112
  export async function loadBridgeCommandFacade() {
108
113
  const controller = await import('../core/codex-lb/desktop-controller.js');
109
114
  const execute = controller[CONTROLLER_FACADE_EXPORT];
@@ -260,6 +265,14 @@ async function parseInvocation(args, io) {
260
265
  allowOnly(parsed, ['--json'], []);
261
266
  return { ...base, request: { operation: 'models.list' }, label: 'Selectable bridge models' };
262
267
  }
268
+ if (action === 'refresh' && target === undefined) {
269
+ allowOnly(parsed, ['--json'], []);
270
+ return {
271
+ ...base,
272
+ request: { operation: 'models.refresh', reason: 'cli', force: true, allow_config_writes: true },
273
+ label: 'Model list refresh'
274
+ };
275
+ }
263
276
  if (action === 'select') {
264
277
  allowOnly(parsed, ['--json'], ['--set']);
265
278
  const raw = parsed.values.get('--set') || '';
@@ -45,7 +45,9 @@ export async function run(_command, args = []) {
45
45
  return;
46
46
  }
47
47
  if (action === 'update' || action === 'update-now') {
48
- const result = await updateCodexCliNow();
48
+ const updated = await updateCodexCliNow();
49
+ const modelRefresh = await (await import('../core/codex-lb/model-refresh-after.js')).refreshModelListAfter('codex-update');
50
+ const result = modelRefresh ? { ...updated, model_refresh: modelRefresh } : updated;
49
51
  if (flag(args, '--json')) {
50
52
  printJson(result);
51
53
  if (!result.ok)
@@ -54,6 +56,8 @@ export async function run(_command, args = []) {
54
56
  }
55
57
  for (const line of codexCliUpdateConsoleLines(result))
56
58
  console.log(line);
59
+ if (typeof modelRefresh?.line === 'string')
60
+ console.log(modelRefresh.line);
57
61
  if (!result.ok)
58
62
  process.exitCode = 1;
59
63
  return;
@@ -238,6 +238,8 @@ export function renderDoctorConsoleReport(result, extras) {
238
238
  if (codexCurrentAppDoctor.fixed?.length)
239
239
  line(` doctor --fix repaired: ${codexCurrentAppDoctor.fixed.join(', ')}`);
240
240
  line(`Desktop Bridge: ${desktopBridge.ok ? 'ready' : 'blocked'} (${desktopBridge.status?.readiness?.state || 'unavailable'})`);
241
+ if (desktopBridge.model_refresh?.line)
242
+ line(` ${desktopBridge.model_refresh.line}`);
241
243
  for (const providerId of ['codex-lb', 'openrouter']) {
242
244
  const provider = desktopBridge.providers?.[providerId];
243
245
  if (!provider)
@@ -376,6 +376,8 @@ async function runDoctorGlobalOnlyFix(args = [], root, deps = {}) {
376
376
  ? 'repaired'
377
377
  : bridgeRepair?.ok === false ? 'failed' : 'nothing_to_repair'}`);
378
378
  console.log(`Desktop Bridge: ${result.desktop_bridge.ok ? 'ready' : 'blocked'} (${result.desktop_bridge.status?.readiness?.state || 'unavailable'})`);
379
+ if (result.desktop_bridge_repair?.model_refresh?.line)
380
+ console.log(result.desktop_bridge_repair.model_refresh.line);
379
381
  if (result.migration_receipt) {
380
382
  console.log(`Migration receipt: ${result.migration_receipt.status} (${result.home})`);
381
383
  }
@@ -737,9 +739,8 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
737
739
  const codexApp = deepDiagnostics
738
740
  ? await codexAppIntegrationStatus({ codex }).catch((err) => ({ ok: false, error: err.message }))
739
741
  : { ok: false, skipped: true, warnings: ['codex_app_optional_diagnostic_skipped'] };
740
- const desktopBridgeBeforeFix = await inspectDoctorDesktopBridgeStatus({
741
- processEnv: process.env
742
- }, { desktopBridgeStatusImpl: deps.desktopBridgeStatusImpl });
742
+ let desktopBridgeModelRefresh = doctorFix ? null : (await (deps.desktopBridgeRepairImpl || repairDoctorDesktopBridgeCatalog)({ fix: false, reason: 'doctor' }).catch(() => null))?.model_refresh ?? null;
743
+ const desktopBridgeBeforeFix = await inspectDoctorDesktopBridgeStatus({ processEnv: process.env }, { desktopBridgeStatusImpl: deps.desktopBridgeStatusImpl });
743
744
  const providerContext = deepDiagnostics
744
745
  ? await resolveProviderContext({ root, route: '$Doctor', serviceTier: process.env.SKS_SERVICE_TIER || 'fast' }).catch((err) => ({
745
746
  schema: 'sks.provider-context.v2',
@@ -1091,7 +1092,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
1091
1092
  {
1092
1093
  id: 'desktop_bridge_catalog_repair',
1093
1094
  required_for_ready: false,
1094
- run: async () => repairDoctorDesktopBridgeCatalog({ fix: doctorFix })
1095
+ run: async () => { const repair = await repairDoctorDesktopBridgeCatalog({ fix: doctorFix }); desktopBridgeModelRefresh = repair.model_refresh ?? null; return repair; }
1095
1096
  }
1096
1097
  ].filter((phase) => doctorPhaseIds.includes(phase.id))
1097
1098
  }).catch((err) => ({
@@ -1118,11 +1119,9 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
1118
1119
  }))
1119
1120
  : null;
1120
1121
  const doctorFixPostcheck = doctorFix ? (await import('../core/doctor/doctor-repair-postcheck.js')).doctorRepairPostcheck(doctorFixTransaction) : null;
1121
- const desktopBridge = doctorFixTransaction
1122
- ? await inspectDoctorDesktopBridgeStatus({
1123
- processEnv: process.env
1124
- }, { desktopBridgeStatusImpl: deps.desktopBridgeStatusImpl })
1125
- : desktopBridgeBeforeFix;
1122
+ const desktopBridge = { ...(doctorFixTransaction
1123
+ ? await inspectDoctorDesktopBridgeStatus({ processEnv: process.env }, { desktopBridgeStatusImpl: deps.desktopBridgeStatusImpl })
1124
+ : desktopBridgeBeforeFix), model_refresh: desktopBridgeModelRefresh };
1126
1125
  const startupRoleRepair = startupConfigRepair?.role_repair;
1127
1126
  const agentRoleConfigRepair = doctorFix && startupRoleRepair
1128
1127
  ? startupRoleRepair
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "sks.skills-manifest.v1",
3
- "package_version": "10.7.1",
3
+ "package_version": "10.7.2",
4
4
  "skills": [
5
5
  {
6
6
  "canonical_name": "sks",
@@ -33,7 +33,6 @@ export const NATIVE_SOURCE_FILES = [
33
33
  'ProvidersOpenRouterOnly.swift',
34
34
  'SubagentModelsModels.swift',
35
35
  'SubagentModelsViewController.swift',
36
- 'RemoteCodingViewController.swift',
37
36
  'DiagnosticsViewController.swift',
38
37
  'SettingsViewController.swift',
39
38
  'LocalDecisionModels.swift',
@@ -43,6 +43,10 @@ export async function stageCombinedBridgeCatalog(input) {
43
43
  || existing.route_index.generation !== input.build.route_index.generation) {
44
44
  throw new Error('combined_catalog_generation_bundle_mismatch');
45
45
  }
46
+ if (JSON.stringify(existing.catalog.provider_statuses) !== JSON.stringify(input.build.catalog.provider_statuses)) {
47
+ await fs.rename(tempCatalog, layout.catalogPath);
48
+ await fsyncDirectory(layout.bundleDirectory);
49
+ }
46
50
  }
47
51
  else {
48
52
  await fs.rename(tempBundle, layout.bundleDirectory);
@@ -139,7 +139,10 @@ export function emptyRouteIndex() {
139
139
  }).route_index;
140
140
  }
141
141
  export function catalogObservationGeneration(catalog) {
142
- return sha256Stable(catalog.provider_statuses);
142
+ return sha256Stable(Object.fromEntries(Object.entries(catalog.provider_statuses).map(([providerId, row]) => {
143
+ const { checked_at: _checkedAt, expires_at: _expiresAt, ...observed } = row;
144
+ return [providerId, observed];
145
+ })));
143
146
  }
144
147
  function validProviderStatuses(value) {
145
148
  if (!value || typeof value !== 'object' || Array.isArray(value))
@@ -4,12 +4,14 @@ import zlib from 'node:zlib';
4
4
  import { pipeline } from 'node:stream/promises';
5
5
  import { BRIDGE_OFFICIAL_ROUTE_ID, BRIDGE_REMOTE_COMPACTION } from '../bridge-contracts.js';
6
6
  import { buildOfficialPassthroughHeaders, buildProviderUpstreamHeaders, rewriteResponseHeaders } from './header-policy.js';
7
- import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
7
+ import { createDesktopBridgeRejectionLogger, desktopBridgeRequestShape, safeUpstreamParam } from './rejection-log.js';
8
8
  import { OPENROUTER_ONLY_REFUSAL_CODES } from './exclusive-provider-guard.js';
9
9
  import { ensureDesktopBridgeRemoteTarget, isNoRouteUpstreamError, isResponsesFamilyPath, isUnreachableUpstreamError, pinnedPublicModel, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader, threadRouteMove } from './security.js';
10
10
  import { admitCodexLbTurnState, forgetCodexLbTurnStates, rememberCodexLbTurnState } from './codex-lb-turn-state.js';
11
11
  import { ProviderHandoffRefusedError, assertProviderHandoffAllowed, providerHandoffRefusalBody, stripProviderBoundItems, } from './provider-handoff.js';
12
12
  import { desktopBridgeListenOrigin } from './state.js';
13
+ import { adaptRequestForRoute, logRequestAdaptation, providerExtraInclude } from './provider-request-compat.js';
14
+ import { desktopBridgeModelsEtag } from './models-endpoint.js';
13
15
  import { buildCompactionSummaryRequest, compactEndpointResult, compactionCreatedEvent, compactionFailedEvent, compactionResponseEvents, expandBridgeCompactionItems, newCompactionResponseId, parseProviderSummary, requestsRemoteCompaction, safeUpstreamErrorId, sseEvent, summaryFailure, summaryWithinBounds, } from './compaction-adapter.js';
14
16
  import { MODEL_CAPACITY_ERROR_CODE, MODEL_CAPACITY_RETRY_AFTER_SECONDS, isAdvisableEventStream, modelCapacityAdvisingSse } from './capacity-retry-advice.js';
15
17
  import { DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES, DesktopBridgeError, DesktopBridgeRequestBodyTooLargeError, isDesktopBridgeRoutingConflict } from './types.js';
@@ -69,6 +71,12 @@ function modelBodyKind(req, pathname) {
69
71
  return req.method === 'POST' ? 'responses' : null;
70
72
  return req.method === 'POST' && RESPONSES_PATHS.some((prefix) => pathname.startsWith(`${prefix}/`)) ? 'sub-endpoint' : null;
71
73
  }
74
+ function advertisedModelsEtag(req, config) {
75
+ if (!config.modelsCatalog)
76
+ return null;
77
+ const pathname = new URL(String(req.url || '/'), 'http://bridge.invalid').pathname;
78
+ return modelBodyKind(req, pathname) === 'responses' ? desktopBridgeModelsEtag(config.modelsCatalog.generation) : null;
79
+ }
72
80
  function isCompactEndpoint(req, pathname) {
73
81
  return req.method === 'POST' && RESPONSES_PATHS.some((prefix) => pathname === `${prefix}/compact`);
74
82
  }
@@ -155,6 +163,11 @@ export async function prepareDesktopBridgeRequest(req, config) {
155
163
  rewrite = true;
156
164
  if (route.session_pin?.handoff_from && stripProviderBoundItems(payload, Boolean(move)))
157
165
  rewrite = true;
166
+ const adapted = adaptRequestForRoute(payload, route, providerExtraInclude(config, route));
167
+ if (adapted.length) {
168
+ rewrite = true;
169
+ logRequestAdaptation(config, route, adapted, 'http');
170
+ }
158
171
  if (payload && rewrite)
159
172
  body = Buffer.from(JSON.stringify(payload));
160
173
  const compaction = bodyKind === 'responses' && requestsRemoteCompaction(payload)
@@ -260,6 +273,7 @@ async function readRedactedUpstreamError(response) {
260
273
  }
261
274
  let upstreamCode = null;
262
275
  let upstreamType = null;
276
+ let upstreamParam = null;
263
277
  const raw = Buffer.concat(chunks);
264
278
  const encoding = String(response.headers['content-encoding'] || '').trim().toLowerCase();
265
279
  let decoded = complete ? raw : null;
@@ -276,12 +290,14 @@ async function readRedactedUpstreamError(response) {
276
290
  const parsed = JSON.parse(text);
277
291
  upstreamCode = safeUpstreamErrorId(parsed?.error?.code) ?? safeUpstreamErrorId(parsed?.detail);
278
292
  upstreamType = safeUpstreamErrorId(parsed?.error?.type);
293
+ upstreamParam = safeUpstreamParam(parsed?.error?.param);
279
294
  }
280
295
  catch {
281
296
  }
282
297
  return {
283
298
  upstreamCode,
284
299
  upstreamType,
300
+ upstreamParam,
285
301
  unreadableBody: UNREADABLE_BODY_HINT.test(text.slice(0, 4096)),
286
302
  body: decoded,
287
303
  };
@@ -400,6 +416,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
400
416
  delete headers['content-encoding'];
401
417
  const agent = upstreamAgent(remote.secure, `${remote.address}:${remote.port}`, config.idleTimeoutMs);
402
418
  const idleTimeoutMs = request.compaction ? Math.max(config.idleTimeoutMs, NATIVE_COMPACTION_IDLE_FLOOR_MS) : config.idleTimeoutMs;
419
+ const modelsEtag = advertisedModelsEtag(req, config);
403
420
  const replayable = Buffer.isBuffer(request.body);
404
421
  let body = request.body;
405
422
  let plainFallbackActive = false;
@@ -468,7 +485,10 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
468
485
  }
469
486
  responseStarted = true;
470
487
  try {
471
- res.writeHead(statusCode, rewriteResponseHeaders(response.headers, upstreamBaseUrl, authenticatedLocalBaseUrl));
488
+ const responseHeaders = rewriteResponseHeaders(response.headers, upstreamBaseUrl, authenticatedLocalBaseUrl);
489
+ if (modelsEtag)
490
+ responseHeaders['x-models-etag'] = modelsEtag;
491
+ res.writeHead(statusCode, responseHeaders);
472
492
  }
473
493
  catch (error) {
474
494
  response.destroy(error instanceof Error ? error : undefined);
@@ -479,7 +499,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
479
499
  return;
480
500
  }
481
501
  if (statusCode >= 400) {
482
- void readRedactedUpstreamError(response).then(({ upstreamCode, upstreamType, unreadableBody, body: upstreamBody }) => {
502
+ void readRedactedUpstreamError(response).then(({ upstreamCode, upstreamType, upstreamParam, unreadableBody, body: upstreamBody }) => {
483
503
  if (budget.plainFallback && (statusCode === 415
484
504
  || (unreadableBody && !upstreamType && !upstreamCode && (statusCode === 400 || statusCode === 422)))) {
485
505
  responseStarted = true;
@@ -499,16 +519,26 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
499
519
  const body = verbatim
500
520
  ? scrubUpstreamErrorBody(upstreamBody, request.credential.value)
501
521
  : buildRedactedUpstreamErrorBody(statusCode, upstreamCode, upstreamType, transientMislabel);
522
+ const rejected = statusCode >= 400 && statusCode <= 499;
502
523
  logHttpRejection({
503
- code: transientMislabel && !verbatim
504
- ? `bridge_upstream_status_${statusCode}_translated_503`
505
- : upstreamCode ? `bridge_upstream_status_${statusCode}:${upstreamCode}` : `bridge_upstream_status_${statusCode}`,
524
+ code: rejected
525
+ ? 'bridge_upstream_request_rejected'
526
+ : transientMislabel && !verbatim
527
+ ? `bridge_upstream_status_${statusCode}_translated_503`
528
+ : upstreamCode ? `bridge_upstream_status_${statusCode}:${upstreamCode}` : `bridge_upstream_status_${statusCode}`,
506
529
  transport: 'http',
507
530
  ...(req.method === undefined ? {} : { method: req.method }),
508
531
  ...(req.url === undefined ? {} : { url: req.url }),
509
532
  status: clientStatus,
510
533
  provider_id: provider.provider_id,
511
534
  public_model: request.route.public_model,
535
+ ...(rejected ? {
536
+ upstream_status: statusCode,
537
+ upstream_code: upstreamCode,
538
+ upstream_type: upstreamType,
539
+ upstream_param: upstreamParam,
540
+ request_shape: desktopBridgeRequestShape(request.payload),
541
+ } : {}),
512
542
  });
513
543
  responseStarted = true;
514
544
  const responseHeaders = rewriteResponseHeaders(response.headers, upstreamBaseUrl, authenticatedLocalBaseUrl);
@@ -518,6 +548,8 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
518
548
  if (verbatim && upstreamCode === MODEL_CAPACITY_ERROR_CODE && responseHeaders['retry-after'] === undefined) {
519
549
  responseHeaders['retry-after'] = String(MODEL_CAPACITY_RETRY_AFTER_SECONDS);
520
550
  }
551
+ if (modelsEtag)
552
+ responseHeaders['x-models-etag'] = modelsEtag;
521
553
  res.writeHead(clientStatus, responseHeaders);
522
554
  res.end(body, () => finish());
523
555
  }).catch(finish);
@@ -536,6 +568,8 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
536
568
  const responseHeaders = rewriteResponseHeaders(response.headers, upstreamBaseUrl, authenticatedLocalBaseUrl);
537
569
  if (advising)
538
570
  delete responseHeaders['content-length'];
571
+ if (modelsEtag)
572
+ responseHeaders['x-models-etag'] = modelsEtag;
539
573
  res.writeHead(statusCode, responseHeaders);
540
574
  }
541
575
  catch (error) {
@@ -715,12 +749,18 @@ async function answerCompactionWithProviderSummary(req, res, config, input) {
715
749
  const streaming = input.kind === 'trigger';
716
750
  const responseId = newCompactionResponseId();
717
751
  const publicModel = input.route.public_model;
718
- const body = Buffer.from(JSON.stringify(buildCompactionSummaryRequest(input.payload, input.route.upstream_model)));
752
+ const summaryRequest = buildCompactionSummaryRequest(input.payload, input.route.upstream_model);
753
+ adaptRequestForRoute(summaryRequest, input.route, providerExtraInclude(config, input.route));
754
+ const body = Buffer.from(JSON.stringify(summaryRequest));
719
755
  let keepalive = null;
720
756
  const startStream = () => {
721
757
  if (!streaming || res.headersSent)
722
758
  return;
723
- res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-store', connection: 'keep-alive' });
759
+ const modelsEtag = advertisedModelsEtag(req, config);
760
+ res.writeHead(200, {
761
+ 'content-type': 'text/event-stream', 'cache-control': 'no-store', connection: 'keep-alive',
762
+ ...(modelsEtag ? { 'x-models-etag': modelsEtag } : {}),
763
+ });
724
764
  res.write(sseEvent(compactionCreatedEvent(responseId, publicModel)));
725
765
  keepalive = setInterval(() => { if (!res.writableEnded)
726
766
  res.write(': keepalive\n\n'); }, COMPACTION_KEEPALIVE_MS);
@@ -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
+ }