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.
- 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-app.js +3 -3
- 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/codex-context-window.js +23 -8
- package/dist/core/codex-app/menubar/constants.js +0 -1
- package/dist/core/codex-control/model-call-concurrency.js +6 -5
- 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/capacity-retry-advice.js +35 -11
- 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
|
@@ -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)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
+
}
|