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.
- package/README.md +1 -1
- 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 +44 -12
- 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/codex-app/menubar/constants.js +0 -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/http-forward.js +48 -8
- package/dist/core/codex-lb/desktop-bridge/models-endpoint.js +62 -0
- package/dist/core/codex-lb/desktop-bridge/provider-handoff.js +13 -4
- 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 +52 -4
- package/dist/core/codex-lb/desktop-bridge/server.js +5 -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 +114 -5
- 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 -2
- 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 +41 -4
- package/dist/core/codex-lb/model-refresh-after.js +16 -0
- 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/subagents/model-tiers.js +19 -3
- package/dist/core/update/update-migration-state/desktop-bridge-catalog-repair-stage.js +4 -3
- 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 +1 -1
- package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +0 -49
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readText, writeJsonAtomic } from '../../fsx.js';
|
|
4
|
+
import { safeWriteCodexConfigToml } from '../../codex-runtime/codex-desktop-config-policy.js';
|
|
5
|
+
import { readTopLevelTomlString } from '../../codex-app/codex-model-catalog.js';
|
|
6
|
+
import { codexReadsBridgeModels, hasUserOwnedModelCatalog, setDesktopBridgeManagedModelCatalog } from '../../../cli/install-helpers-codex-lb-config.js';
|
|
7
|
+
import { captureCodexAuthSnapshot } from '../desktop-auth-invariant.js';
|
|
8
|
+
import { desktopBridgeModelsEtag } from '../desktop-bridge/models-endpoint.js';
|
|
9
|
+
export const MODEL_REFRESH_STATE_SCHEMA = 'sks.bridge-model-refresh-state.v1';
|
|
10
|
+
export const MODEL_REFRESH_DEBOUNCE_MS = 60_000;
|
|
11
|
+
export const MODEL_REFRESH_RECOVERY_COMMAND = 'sks bridge models refresh';
|
|
12
|
+
export function modelRefreshStatePath(home) {
|
|
13
|
+
return path.join(path.resolve(home), '.codex', 'sks', 'sks-bridge-model-refresh.json');
|
|
14
|
+
}
|
|
15
|
+
export async function readModelRefreshState(home) {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(await readText(modelRefreshStatePath(home), ''));
|
|
18
|
+
return parsed?.schema === MODEL_REFRESH_STATE_SCHEMA && typeof parsed.refreshed_at === 'string'
|
|
19
|
+
&& typeof parsed.provider_registry_generation === 'string' && typeof parsed.generation === 'string'
|
|
20
|
+
&& Number.isSafeInteger(parsed.total)
|
|
21
|
+
? parsed
|
|
22
|
+
: null;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function writeModelRefreshState(home, state) {
|
|
29
|
+
await writeJsonAtomic(modelRefreshStatePath(home), { schema: MODEL_REFRESH_STATE_SCHEMA, ...state }, { mode: 0o600 });
|
|
30
|
+
}
|
|
31
|
+
export function refreshedRecently(state, registryGeneration, nowMs) {
|
|
32
|
+
if (!state || state.provider_registry_generation !== registryGeneration)
|
|
33
|
+
return false;
|
|
34
|
+
const age = nowMs - Date.parse(state.refreshed_at);
|
|
35
|
+
return Number.isFinite(age) && age >= 0 && age < MODEL_REFRESH_DEBOUNCE_MS;
|
|
36
|
+
}
|
|
37
|
+
export function listedModelSlugs(catalog) {
|
|
38
|
+
if (!catalog)
|
|
39
|
+
return [];
|
|
40
|
+
return [...new Set(catalog.models
|
|
41
|
+
.filter((model) => !/^hid(?:e|den)$/i.test(String(model.visibility || 'list')))
|
|
42
|
+
.map((model) => String(model.slug || ''))
|
|
43
|
+
.filter(Boolean))].sort();
|
|
44
|
+
}
|
|
45
|
+
export function modelListDiff(before, after) {
|
|
46
|
+
const was = new Set(before);
|
|
47
|
+
const is = new Set(after);
|
|
48
|
+
return { added: after.filter((slug) => !was.has(slug)), removed: before.filter((slug) => !is.has(slug)) };
|
|
49
|
+
}
|
|
50
|
+
export function modelRefreshLine(input) {
|
|
51
|
+
const line = `Model list refreshed: ${input.total} models (+${input.added} −${input.removed}) generation ${input.generation.slice(0, 8)}`;
|
|
52
|
+
return input.restartCodex ? `${line}. Restart Codex to see the new model list.` : line;
|
|
53
|
+
}
|
|
54
|
+
export async function bridgeServesModels(baseUrl, catalog, options = {}) {
|
|
55
|
+
const expected = desktopBridgeModelsEtag(catalog.generation);
|
|
56
|
+
let failure = 'bridge_models_self_check_unreachable';
|
|
57
|
+
for (let attempt = 0; attempt < (options.attempts ?? 3); attempt += 1) {
|
|
58
|
+
if (attempt > 0)
|
|
59
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
60
|
+
failure = await new Promise((resolve) => {
|
|
61
|
+
let url;
|
|
62
|
+
try {
|
|
63
|
+
url = new URL(`${baseUrl.replace(/\/+$/, '')}/models`);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
resolve('bridge_models_self_check_url_invalid');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (url.protocol !== 'http:' || !['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname)) {
|
|
70
|
+
resolve('bridge_models_self_check_not_loopback');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const request = http.get(url, { timeout: options.timeoutMs ?? 3_000 }, (response) => {
|
|
74
|
+
const chunks = [];
|
|
75
|
+
response.on('data', (chunk) => chunks.push(chunk));
|
|
76
|
+
response.once('end', () => {
|
|
77
|
+
if (response.statusCode !== 200) {
|
|
78
|
+
resolve(`bridge_models_self_check_status_${response.statusCode}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (response.headers.etag !== expected) {
|
|
82
|
+
resolve('bridge_models_self_check_generation_mismatch');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const models = JSON.parse(Buffer.concat(chunks).toString('utf8')).models;
|
|
87
|
+
resolve(Array.isArray(models) && models.length === catalog.models.length ? null : 'bridge_models_self_check_rows_mismatch');
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
resolve('bridge_models_self_check_body_invalid');
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
response.once('error', () => resolve('bridge_models_self_check_unreachable'));
|
|
94
|
+
});
|
|
95
|
+
request.once('timeout', () => request.destroy());
|
|
96
|
+
request.once('error', () => resolve('bridge_models_self_check_unreachable'));
|
|
97
|
+
});
|
|
98
|
+
if (failure === null)
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return failure;
|
|
102
|
+
}
|
|
103
|
+
export async function alignCodexModelSource(input) {
|
|
104
|
+
const current = await readText(input.configPath, '');
|
|
105
|
+
if (hasUserOwnedModelCatalog(current)) {
|
|
106
|
+
return { source: 'user_model_catalog_json', changed: false, warnings: ['codex_model_list_pinned_by_user_model_catalog_json'] };
|
|
107
|
+
}
|
|
108
|
+
const baseUrl = readTopLevelTomlString(current, 'openai_base_url');
|
|
109
|
+
if (!baseUrl || (!codexReadsBridgeModels(current) && !readTopLevelTomlString(current, 'model_catalog_json'))) {
|
|
110
|
+
return { source: 'not_managed', changed: false, warnings: [] };
|
|
111
|
+
}
|
|
112
|
+
const auth = await captureCodexAuthSnapshot({ home: input.home, authPath: input.authPath }).catch(() => null);
|
|
113
|
+
const warnings = [];
|
|
114
|
+
let wantBridge = auth?.mode === 'chatgpt_oauth';
|
|
115
|
+
if (wantBridge) {
|
|
116
|
+
const failure = await input.selfCheck(baseUrl, input.catalog);
|
|
117
|
+
if (failure) {
|
|
118
|
+
warnings.push(failure);
|
|
119
|
+
wantBridge = false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!wantBridge && !input.catalogPath) {
|
|
123
|
+
return { source: codexReadsBridgeModels(current) ? 'bridge_models' : 'model_catalog_json', changed: false, warnings: [...warnings, 'codex_model_catalog_path_unavailable'] };
|
|
124
|
+
}
|
|
125
|
+
const next = setDesktopBridgeManagedModelCatalog(current, wantBridge ? null : input.catalogPath);
|
|
126
|
+
const source = wantBridge ? 'bridge_models' : 'model_catalog_json';
|
|
127
|
+
if (next === current)
|
|
128
|
+
return { source, changed: false, warnings };
|
|
129
|
+
if (!input.allowConfigWrites) {
|
|
130
|
+
return {
|
|
131
|
+
source: codexReadsBridgeModels(current) ? 'bridge_models' : 'model_catalog_json',
|
|
132
|
+
changed: false,
|
|
133
|
+
warnings: [...warnings, `codex_model_source_change_deferred:${source}`]
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const write = await safeWriteCodexConfigToml(input.configPath, current, next, 'desktop-bridge-model-source', { verifyUnchangedBeforeWrite: true });
|
|
137
|
+
if (!write.ok)
|
|
138
|
+
return { source: codexReadsBridgeModels(current) ? 'bridge_models' : 'model_catalog_json', changed: false, warnings: [...warnings, `codex_model_source_write_refused:${write.status}`] };
|
|
139
|
+
return { source, changed: true, warnings };
|
|
140
|
+
}
|
|
@@ -9,7 +9,7 @@ import { readActiveCombinedBridgeCatalog } from '../combined-catalog.js';
|
|
|
9
9
|
import { MAX_SELECTED_OPENROUTER_MODELS, bridgeAvailableModelsPath, readBridgeModelSelectionState, writeBridgeModelSelection } from '../combined-catalog/model-selection.js';
|
|
10
10
|
import { ensureUserReadOnlyListRole } from '../../subagents/read-only-list-role.js';
|
|
11
11
|
import { canonicalizeBridgeModelId } from '../route-index.js';
|
|
12
|
-
import {
|
|
12
|
+
import { syncAsRefresh } from './catalog.js';
|
|
13
13
|
import { hasOpenRouterRoute, openRouterOnlyStoreLocation } from './openrouter-only-catalog.js';
|
|
14
14
|
import { applyOpenRouterOnlyMainModel, openRouterOnlyMainModelTarget, restoreOpenRouterOnlyMainModel } from './openrouter-only-main-model.js';
|
|
15
15
|
import { openRouterOnlyProviderBlocker, openRouterOnlyStatusFromCore } from './openrouter-only-status.js';
|
|
@@ -116,10 +116,10 @@ async function enableOpenRouterOnly(noRestart, options) {
|
|
|
116
116
|
if (entries.length === 0) {
|
|
117
117
|
return finish('openrouter-only.set', false, ['openrouter_only_no_openrouter_models'], {}, options);
|
|
118
118
|
}
|
|
119
|
-
const sync = await
|
|
119
|
+
const sync = await syncAsRefresh({
|
|
120
120
|
...options,
|
|
121
121
|
settings: { ...(options.settings || {}), auth_priority_enabled: false }
|
|
122
|
-
}, {
|
|
122
|
+
}, 'openrouter-only.set', {
|
|
123
123
|
openRouterOnly: { ...before, enabled: true, subagent_models: entries },
|
|
124
124
|
onActivated: async () => {
|
|
125
125
|
await requireRoutableEntry(core.paths, { ...before, enabled: true, subagent_models: entries });
|
|
@@ -204,7 +204,7 @@ export async function turnOffOpenRouterOnly(options, behavior = {}) {
|
|
|
204
204
|
return { ok: true, changed: false, catalog_sync: null, main_model: null, blockers: [] };
|
|
205
205
|
}
|
|
206
206
|
const off = { ...before, enabled: false };
|
|
207
|
-
const sync = (commit) =>
|
|
207
|
+
const sync = (commit) => syncAsRefresh(options, 'openrouter-only.off', {
|
|
208
208
|
...(behavior.restartService === undefined ? {} : { restartService: behavior.restartService }),
|
|
209
209
|
openRouterOnly: off,
|
|
210
210
|
...(commit ? { onActivated: () => writeOpenRouterOnlyState({ enabled: false }, location) } : {})
|
|
@@ -319,7 +319,7 @@ export async function setSubagentModels(raw, noRestart, options, expectedProfile
|
|
|
319
319
|
}, options);
|
|
320
320
|
}
|
|
321
321
|
const exposedBefore = await exposedModelIds(paths);
|
|
322
|
-
const sync = await
|
|
322
|
+
const sync = await syncAsRefresh(options, 'subagent-models.set', {
|
|
323
323
|
openRouterOnly: { ...before, subagent_models: entries },
|
|
324
324
|
onActivated: async () => {
|
|
325
325
|
if (entries.length > 0)
|