sneakoscope 10.7.0 → 10.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -5
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/command-manifest-lite.js +1 -1
- package/dist/cli/install-helpers-codex-lb-config.js +55 -20
- package/dist/commands/bridge.js +14 -1
- package/dist/commands/codex.js +5 -1
- package/dist/commands/doctor-console.js +2 -0
- package/dist/commands/doctor.js +8 -9
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/agents/agent-role-config.js +6 -2
- package/dist/core/agents/sks-model-pin-hygiene.js +150 -0
- package/dist/core/codex/codex-config-guard.js +18 -13
- package/dist/core/codex-app/codex-app-ui-state-snapshot.js +7 -1
- package/dist/core/codex-app/menubar/constants.js +0 -1
- package/dist/core/codex-lb/bridge-contracts.js +5 -0
- package/dist/core/codex-lb/combined-catalog/builder.js +4 -2
- package/dist/core/codex-lb/combined-catalog/latest-generation.js +7 -34
- package/dist/core/codex-lb/combined-catalog/normalize.js +3 -1
- package/dist/core/codex-lb/combined-catalog/persistence.js +4 -0
- package/dist/core/codex-lb/combined-catalog/storage.js +4 -1
- package/dist/core/codex-lb/desktop-bridge/codex-lb-turn-state.js +35 -0
- package/dist/core/codex-lb/desktop-bridge/compaction-adapter.js +3 -0
- package/dist/core/codex-lb/desktop-bridge/header-policy.js +10 -4
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +138 -36
- package/dist/core/codex-lb/desktop-bridge/models-endpoint.js +62 -0
- package/dist/core/codex-lb/desktop-bridge/provider-handoff.js +88 -0
- package/dist/core/codex-lb/desktop-bridge/provider-request-compat.js +89 -0
- package/dist/core/codex-lb/desktop-bridge/rejection-log.js +61 -0
- package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +108 -16
- package/dist/core/codex-lb/desktop-bridge/security.js +101 -55
- package/dist/core/codex-lb/desktop-bridge/server.js +15 -2
- package/dist/core/codex-lb/desktop-bridge/types.js +3 -0
- package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +3 -0
- package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +2 -6
- package/dist/core/codex-lb/desktop-bridge-migration.js +14 -2
- package/dist/core/codex-lb/desktop-controller-v3/catalog.js +125 -3
- package/dist/core/codex-lb/desktop-controller-v3/model-refresh.js +140 -0
- package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +5 -5
- package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +14 -11
- package/dist/core/codex-lb/desktop-controller-v3/status.js +2 -3
- package/dist/core/codex-lb/desktop-controller-v3.js +3 -1
- package/dist/core/codex-lb/desktop-service.js +51 -17
- package/dist/core/codex-lb/model-refresh-after.js +16 -0
- package/dist/core/codex-lb/request-route-resolver.js +1 -1
- package/dist/core/codex-runtime/codex-desktop-config-policy.js +0 -1
- package/dist/core/commands/basic-cli.js +5 -0
- package/dist/core/commands/menubar-command.js +5 -1
- package/dist/core/doctor/desktop-bridge-catalog-repair.js +20 -17
- package/dist/core/hooks-runtime/jev-spawn-routing.js +1 -5
- package/dist/core/hooks-runtime/parent-orchestration-gate.js +6 -8
- package/dist/core/hooks-runtime.js +13 -9
- package/dist/core/init/skills.js +22 -3
- package/dist/core/provider/provider-context.js +4 -1
- package/dist/core/search/context.js +2 -2
- package/dist/core/subagents/model-tiers.js +19 -3
- package/dist/core/update/update-migration-state/desktop-bridge-catalog-repair-stage.js +4 -3
- package/dist/core/update/update-migration-state.js +10 -40
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +0 -1
- package/dist/native/sks-menubar/Sources/OverviewSummary.swift +32 -0
- package/dist/native/sks-menubar/Sources/OverviewViewController.swift +15 -5
- package/dist/native/sks-menubar/Sources/SidebarItem.swift +0 -2
- package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +3 -0
- package/package.json +2 -2
- package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +0 -49
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildBridgeRouteIndex, sha256Stable } from '../route-index.js';
|
|
2
2
|
import { COMBINED_BRIDGE_CATALOG_SCHEMA } from './contracts.js';
|
|
3
|
-
import { normalizeProviderCatalog, providerCatalogStatus, routeProviderState } from './normalize.js';
|
|
3
|
+
import { OPENROUTER_PRIORITY_BASE, normalizeProviderCatalog, providerCatalogStatus, routeProviderState } from './normalize.js';
|
|
4
4
|
import { pinCatalogToLatestGenerations } from './latest-generation.js';
|
|
5
5
|
import { compareModels, unique } from './shared.js';
|
|
6
6
|
import { applyBridgeModelSelection, availableModelRows, emptyBridgeModelSelection } from './model-selection.js';
|
|
@@ -11,7 +11,9 @@ export function buildCombinedBridgeCatalog(registry, options) {
|
|
|
11
11
|
openrouter: normalizeProviderCatalog(options.catalogs.openrouter)
|
|
12
12
|
};
|
|
13
13
|
const selection = options.selection || emptyBridgeModelSelection(createdAt);
|
|
14
|
-
|
|
14
|
+
let openRouterIndex = 0;
|
|
15
|
+
const allModels = pinCatalogToLatestGenerations([...normalized['codex-lb'].models, ...normalized.openrouter.models].sort(compareModels)).map((model) => model.provider_id === 'openrouter' ? { ...model, priority: OPENROUTER_PRIORITY_BASE + openRouterIndex++ } : model)
|
|
16
|
+
.sort((left, right) => left.priority - right.priority || left.slug.localeCompare(right.slug) || compareModels(left, right));
|
|
15
17
|
const availableOpenRouterModels = availableModelRows(allModels, selection);
|
|
16
18
|
const models = options.selection ? applyBridgeModelSelection(allModels, selection) : allModels;
|
|
17
19
|
const routeBuild = buildBridgeRouteIndex({
|
|
@@ -3,13 +3,6 @@ function isListed(model) {
|
|
|
3
3
|
const visibility = String(model.visibility || 'list').toLowerCase();
|
|
4
4
|
return visibility !== 'hide' && visibility !== 'hidden';
|
|
5
5
|
}
|
|
6
|
-
function escapeRegExp(text) {
|
|
7
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8
|
-
}
|
|
9
|
-
function displayNamePattern(displayName) {
|
|
10
|
-
const body = escapeRegExp(displayName.trim()).replace(/\\?-|\s+/g, '[-\\s]');
|
|
11
|
-
return new RegExp(`(?<![\\w-])${body}(?![\\w-])`, 'gi');
|
|
12
|
-
}
|
|
13
6
|
export function pinCatalogToLatestGenerations(models) {
|
|
14
7
|
const newest = new Map();
|
|
15
8
|
for (const model of models) {
|
|
@@ -20,34 +13,14 @@ export function pinCatalogToLatestGenerations(models) {
|
|
|
20
13
|
if (!best || compareModelVersions(id.version, parseGptModelId(best.public_id).version) > 0)
|
|
21
14
|
newest.set(id.family, model);
|
|
22
15
|
}
|
|
23
|
-
const byId = new Map(models.map((model) => [model.public_id, model]));
|
|
24
16
|
return models.map((model) => {
|
|
25
17
|
const id = parseGptModelId(model.public_id);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const target = upgrade && typeof upgrade === 'object' ? String(upgrade.model || '') : '';
|
|
34
|
-
const targetId = parseGptModelId(target);
|
|
35
|
-
const replacement = targetId ? newest.get(targetId.family) : undefined;
|
|
36
|
-
if (targetId && replacement && replacement.public_id !== target
|
|
37
|
-
&& compareModelVersions(parseGptModelId(replacement.public_id).version, targetId.version) > 0) {
|
|
38
|
-
const old = byId.get(target);
|
|
39
|
-
const markdown = upgrade.migration_markdown;
|
|
40
|
-
next = {
|
|
41
|
-
...next,
|
|
42
|
-
upgrade: {
|
|
43
|
-
...upgrade,
|
|
44
|
-
model: replacement.public_id,
|
|
45
|
-
...(typeof markdown === 'string' && old?.display_name
|
|
46
|
-
? { migration_markdown: markdown.replace(displayNamePattern(old.display_name), () => replacement.display_name) }
|
|
47
|
-
: {})
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
return next;
|
|
18
|
+
if (!id || !isListed(model))
|
|
19
|
+
return model;
|
|
20
|
+
const best = newest.get(id.family);
|
|
21
|
+
if (!best || compareModelVersions(parseGptModelId(best.public_id).version, id.version) <= 0)
|
|
22
|
+
return model;
|
|
23
|
+
const { upgrade: _retired, ...rest } = model;
|
|
24
|
+
return { ...rest, visibility: 'hide' };
|
|
52
25
|
});
|
|
53
26
|
}
|
|
@@ -130,7 +130,7 @@ function normalizeCodexLbBridgeCatalogModels(payload, sourceCatalogGeneration) {
|
|
|
130
130
|
...codexModelInfoFields(publicId, row, {
|
|
131
131
|
tools: row.supports_tools === true || row.supports_tool_choice === true
|
|
132
132
|
}),
|
|
133
|
-
...(typeof row.priority === 'number' ? {} : { priority:
|
|
133
|
+
...(typeof row.priority === 'number' ? {} : { priority: CODEX_LB_DEFAULT_PRIORITY })
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
136
|
catch (error) {
|
|
@@ -164,6 +164,8 @@ export const OPENROUTER_REASONING_LEVELS = [
|
|
|
164
164
|
{ effort: 'xhigh', description: 'Extra high reasoning depth for complex problems' }
|
|
165
165
|
];
|
|
166
166
|
export const OPENROUTER_DEFAULT_REASONING_LEVEL = 'medium';
|
|
167
|
+
export const CODEX_LB_DEFAULT_PRIORITY = 500;
|
|
168
|
+
export const OPENROUTER_PRIORITY_BASE = 1000;
|
|
167
169
|
function openRouterReasoningLevels() {
|
|
168
170
|
return OPENROUTER_REASONING_LEVELS.map((level) => ({ ...level }));
|
|
169
171
|
}
|
|
@@ -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))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const MAX_THREADS = 4_096;
|
|
2
|
+
const MAX_VALUES_PER_THREAD = 8;
|
|
3
|
+
const issued = new Map();
|
|
4
|
+
function scope(threadId, keyFingerprint) {
|
|
5
|
+
return `${keyFingerprint}\u0000${threadId}`;
|
|
6
|
+
}
|
|
7
|
+
function headerValues(value) {
|
|
8
|
+
const values = Array.isArray(value) ? value : value === undefined ? [] : [String(value)];
|
|
9
|
+
return values.map((entry) => entry.trim()).filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
export function rememberCodexLbTurnState(threadId, keyFingerprint, headers) {
|
|
12
|
+
const values = headerValues(headers['x-codex-turn-state']);
|
|
13
|
+
if (!threadId || !values.length)
|
|
14
|
+
return;
|
|
15
|
+
const key = scope(threadId, keyFingerprint);
|
|
16
|
+
const kept = (issued.get(key) ?? []).filter((value) => !values.includes(value));
|
|
17
|
+
issued.delete(key);
|
|
18
|
+
issued.set(key, [...kept, ...values].slice(-MAX_VALUES_PER_THREAD));
|
|
19
|
+
while (issued.size > MAX_THREADS)
|
|
20
|
+
issued.delete(issued.keys().next().value);
|
|
21
|
+
}
|
|
22
|
+
export function admitCodexLbTurnState(headers, threadId, keyFingerprint) {
|
|
23
|
+
const values = headerValues(headers['x-codex-turn-state']);
|
|
24
|
+
if (!values.length) {
|
|
25
|
+
delete headers['x-codex-turn-state'];
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const known = threadId ? issued.get(scope(threadId, keyFingerprint)) : undefined;
|
|
29
|
+
if (values.length === 1 && known?.includes(values[0]))
|
|
30
|
+
return;
|
|
31
|
+
delete headers['x-codex-turn-state'];
|
|
32
|
+
}
|
|
33
|
+
export function forgetCodexLbTurnStates() {
|
|
34
|
+
issued.clear();
|
|
35
|
+
}
|
|
@@ -43,6 +43,9 @@ export function decodeBridgeCompaction(encrypted) {
|
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
export function isNativeCompactionItem(item) {
|
|
47
|
+
return isObject(item) && COMPACTION_ITEM_TYPES.has(String(item.type)) && decodeBridgeCompaction(item.encrypted_content) === null;
|
|
48
|
+
}
|
|
46
49
|
function summaryMessage(summary) {
|
|
47
50
|
return { type: 'message', role: 'user', content: [{ type: 'input_text', text: `${CODEX_SUMMARY_PREFIX}\n${summary}` }] };
|
|
48
51
|
}
|
|
@@ -7,9 +7,10 @@ const REQUEST_HEADER_ALLOWLIST = new Set([
|
|
|
7
7
|
]);
|
|
8
8
|
const CODEX_LOCAL_HEADERS = new Set([
|
|
9
9
|
'thread-id', 'thread_id', 'session-id', 'session_id', 'x-client-request-id',
|
|
10
|
-
'x-codex-installation-id', 'x-codex-turn-metadata', 'x-codex-window-id',
|
|
10
|
+
'x-codex-installation-id', 'x-codex-turn-metadata', 'x-codex-window-id', 'x-codex-turn-state',
|
|
11
11
|
'originator', 'openai-beta',
|
|
12
12
|
]);
|
|
13
|
+
const CLIENT_IDENTITY_HEADERS = new Set(['authorization', 'cookie', 'chatgpt-account-id']);
|
|
13
14
|
const NEVER_FORWARD_FROM_CLIENT = new Set([
|
|
14
15
|
'authorization', 'cookie', 'forwarded', 'proxy-authorization', 'x-api-key', 'x-codex-lb-api-key',
|
|
15
16
|
'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-port', 'x-forwarded-proto', 'x-real-ip',
|
|
@@ -39,14 +40,19 @@ function injectCredential(result, providerId, transport, credential) {
|
|
|
39
40
|
result.authorization = `Bearer ${credential.value}`;
|
|
40
41
|
}
|
|
41
42
|
export function buildProviderUpstreamHeaders(inbound, context, upstreamHost) {
|
|
43
|
+
if (context.providerId === 'codex-lb') {
|
|
44
|
+
const result = buildOfficialPassthroughHeaders(inbound, upstreamHost);
|
|
45
|
+
for (const name of CLIENT_IDENTITY_HEADERS)
|
|
46
|
+
delete result[name];
|
|
47
|
+
injectCredential(result, context.providerId, context.authTransport, context.credential);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
42
50
|
const result = {};
|
|
43
51
|
for (const [rawName, rawValue] of Object.entries(inbound)) {
|
|
44
52
|
if (rawValue === undefined)
|
|
45
53
|
continue;
|
|
46
54
|
const name = rawName.toLowerCase();
|
|
47
|
-
|
|
48
|
-
|| (context.providerId === 'codex-lb' && CODEX_LOCAL_HEADERS.has(name));
|
|
49
|
-
if (!REQUEST_HEADER_ALLOWLIST.has(name) && !providerAllowed)
|
|
55
|
+
if (!REQUEST_HEADER_ALLOWLIST.has(name) && !(context.providerId === 'openrouter' && OPENROUTER_CLIENT_HEADERS.has(name)))
|
|
50
56
|
continue;
|
|
51
57
|
result[name] = rawValue;
|
|
52
58
|
}
|
|
@@ -2,16 +2,21 @@ import http, {} from 'node:http';
|
|
|
2
2
|
import https from 'node:https';
|
|
3
3
|
import zlib from 'node:zlib';
|
|
4
4
|
import { pipeline } from 'node:stream/promises';
|
|
5
|
-
import { BRIDGE_OFFICIAL_ROUTE_ID } from '../bridge-contracts.js';
|
|
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';
|
|
8
|
-
import { OPENROUTER_ONLY_REFUSAL_CODES
|
|
9
|
-
import { ensureDesktopBridgeRemoteTarget, isNoRouteUpstreamError, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader } from './security.js';
|
|
7
|
+
import { createDesktopBridgeRejectionLogger, desktopBridgeRequestShape, safeUpstreamParam } from './rejection-log.js';
|
|
8
|
+
import { OPENROUTER_ONLY_REFUSAL_CODES } from './exclusive-provider-guard.js';
|
|
9
|
+
import { ensureDesktopBridgeRemoteTarget, isNoRouteUpstreamError, isResponsesFamilyPath, isUnreachableUpstreamError, pinnedPublicModel, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader, threadRouteMove } from './security.js';
|
|
10
|
+
import { admitCodexLbTurnState, forgetCodexLbTurnStates, rememberCodexLbTurnState } from './codex-lb-turn-state.js';
|
|
11
|
+
import { ProviderHandoffRefusedError, assertProviderHandoffAllowed, providerHandoffRefusalBody, stripProviderBoundItems, } from './provider-handoff.js';
|
|
10
12
|
import { desktopBridgeListenOrigin } from './state.js';
|
|
13
|
+
import { adaptRequestForRoute, logRequestAdaptation, providerExtraInclude } from './provider-request-compat.js';
|
|
14
|
+
import { desktopBridgeModelsEtag } from './models-endpoint.js';
|
|
11
15
|
import { buildCompactionSummaryRequest, compactEndpointResult, compactionCreatedEvent, compactionFailedEvent, compactionResponseEvents, expandBridgeCompactionItems, newCompactionResponseId, parseProviderSummary, requestsRemoteCompaction, safeUpstreamErrorId, sseEvent, summaryFailure, summaryWithinBounds, } from './compaction-adapter.js';
|
|
12
|
-
import { isAdvisableEventStream, modelCapacityAdvisingSse } from './capacity-retry-advice.js';
|
|
13
|
-
import { DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES, DesktopBridgeError, DesktopBridgeRequestBodyTooLargeError } from './types.js';
|
|
16
|
+
import { MODEL_CAPACITY_ERROR_CODE, MODEL_CAPACITY_RETRY_AFTER_SECONDS, isAdvisableEventStream, modelCapacityAdvisingSse } from './capacity-retry-advice.js';
|
|
17
|
+
import { DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES, DesktopBridgeError, DesktopBridgeRequestBodyTooLargeError, isDesktopBridgeRoutingConflict } from './types.js';
|
|
14
18
|
const MAX_UPSTREAM_ERROR_BODY_BYTES = 1024 * 1024;
|
|
19
|
+
const NATIVE_COMPACTION_IDLE_FLOOR_MS = 300_000;
|
|
15
20
|
function decodeBridgeRequestBody(body, encoding, maximum) {
|
|
16
21
|
const options = { maxOutputLength: maximum };
|
|
17
22
|
try {
|
|
@@ -61,11 +66,16 @@ async function reencodeLikeClient(plain, encoding) {
|
|
|
61
66
|
return new Promise((resolve) => compress(plain, (error, result) => resolve(error ? null : result)));
|
|
62
67
|
}
|
|
63
68
|
const RESPONSES_PATHS = ['/backend-api/codex/responses', '/api/v1/responses', '/v1/responses'];
|
|
64
|
-
function modelBodyKind(req, pathname
|
|
69
|
+
function modelBodyKind(req, pathname) {
|
|
65
70
|
if (RESPONSES_PATHS.includes(pathname))
|
|
66
71
|
return req.method === 'POST' ? 'responses' : null;
|
|
67
|
-
return
|
|
68
|
-
|
|
72
|
+
return req.method === 'POST' && RESPONSES_PATHS.some((prefix) => pathname.startsWith(`${prefix}/`)) ? 'sub-endpoint' : null;
|
|
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;
|
|
69
79
|
}
|
|
70
80
|
function isCompactEndpoint(req, pathname) {
|
|
71
81
|
return req.method === 'POST' && RESPONSES_PATHS.some((prefix) => pathname === `${prefix}/compact`);
|
|
@@ -105,7 +115,7 @@ export async function prepareDesktopBridgeRequest(req, config) {
|
|
|
105
115
|
let contentEncodingStripped = false;
|
|
106
116
|
let encoding = '';
|
|
107
117
|
let encodedBody = null;
|
|
108
|
-
const bodyKind = modelBodyKind(req, pathname
|
|
118
|
+
const bodyKind = modelBodyKind(req, pathname);
|
|
109
119
|
if (bodyKind) {
|
|
110
120
|
const maximum = config.maxRequestBodyBytes ?? DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES;
|
|
111
121
|
body = await readBoundedBody(req, maximum);
|
|
@@ -131,12 +141,19 @@ export async function prepareDesktopBridgeRequest(req, config) {
|
|
|
131
141
|
body = decoded;
|
|
132
142
|
}
|
|
133
143
|
const headerModel = singleBridgeHeader(req.headers, 'x-sks-model');
|
|
134
|
-
const model = typeof payload?.model === 'string' ? payload.model : headerModel;
|
|
135
144
|
const sessionIdentity = resolveCodexSessionIdentity(req.headers, payload);
|
|
136
|
-
const
|
|
145
|
+
const model = typeof payload?.model === 'string'
|
|
146
|
+
? payload.model
|
|
147
|
+
: headerModel || (isResponsesFamilyPath(pathname) ? pinnedPublicModel(sessionIdentity.thread_id, config) : null);
|
|
148
|
+
const routeRequest = {
|
|
137
149
|
public_model: String(model || ''), session_id: sessionIdentity.thread_id,
|
|
138
150
|
pathname, transport: 'http', headers: req.headers, identity: sessionIdentity,
|
|
139
|
-
}
|
|
151
|
+
};
|
|
152
|
+
let move = null;
|
|
153
|
+
const route = await resolveAndBindDesktopBridgeRouteContext(routeRequest, config, (resolved) => {
|
|
154
|
+
move = threadRouteMove(routeRequest, resolved, config);
|
|
155
|
+
assertProviderHandoffAllowed(payload, move);
|
|
156
|
+
});
|
|
140
157
|
let rewrite = false;
|
|
141
158
|
if (payload && (bodyKind === 'responses' || typeof payload.model === 'string') && payload.model !== route.upstream_model) {
|
|
142
159
|
payload.model = route.upstream_model;
|
|
@@ -144,14 +161,20 @@ export async function prepareDesktopBridgeRequest(req, config) {
|
|
|
144
161
|
}
|
|
145
162
|
if (expandBridgeCompactionItems(payload))
|
|
146
163
|
rewrite = true;
|
|
164
|
+
if (route.session_pin?.handoff_from && stripProviderBoundItems(payload, Boolean(move)))
|
|
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
|
+
}
|
|
147
171
|
if (payload && rewrite)
|
|
148
172
|
body = Buffer.from(JSON.stringify(payload));
|
|
149
173
|
const compaction = bodyKind === 'responses' && requestsRemoteCompaction(payload)
|
|
150
174
|
? 'trigger'
|
|
151
175
|
: bodyKind === 'sub-endpoint' && isCompactEndpoint(req, pathname) ? 'endpoint' : null;
|
|
152
176
|
let plainBody = null;
|
|
153
|
-
|
|
154
|
-
if (encodedBody && body && !answeredByBridge && keepsClientEncoding(route)) {
|
|
177
|
+
if (encodedBody && body && !answeredByBridge(compaction, route) && keepsClientEncoding(route)) {
|
|
155
178
|
const upstreamBody = rewrite ? await reencodeLikeClient(body, encoding) : encodedBody;
|
|
156
179
|
if (upstreamBody) {
|
|
157
180
|
plainBody = body;
|
|
@@ -160,7 +183,10 @@ export async function prepareDesktopBridgeRequest(req, config) {
|
|
|
160
183
|
}
|
|
161
184
|
}
|
|
162
185
|
const credential = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID ? null : await resolveCredential(config, route);
|
|
163
|
-
return { body, route, credential, contentEncodingStripped, plainBody, compaction, payload };
|
|
186
|
+
return { body, route, credential, contentEncodingStripped, plainBody, compaction, payload, threadId: sessionIdentity.thread_id, move };
|
|
187
|
+
}
|
|
188
|
+
function answeredByBridge(compaction, route) {
|
|
189
|
+
return Boolean(compaction) && BRIDGE_REMOTE_COMPACTION[route.provider_id] === 'bridge_summary';
|
|
164
190
|
}
|
|
165
191
|
function connectTimeout(request, config) {
|
|
166
192
|
request.once('socket', (socket) => {
|
|
@@ -191,8 +217,12 @@ function writeHttpBridgeError(res, error, req) {
|
|
|
191
217
|
res.destroy(error instanceof Error ? error : undefined);
|
|
192
218
|
return;
|
|
193
219
|
}
|
|
194
|
-
|
|
195
|
-
|
|
220
|
+
if (error instanceof ProviderHandoffRefusedError) {
|
|
221
|
+
res.writeHead(400, { 'content-type': 'application/json', 'cache-control': 'no-store', connection: 'close' });
|
|
222
|
+
res.end(providerHandoffRefusalBody(error));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
res.writeHead(isDesktopBridgeRoutingConflict(code) || OPENROUTER_ONLY_REFUSAL_CODES.has(code) ? 409 : 502, {
|
|
196
226
|
'content-type': 'application/json', 'cache-control': 'no-store', connection: 'close',
|
|
197
227
|
});
|
|
198
228
|
res.end(JSON.stringify({ error: { type: 'sks_bridge_error', code, message: code } }));
|
|
@@ -229,27 +259,57 @@ function redactedUpstreamClientMessage(statusCode, transient) {
|
|
|
229
259
|
const UNREADABLE_BODY_HINT = /json|pars(?:e|ing)|decod|encod|compress|utf-?8/i;
|
|
230
260
|
async function readRedactedUpstreamError(response) {
|
|
231
261
|
let total = 0;
|
|
262
|
+
let complete = true;
|
|
232
263
|
const chunks = [];
|
|
233
264
|
for await (const raw of response) {
|
|
234
265
|
const chunk = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
235
266
|
total += chunk.length;
|
|
236
267
|
if (total > MAX_UPSTREAM_ERROR_BODY_BYTES) {
|
|
237
268
|
response.destroy();
|
|
269
|
+
complete = false;
|
|
238
270
|
break;
|
|
239
271
|
}
|
|
240
272
|
chunks.push(chunk);
|
|
241
273
|
}
|
|
242
274
|
let upstreamCode = null;
|
|
243
275
|
let upstreamType = null;
|
|
244
|
-
|
|
276
|
+
let upstreamParam = null;
|
|
277
|
+
const raw = Buffer.concat(chunks);
|
|
278
|
+
const encoding = String(response.headers['content-encoding'] || '').trim().toLowerCase();
|
|
279
|
+
let decoded = complete ? raw : null;
|
|
280
|
+
if (decoded && encoding && encoding !== 'identity') {
|
|
281
|
+
try {
|
|
282
|
+
decoded = decodeBridgeRequestBody(decoded, encoding, 4 * MAX_UPSTREAM_ERROR_BODY_BYTES);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
decoded = null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const text = (decoded ?? raw).toString('utf8');
|
|
245
289
|
try {
|
|
246
290
|
const parsed = JSON.parse(text);
|
|
247
291
|
upstreamCode = safeUpstreamErrorId(parsed?.error?.code) ?? safeUpstreamErrorId(parsed?.detail);
|
|
248
292
|
upstreamType = safeUpstreamErrorId(parsed?.error?.type);
|
|
293
|
+
upstreamParam = safeUpstreamParam(parsed?.error?.param);
|
|
249
294
|
}
|
|
250
295
|
catch {
|
|
251
296
|
}
|
|
252
|
-
return {
|
|
297
|
+
return {
|
|
298
|
+
upstreamCode,
|
|
299
|
+
upstreamType,
|
|
300
|
+
upstreamParam,
|
|
301
|
+
unreadableBody: UNREADABLE_BODY_HINT.test(text.slice(0, 4096)),
|
|
302
|
+
body: decoded,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function scrubUpstreamErrorBody(body, credential) {
|
|
306
|
+
let text = body.toString('utf8');
|
|
307
|
+
if (credential)
|
|
308
|
+
text = text.split(credential).join('[REDACTED]');
|
|
309
|
+
text = text
|
|
310
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/-]{8,}=*/gi, 'Bearer [REDACTED]')
|
|
311
|
+
.replace(/\bsk-[A-Za-z0-9_-]{16,}/g, '[REDACTED]');
|
|
312
|
+
return Buffer.from(text, 'utf8');
|
|
253
313
|
}
|
|
254
314
|
function buildRedactedUpstreamErrorBody(statusCode, upstreamCode, upstreamType, transient) {
|
|
255
315
|
return Buffer.from(JSON.stringify({
|
|
@@ -284,6 +344,7 @@ export function destroyDesktopBridgeUpstreamAgents() {
|
|
|
284
344
|
agent.destroy();
|
|
285
345
|
upstreamAgents.clear();
|
|
286
346
|
encodedBodyRefusals.clear();
|
|
347
|
+
forgetCodexLbTurnStates();
|
|
287
348
|
}
|
|
288
349
|
const STALE_CONNECTION_ERROR_CODES = new Set(['ECONNRESET', 'EPIPE', 'ECONNABORTED']);
|
|
289
350
|
function isStalePooledSocketFailure(request, error) {
|
|
@@ -319,7 +380,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
319
380
|
if (!official && !request.credential)
|
|
320
381
|
throw new DesktopBridgeError('bridge_provider_credential_invalid');
|
|
321
382
|
await ensureDesktopBridgeRemoteTarget(remote, config.remoteLookup);
|
|
322
|
-
if (!official && request.compaction && request.payload) {
|
|
383
|
+
if (!official && request.compaction && answeredByBridge(request.compaction, request.route) && request.payload) {
|
|
323
384
|
await answerCompactionWithProviderSummary(req, res, config, {
|
|
324
385
|
kind: request.compaction,
|
|
325
386
|
payload: request.payload,
|
|
@@ -341,6 +402,12 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
341
402
|
: buildProviderUpstreamHeaders(req.headers, {
|
|
342
403
|
providerId: provider.provider_id, authTransport: provider.auth_transport, credential: request.credential,
|
|
343
404
|
}, target.host);
|
|
405
|
+
const codexLb = provider?.provider_id === 'codex-lb';
|
|
406
|
+
const threadId = request.threadId ?? null;
|
|
407
|
+
if (request.move)
|
|
408
|
+
delete headers['x-codex-turn-state'];
|
|
409
|
+
else if (codexLb)
|
|
410
|
+
admitCodexLbTurnState(headers, threadId, request.credential.fingerprint);
|
|
344
411
|
if (request.body)
|
|
345
412
|
headers['content-length'] = String(request.body.length);
|
|
346
413
|
else
|
|
@@ -348,6 +415,8 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
348
415
|
if (request.contentEncodingStripped)
|
|
349
416
|
delete headers['content-encoding'];
|
|
350
417
|
const agent = upstreamAgent(remote.secure, `${remote.address}:${remote.port}`, config.idleTimeoutMs);
|
|
418
|
+
const idleTimeoutMs = request.compaction ? Math.max(config.idleTimeoutMs, NATIVE_COMPACTION_IDLE_FLOOR_MS) : config.idleTimeoutMs;
|
|
419
|
+
const modelsEtag = advertisedModelsEtag(req, config);
|
|
351
420
|
const replayable = Buffer.isBuffer(request.body);
|
|
352
421
|
let body = request.body;
|
|
353
422
|
let plainFallbackActive = false;
|
|
@@ -371,7 +440,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
371
440
|
...(remote.tlsServername ? { servername: remote.tlsServername } : {}),
|
|
372
441
|
});
|
|
373
442
|
connectTimeout(upstream, config);
|
|
374
|
-
upstream.setTimeout(
|
|
443
|
+
upstream.setTimeout(idleTimeoutMs, () => upstream.destroy(new DesktopBridgeError('bridge_upstream_idle_timeout')));
|
|
375
444
|
req.once('aborted', abort);
|
|
376
445
|
res.once('close', abort);
|
|
377
446
|
upstream.once('error', (error) => {
|
|
@@ -390,6 +459,8 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
390
459
|
});
|
|
391
460
|
upstream.once('response', (response) => {
|
|
392
461
|
const statusCode = response.statusCode || 502;
|
|
462
|
+
if (codexLb)
|
|
463
|
+
rememberCodexLbTurnState(threadId, request.credential.fingerprint, response.headers);
|
|
393
464
|
if (plainFallbackActive && statusCode < 400 && !official && !encodedBodyRefusals.has(provider.provider_id)) {
|
|
394
465
|
encodedBodyRefusals.add(provider.provider_id);
|
|
395
466
|
logHttpRejection({ code: 'bridge_upstream_encoded_body_refused', transport: 'http', ...(req.method === undefined ? {} : { method: req.method }), ...(req.url === undefined ? {} : { url: req.url }), provider_id: provider.provider_id });
|
|
@@ -414,7 +485,10 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
414
485
|
}
|
|
415
486
|
responseStarted = true;
|
|
416
487
|
try {
|
|
417
|
-
|
|
488
|
+
const responseHeaders = rewriteResponseHeaders(response.headers, upstreamBaseUrl, authenticatedLocalBaseUrl);
|
|
489
|
+
if (modelsEtag)
|
|
490
|
+
responseHeaders['x-models-etag'] = modelsEtag;
|
|
491
|
+
res.writeHead(statusCode, responseHeaders);
|
|
418
492
|
}
|
|
419
493
|
catch (error) {
|
|
420
494
|
response.destroy(error instanceof Error ? error : undefined);
|
|
@@ -425,7 +499,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
425
499
|
return;
|
|
426
500
|
}
|
|
427
501
|
if (statusCode >= 400) {
|
|
428
|
-
void readRedactedUpstreamError(response).then(({ upstreamCode, upstreamType, unreadableBody }) => {
|
|
502
|
+
void readRedactedUpstreamError(response).then(({ upstreamCode, upstreamType, upstreamParam, unreadableBody, body: upstreamBody }) => {
|
|
429
503
|
if (budget.plainFallback && (statusCode === 415
|
|
430
504
|
|| (unreadableBody && !upstreamType && !upstreamCode && (statusCode === 400 || statusCode === 422)))) {
|
|
431
505
|
responseStarted = true;
|
|
@@ -440,27 +514,42 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
440
514
|
: 'bridge_upstream_transient_mislabel')));
|
|
441
515
|
return;
|
|
442
516
|
}
|
|
443
|
-
const
|
|
444
|
-
const
|
|
517
|
+
const verbatim = codexLb && upstreamBody !== null;
|
|
518
|
+
const clientStatus = verbatim ? statusCode : transientMislabel ? 503 : statusCode;
|
|
519
|
+
const body = verbatim
|
|
520
|
+
? scrubUpstreamErrorBody(upstreamBody, request.credential.value)
|
|
521
|
+
: buildRedactedUpstreamErrorBody(statusCode, upstreamCode, upstreamType, transientMislabel);
|
|
522
|
+
const rejected = statusCode >= 400 && statusCode <= 499;
|
|
445
523
|
logHttpRejection({
|
|
446
|
-
code:
|
|
447
|
-
?
|
|
448
|
-
:
|
|
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}`,
|
|
449
529
|
transport: 'http',
|
|
450
530
|
...(req.method === undefined ? {} : { method: req.method }),
|
|
451
531
|
...(req.url === undefined ? {} : { url: req.url }),
|
|
452
532
|
status: clientStatus,
|
|
453
533
|
provider_id: provider.provider_id,
|
|
454
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
|
+
} : {}),
|
|
455
542
|
});
|
|
456
543
|
responseStarted = true;
|
|
457
544
|
const responseHeaders = rewriteResponseHeaders(response.headers, upstreamBaseUrl, authenticatedLocalBaseUrl);
|
|
458
545
|
responseHeaders['content-length'] = String(body.length);
|
|
459
546
|
delete responseHeaders['transfer-encoding'];
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
547
|
+
delete responseHeaders['content-encoding'];
|
|
548
|
+
if (verbatim && upstreamCode === MODEL_CAPACITY_ERROR_CODE && responseHeaders['retry-after'] === undefined) {
|
|
549
|
+
responseHeaders['retry-after'] = String(MODEL_CAPACITY_RETRY_AFTER_SECONDS);
|
|
550
|
+
}
|
|
551
|
+
if (modelsEtag)
|
|
552
|
+
responseHeaders['x-models-etag'] = modelsEtag;
|
|
464
553
|
res.writeHead(clientStatus, responseHeaders);
|
|
465
554
|
res.end(body, () => finish());
|
|
466
555
|
}).catch(finish);
|
|
@@ -479,6 +568,8 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
479
568
|
const responseHeaders = rewriteResponseHeaders(response.headers, upstreamBaseUrl, authenticatedLocalBaseUrl);
|
|
480
569
|
if (advising)
|
|
481
570
|
delete responseHeaders['content-length'];
|
|
571
|
+
if (modelsEtag)
|
|
572
|
+
responseHeaders['x-models-etag'] = modelsEtag;
|
|
482
573
|
res.writeHead(statusCode, responseHeaders);
|
|
483
574
|
}
|
|
484
575
|
catch (error) {
|
|
@@ -638,6 +729,10 @@ export async function requestProviderCompactionSummary(config, input, body, onAc
|
|
|
638
729
|
}
|
|
639
730
|
}
|
|
640
731
|
}
|
|
732
|
+
function singleRetryAfter(value) {
|
|
733
|
+
const text = String(Array.isArray(value) ? value[0] ?? '' : value ?? '').trim();
|
|
734
|
+
return /^[A-Za-z0-9 ,:.-]{1,64}$/.test(text) ? text : null;
|
|
735
|
+
}
|
|
641
736
|
function upstreamErrorIds(body) {
|
|
642
737
|
try {
|
|
643
738
|
const parsed = JSON.parse(body.toString('utf8'));
|
|
@@ -654,12 +749,18 @@ async function answerCompactionWithProviderSummary(req, res, config, input) {
|
|
|
654
749
|
const streaming = input.kind === 'trigger';
|
|
655
750
|
const responseId = newCompactionResponseId();
|
|
656
751
|
const publicModel = input.route.public_model;
|
|
657
|
-
const
|
|
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));
|
|
658
755
|
let keepalive = null;
|
|
659
756
|
const startStream = () => {
|
|
660
757
|
if (!streaming || res.headersSent)
|
|
661
758
|
return;
|
|
662
|
-
|
|
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
|
+
});
|
|
663
764
|
res.write(sseEvent(compactionCreatedEvent(responseId, publicModel)));
|
|
664
765
|
keepalive = setInterval(() => { if (!res.writableEnded)
|
|
665
766
|
res.write(': keepalive\n\n'); }, COMPACTION_KEEPALIVE_MS);
|
|
@@ -683,9 +784,10 @@ async function answerCompactionWithProviderSummary(req, res, config, input) {
|
|
|
683
784
|
const transient = isTransientUpstreamFailure(call.status, upstreamType, upstreamCode);
|
|
684
785
|
const clientStatus = transient ? 503 : call.status;
|
|
685
786
|
const errorBody = buildRedactedUpstreamErrorBody(call.status, upstreamCode, upstreamType, transient);
|
|
787
|
+
const retryAfter = singleRetryAfter(call.headers['retry-after']);
|
|
686
788
|
res.writeHead(clientStatus, {
|
|
687
789
|
'content-type': 'application/json', 'cache-control': 'no-store', 'content-length': String(errorBody.length),
|
|
688
|
-
...(
|
|
790
|
+
...(retryAfter ? { 'retry-after': retryAfter } : {}),
|
|
689
791
|
});
|
|
690
792
|
res.end(errorBody);
|
|
691
793
|
return;
|
|
@@ -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
|
+
}
|