sneakoscope 8.1.0 → 8.1.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/install-helpers-codex-lb-chain.js +6 -6
- package/dist/cli/install-helpers-codex-lb-config.js +24 -3
- package/dist/cli/install-helpers.js +36 -9
- package/dist/commands/codex-lb.js +44 -22
- package/dist/commands/doctor.js +5 -5
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/codex-app.js +5 -5
- package/dist/core/codex-control/codex-task-runner.js +1 -1
- package/dist/core/codex-lb/codex-lb-env.js +4 -4
- package/dist/core/codex-lb/desktop-controller.js +36 -19
- package/dist/core/codex-lb/desktop-mode.js +1 -1
- package/dist/core/codex-lb/probes/bridge-probe.js +10 -15
- package/dist/core/codex-lb/probes/cli-image-tool-probe.js +7 -4
- package/dist/core/image-ux-review/imagegen-adapter.js +2 -4
- package/dist/core/imagegen/imagegen-capability.js +4 -6
- package/dist/core/init.js +1 -1
- package/dist/core/provider/provider-context.js +2 -2
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/ProvidersConnectTest.swift +8 -5
- package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +68 -16
- package/dist/native/sks-menubar/Sources/ProvidersReliability.swift +3 -2
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +46 -40
- package/dist/scripts/codex-lb-fast-mode-truth-check.js +3 -3
- package/dist/scripts/codex-lb-fast-ui-preservation-check.js +2 -9
- package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Proof-first orchestration for Codex CLI, ChatGPT Desktop, AI coding agents, mult
|
|
|
22
22
|
Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.
|
|
23
23
|
<!-- END SKS SEARCH VISIBILITY MARKETING -->
|
|
24
24
|
|
|
25
|
-
This README documents package **SKS 8.1.
|
|
25
|
+
This README documents package **SKS 8.1.2** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
|
|
26
26
|
|
|
27
27
|
Use the official latest stable SKS and Codex CLI releases. The Codex compatibility SSOT is always the **current latest stable** host; capability probes measure what that host can actually do. Product docs do not crown a fixed `0.x.y` string as SSOT (release pins and schema directories are measured artifacts for the current package, not a permanent product version claim). Menu Bar / Center induce updates to the latest stable build. Run `sks update-check` for what is installed and read the capability report for what is supported. Install SSOT is npm `sneakoscope@latest`; PATH `sks` and Menu Bar stamped generation must match that version or gates fail. It resolves managed SKS skills from the authoritative global install, preserves a runnable Naruto child slot when `max_threads=2`, and keeps Menu Bar repair transactional so stamped generations remain verifiable. Naruto uses stable opt-in multi-agent V2 when the host exposes it (Codex official multi-agent wrap-only; SKS does not reimplement a parallel runtime). Local code search is mode-separated (`sks search files|text|structure|symbol|context`); `context` is answered by the compiled TriWiki Context Graph (`context-graph.json` is exhaustive authority; `context-pack.json` and managed `AGENTS.md` are bounded projections) — see [docs/architecture/context-graph.md](docs/architecture/context-graph.md) and [docs/PRODUCT-CONTRACT.md](docs/PRODUCT-CONTRACT.md). See [CHANGELOG.md](CHANGELOG.md).
|
|
28
28
|
|
|
@@ -172,13 +172,13 @@ function codexLbServiceTierEvidence(...responses) {
|
|
|
172
172
|
fast_actual: actual === CODEX_LB_CANONICAL_FAST_SERVICE_TIER || effective === CODEX_LB_CANONICAL_FAST_SERVICE_TIER
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
|
-
function codexLbGatewayHeaders(apiKey, authTransport = '
|
|
176
|
-
const credentialHeader =
|
|
177
|
-
? {
|
|
178
|
-
: {
|
|
175
|
+
function codexLbGatewayHeaders(apiKey, authTransport = 'authorization-bearer-compat') {
|
|
176
|
+
const credentialHeader = authTransport === 'x-codex-lb-api-key'
|
|
177
|
+
? { 'X-Codex-LB-API-Key': String(apiKey) }
|
|
178
|
+
: { authorization: `Bearer ${String(apiKey)}` };
|
|
179
179
|
return { ...credentialHeader, 'content-type': 'application/json' };
|
|
180
180
|
}
|
|
181
|
-
async function fetchCodexLbResponse(fetchImpl, endpoint, apiKey, body, timeoutMs, authTransport = '
|
|
181
|
+
async function fetchCodexLbResponse(fetchImpl, endpoint, apiKey, body, timeoutMs, authTransport = 'authorization-bearer-compat') {
|
|
182
182
|
const controller = new AbortController();
|
|
183
183
|
const timer = setTimeout(() => controller.abort(), timeoutMs).unref?.();
|
|
184
184
|
try {
|
|
@@ -436,7 +436,7 @@ export async function checkCodexLbResponseChain(status = {}, opts = {}) {
|
|
|
436
436
|
if (!model)
|
|
437
437
|
return { ok: true, status: 'skipped', skipped: true, reason: 'model_unselected_use_explicit_model_or_codex_catalog' };
|
|
438
438
|
const timeoutMs = Number(opts.timeoutMs || env.SKS_CODEX_LB_CHAIN_CHECK_TIMEOUT_MS || 8000);
|
|
439
|
-
const gatewayAuthTransport = opts.gatewayAuthTransport || '
|
|
439
|
+
const gatewayAuthTransport = opts.gatewayAuthTransport || 'authorization-bearer-compat';
|
|
440
440
|
const serviceTier = opts.fastMode === true || opts.serviceTier === 'fast' || opts.serviceTier === CODEX_LB_CANONICAL_FAST_SERVICE_TIER
|
|
441
441
|
? CODEX_LB_CANONICAL_FAST_SERVICE_TIER
|
|
442
442
|
: null;
|
|
@@ -17,6 +17,25 @@ export const CODEX_LB_MODEL_CATALOG_MARKER = '# sks-codex-lb-managed-model-catal
|
|
|
17
17
|
export const CODEX_LB_PROVIDER_SELECTION_MARKER = '# sks-codex-lb-managed-provider-selection';
|
|
18
18
|
export const CODEX_LB_OAUTH_SELECTION_MARKER = '# sks-codex-lb-managed-oauth-selection';
|
|
19
19
|
export const LEGACY_CODEX_LB_OPENAI_ROUTING_MARKER = '# sks-codex-lb-managed-openai-base-url';
|
|
20
|
+
const SKS_SWITCHABLE_THIRD_PARTY_PROVIDER_IDS = ['openrouter', 'sks-router'];
|
|
21
|
+
const SKS_THIRD_PARTY_CATALOG_BASENAMES = ['sks-openrouter-catalog.json', 'opencodex-catalog.json'];
|
|
22
|
+
export function releaseSksManagedThirdPartySelection(text) {
|
|
23
|
+
let next = String(text || '');
|
|
24
|
+
const selected = topLevelTomlString(next, 'model_provider');
|
|
25
|
+
if (!selected || !SKS_SWITCHABLE_THIRD_PARTY_PROVIDER_IDS.includes(selected)) {
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
28
|
+
if (!new RegExp(`(^|\\n)\\[model_providers\\.${escapeRegExp(selected)}\\]`).test(next))
|
|
29
|
+
return next;
|
|
30
|
+
next = removeTopLevelTomlKeyIfValue(next, 'model_provider', selected);
|
|
31
|
+
const catalog = topLevelTomlString(next, 'model_catalog_json');
|
|
32
|
+
if (catalog
|
|
33
|
+
&& !topLevelHasLine(next, CODEX_LB_MODEL_CATALOG_MARKER)
|
|
34
|
+
&& SKS_THIRD_PARTY_CATALOG_BASENAMES.some((name) => catalog.endsWith(`/${name}`) || catalog === name)) {
|
|
35
|
+
next = removeTopLevelTomlKey(next, 'model_catalog_json');
|
|
36
|
+
}
|
|
37
|
+
return next;
|
|
38
|
+
}
|
|
20
39
|
export function removeCodexLbOrphanManagedMarkers(text) {
|
|
21
40
|
const source = String(text || '');
|
|
22
41
|
const selectedProvider = topLevelTomlString(source, 'model_provider');
|
|
@@ -42,7 +61,7 @@ export function removeCodexLbOrphanManagedMarkers(text) {
|
|
|
42
61
|
};
|
|
43
62
|
}
|
|
44
63
|
export function upsertCodexLbNativeDesktopConfig(text, input) {
|
|
45
|
-
let next = removeCodexLbOrphanManagedMarkers(String(text || '')).text;
|
|
64
|
+
let next = releaseSksManagedThirdPartySelection(removeCodexLbOrphanManagedMarkers(String(text || '')).text);
|
|
46
65
|
const selectedProvider = topLevelTomlString(next, 'model_provider');
|
|
47
66
|
if (selectedProvider && selectedProvider !== 'openai' && selectedProvider !== 'codex-lb') {
|
|
48
67
|
throw new Error('codex_lb_user_owned_model_provider_conflict');
|
|
@@ -89,7 +108,9 @@ export function upsertCodexLbCliProviderConfig(text, input) {
|
|
|
89
108
|
if (topLevelHasLine(cleaned, LEGACY_CODEX_LB_OPENAI_ROUTING_MARKER)) {
|
|
90
109
|
throw new Error('codex_lb_legacy_desktop_config_requires_migration');
|
|
91
110
|
}
|
|
92
|
-
const cliBase =
|
|
111
|
+
const cliBase = input.selectGlobally === true
|
|
112
|
+
? releaseSksManagedThirdPartySelection(removeCodexLbManagedDesktopConfig(cleaned))
|
|
113
|
+
: removeCodexLbManagedDesktopConfig(cleaned);
|
|
93
114
|
const claimedSelection = input.selectGlobally === true
|
|
94
115
|
&& topLevelTomlString(cliBase, 'model_provider') === 'codex-lb'
|
|
95
116
|
&& !topLevelHasLine(cliBase, CODEX_LB_PROVIDER_SELECTION_MARKER)
|
|
@@ -161,7 +182,7 @@ function cliProviderBlock(remoteBaseUrl) {
|
|
|
161
182
|
'name = "codex-lb"',
|
|
162
183
|
`base_url = ${JSON.stringify(remoteBaseUrl)}`,
|
|
163
184
|
'wire_api = "responses"',
|
|
164
|
-
'
|
|
185
|
+
'env_key = "CODEX_LB_API_KEY"',
|
|
165
186
|
'supports_websockets = true',
|
|
166
187
|
'requires_openai_auth = false'
|
|
167
188
|
].join('\n');
|
|
@@ -28,7 +28,7 @@ import { canAskYesNo, compareVersions, hasCodexUnstableFeatureWarningSuppression
|
|
|
28
28
|
import { printCodexLbSetupWarnings } from './codex-lb-setup-warning-output.js';
|
|
29
29
|
import { checkCodexLbResponseChain } from './install-helpers-codex-lb-chain.js';
|
|
30
30
|
import { CODEX_LB_CANONICAL_FAST_SERVICE_TIER, CODEX_LB_PROVIDER_ENV_KEY, askPostinstallQuestion, codexAuthChatgptBackupPath, codexAuthPath, codexLbConfigPath, codexLbEnvPath, hasTopLevelCodexLbSelected, normalizeCodexLbBaseUrl, parseCodexLbEnvKey, redactSecretText } from './install-helpers-codex-lb-shared.js';
|
|
31
|
-
import { CODEX_LB_DESKTOP_BRIDGE_MARKER, CODEX_LB_DESKTOP_COMPAT_MARKER, CODEX_LB_PROVIDER_SELECTION_MARKER, appliedCodexLbPersistenceModes, captureCodexLbSetupWriteState, codexLbSharedOpenAiRoutingState, detectCodexLbSetupDrift, ensureGlobalCodexAppGlmProfile, ensureStoredOpenRouterProviderDuringInstall, hardenCodexLbSetupRecoveryPath, removeCodexLbSetupRecoveryPath, removeCodexLbOrphanManagedMarkers, removeCodexLbSharedOpenAiRouting, removeCodexLbManagedDesktopConfig, restoreCodexLbSetupWriteState, sha256Text, shellSingleQuote, upsertCodexAppGlmConfig, upsertCodexLbConfig, upsertCodexLbCliProviderConfig, upsertCodexLbCompatDesktopConfig, upsertCodexLbNativeDesktopConfig, upsertCodexLbSharedOpenAiRouting, writeCodexLbSetupFileIfUnchanged } from './install-helpers-codex-lb-config.js';
|
|
31
|
+
import { CODEX_LB_DESKTOP_BRIDGE_MARKER, CODEX_LB_DESKTOP_COMPAT_MARKER, CODEX_LB_PROVIDER_SELECTION_MARKER, appliedCodexLbPersistenceModes, captureCodexLbSetupWriteState, codexLbSharedOpenAiRoutingState, detectCodexLbSetupDrift, ensureGlobalCodexAppGlmProfile, ensureStoredOpenRouterProviderDuringInstall, hardenCodexLbSetupRecoveryPath, removeCodexLbSetupRecoveryPath, removeCodexLbOrphanManagedMarkers, removeCodexLbSharedOpenAiRouting, releaseSksManagedThirdPartySelection, removeCodexLbManagedDesktopConfig, restoreCodexLbSetupWriteState, sha256Text, shellSingleQuote, upsertCodexAppGlmConfig, upsertCodexLbConfig, upsertCodexLbCliProviderConfig, upsertCodexLbCompatDesktopConfig, upsertCodexLbNativeDesktopConfig, upsertCodexLbSharedOpenAiRouting, writeCodexLbSetupFileIfUnchanged } from './install-helpers-codex-lb-config.js';
|
|
32
32
|
import { detectLegacyCodexLbDesktopState } from '../core/codex-lb/legacy-migration.js';
|
|
33
33
|
import { ensureCodexImagegenDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureGlobalContext7DuringInstall, ensureGlobalGetdesignSkillDuringInstall, ensureSksCommandDuringInstall } from './install-helpers-install-support.js';
|
|
34
34
|
import { escapeRegExp } from '../core/text/regex.js';
|
|
@@ -416,7 +416,7 @@ export async function configureCodexLbDesktopRouting(opts) {
|
|
|
416
416
|
})
|
|
417
417
|
: mode === 'desktop-dual-auth-compat'
|
|
418
418
|
? upsertCodexLbCompatDesktopConfig(current, { remoteBaseUrl: String(opts.remoteBaseUrl) })
|
|
419
|
-
: removeCodexLbManagedDesktopConfig(current);
|
|
419
|
+
: releaseSksManagedThirdPartySelection(removeCodexLbManagedDesktopConfig(current));
|
|
420
420
|
}
|
|
421
421
|
catch (error) {
|
|
422
422
|
return {
|
|
@@ -494,10 +494,38 @@ export async function configureCodexLbCliProvider(opts) {
|
|
|
494
494
|
const authPath = opts.authPath || codexAuthPath(home);
|
|
495
495
|
const authBefore = await captureCodexAuthSnapshot({ home, authPath });
|
|
496
496
|
const current = await readText(configPath, '');
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
497
|
+
let next;
|
|
498
|
+
try {
|
|
499
|
+
next = upsertCodexLbCliProviderConfig(current, {
|
|
500
|
+
remoteBaseUrl: opts.remoteBaseUrl,
|
|
501
|
+
...(opts.selectGlobally === undefined ? {} : { selectGlobally: opts.selectGlobally })
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
506
|
+
return {
|
|
507
|
+
schema: 'sks.codex-lb-cli-provider.v1',
|
|
508
|
+
ok: false,
|
|
509
|
+
status: message,
|
|
510
|
+
mode: 'cli-provider',
|
|
511
|
+
identity_plane: 'unchanged',
|
|
512
|
+
routing_plane: 'cli_provider',
|
|
513
|
+
oauth_preserved: true,
|
|
514
|
+
auth_mutated: false,
|
|
515
|
+
config_path: configPath,
|
|
516
|
+
auth_path: authPath,
|
|
517
|
+
auth_before: authBefore,
|
|
518
|
+
auth_after: authBefore,
|
|
519
|
+
blockers: [message],
|
|
520
|
+
guidance: message === 'codex_lb_user_owned_model_provider_conflict'
|
|
521
|
+
? [
|
|
522
|
+
'config.toml has a model_provider line SKS does not manage.',
|
|
523
|
+
'Remove or change that line yourself, then retry Use Codex LB.'
|
|
524
|
+
]
|
|
525
|
+
: [],
|
|
526
|
+
backup_path: null
|
|
527
|
+
};
|
|
528
|
+
}
|
|
501
529
|
const write = await safeWriteCodexConfigToml(configPath, current, next, 'codex-lb-cli-provider', { verifyUnchangedBeforeWrite: true });
|
|
502
530
|
const authAfter = await captureCodexAuthSnapshot({ home, authPath });
|
|
503
531
|
try {
|
|
@@ -1893,9 +1921,8 @@ function codexLbProviderEnvKey(text = '') {
|
|
|
1893
1921
|
}
|
|
1894
1922
|
function codexLbProviderHasSeparateGatewayAuth(text = '') {
|
|
1895
1923
|
const block = String(text || '').match(/(^|\n)\[model_providers\.codex-lb\]([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[2] || '';
|
|
1896
|
-
return /
|
|
1897
|
-
&&
|
|
1898
|
-
&& !/(^|\n)\s*env_key\s*=/.test(block);
|
|
1924
|
+
return /(^|\n)\s*env_key\s*=\s*"CODEX_LB_API_KEY"\s*(?:#.*)?(?=\n|$)/.test(block)
|
|
1925
|
+
&& !/X-Codex-LB-API-Key/.test(block);
|
|
1899
1926
|
}
|
|
1900
1927
|
function codexLbFastModeConfigStatus(text = '') {
|
|
1901
1928
|
const globalServiceTier = topLevelTomlString(text, 'service_tier');
|
|
@@ -67,7 +67,10 @@ export async function run(command, args = []) {
|
|
|
67
67
|
baseUrl: loadedEnv.base_url,
|
|
68
68
|
apiKey: loadedEnv.secret_api_key,
|
|
69
69
|
credentialBindingBlockers: loadedEnv.credential_binding.blockers,
|
|
70
|
-
gatewayAuthTransport: options.gatewayAuthTransport
|
|
70
|
+
gatewayAuthTransport: options.gatewayAuthTransport
|
|
71
|
+
|| (directGatewayProbeTransport(status, loadedEnv) === 'x-codex-lb-api-key'
|
|
72
|
+
? 'x-codex-lb-api-key'
|
|
73
|
+
: 'authorization-bearer-compat')
|
|
71
74
|
});
|
|
72
75
|
if (!result.ok)
|
|
73
76
|
process.exitCode = 1;
|
|
@@ -110,7 +113,7 @@ export async function run(command, args = []) {
|
|
|
110
113
|
selected: status.selected === true,
|
|
111
114
|
baseUrl: loadedEnv.base_url,
|
|
112
115
|
apiKey: loadedEnv.secret_api_key,
|
|
113
|
-
authTransport:
|
|
116
|
+
authTransport: directGatewayProbeTransport(status, loadedEnv)
|
|
114
117
|
}, {
|
|
115
118
|
home: path.dirname(path.dirname(status.env_path))
|
|
116
119
|
});
|
|
@@ -149,7 +152,7 @@ export async function run(command, args = []) {
|
|
|
149
152
|
selected: status.selected === true,
|
|
150
153
|
baseUrl: loadedEnv.base_url,
|
|
151
154
|
apiKey: loadedEnv.secret_api_key,
|
|
152
|
-
authTransport:
|
|
155
|
+
authTransport: directGatewayProbeTransport(status, loadedEnv)
|
|
153
156
|
}, {
|
|
154
157
|
home: path.dirname(path.dirname(status.env_path))
|
|
155
158
|
});
|
|
@@ -428,6 +431,25 @@ export async function run(command, args = []) {
|
|
|
428
431
|
}
|
|
429
432
|
if (action === 'setup' || action === 'reconfigure') {
|
|
430
433
|
const options = await codexLbSetupOptions(args);
|
|
434
|
+
if (options.desktopMode === 'cli-provider' && options.gatewayAuthTransport === 'x-codex-lb-api-key') {
|
|
435
|
+
const result = {
|
|
436
|
+
schema: 'sks.codex-lb-setup.v1',
|
|
437
|
+
ok: false,
|
|
438
|
+
status: 'custom_header_transport_requires_desktop_bridge',
|
|
439
|
+
guidance: [
|
|
440
|
+
'The atomic CLI provider always sends Authorization: Bearer (Codex env_key).',
|
|
441
|
+
'For a gateway that only accepts the X-Codex-LB-API-Key header, use Desktop Bridge mode:',
|
|
442
|
+
'sks codex-lb setup --host <domain> --desktop-mode desktop-full --gateway-auth custom-header --api-key-stdin --yes'
|
|
443
|
+
]
|
|
444
|
+
};
|
|
445
|
+
if (flag(args, '--json')) {
|
|
446
|
+
process.exitCode = 1;
|
|
447
|
+
return printJson(result);
|
|
448
|
+
}
|
|
449
|
+
console.error('codex-lb setup rejected: --gateway-auth custom-header only applies to Desktop Bridge mode; the CLI provider always uses Authorization: Bearer.');
|
|
450
|
+
process.exitCode = 1;
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
431
453
|
const plan = buildCodexLbSetupPlan({
|
|
432
454
|
host_or_base_url: options.host || '',
|
|
433
455
|
api_key_source: options.apiKeySource,
|
|
@@ -546,13 +568,6 @@ export async function run(command, args = []) {
|
|
|
546
568
|
gatewayAuthTransport: options.gatewayAuthTransport
|
|
547
569
|
});
|
|
548
570
|
}
|
|
549
|
-
else if (result.ok && options.desktopMode === 'desktop-dual-auth-compat') {
|
|
550
|
-
desktopActivation = await activateCodexLbDesktopMode({
|
|
551
|
-
...controllerOptions(args),
|
|
552
|
-
mode: 'desktop-dual-auth-compat',
|
|
553
|
-
gatewayAuthTransport: 'x-codex-lb-api-key'
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
571
|
const capabilities = options.health && result.ok
|
|
557
572
|
? await buildCodexLbDesktopCapabilities({
|
|
558
573
|
...controllerOptions(args),
|
|
@@ -566,7 +581,7 @@ export async function run(command, args = []) {
|
|
|
566
581
|
selected: result.codex_lb?.selected === true,
|
|
567
582
|
baseUrl: result.base_url || null,
|
|
568
583
|
apiKey: options.apiKey,
|
|
569
|
-
authTransport: '
|
|
584
|
+
authTransport: 'authorization-bearer'
|
|
570
585
|
}, {
|
|
571
586
|
home: path.dirname(path.dirname(result.env_path))
|
|
572
587
|
})
|
|
@@ -1043,19 +1058,26 @@ function normalizeDesktopSetupMode(value) {
|
|
|
1043
1058
|
}
|
|
1044
1059
|
throw new Error(`unsupported_codex_lb_desktop_mode:${normalized}`);
|
|
1045
1060
|
}
|
|
1061
|
+
function directGatewayProbeTransport(status, loadedEnv) {
|
|
1062
|
+
return status?.desktop_mode === 'desktop-native-bridge'
|
|
1063
|
+
&& loadedEnv?.gateway_auth_transport === 'x-codex-lb-api-key'
|
|
1064
|
+
? 'x-codex-lb-api-key'
|
|
1065
|
+
: 'authorization-bearer';
|
|
1066
|
+
}
|
|
1046
1067
|
function normalizeGatewayAuthChoice(value) {
|
|
1047
1068
|
const normalized = String(value || '').trim().toLowerCase();
|
|
1048
1069
|
if (!normalized
|
|
1049
|
-
|| normalized === '
|
|
1050
|
-
|| normalized === 'header'
|
|
1051
|
-
|| normalized === 'x-codex-lb-api-key') {
|
|
1052
|
-
return 'x-codex-lb-api-key';
|
|
1053
|
-
}
|
|
1054
|
-
if (normalized === 'bearer'
|
|
1070
|
+
|| normalized === 'bearer'
|
|
1055
1071
|
|| normalized === 'bearer-compat'
|
|
1072
|
+
|| normalized === 'authorization-bearer'
|
|
1056
1073
|
|| normalized === 'authorization-bearer-compat') {
|
|
1057
1074
|
return 'authorization-bearer-compat';
|
|
1058
1075
|
}
|
|
1076
|
+
if (normalized === 'custom-header'
|
|
1077
|
+
|| normalized === 'header'
|
|
1078
|
+
|| normalized === 'x-codex-lb-api-key') {
|
|
1079
|
+
return 'x-codex-lb-api-key';
|
|
1080
|
+
}
|
|
1059
1081
|
throw new Error(`unsupported_codex_lb_gateway_auth_transport:${normalized}`);
|
|
1060
1082
|
}
|
|
1061
1083
|
function asRecord(value) {
|
|
@@ -1088,7 +1110,7 @@ async function codexLbSetupOptions(args = []) {
|
|
|
1088
1110
|
let desktopMode = normalizeDesktopSetupMode(readOption(args, '--desktop-mode', readOption(args, '--mode', 'cli-provider')));
|
|
1089
1111
|
let gatewayAuthTransport = parseCodexLbGatewayAuthTransport(normalizeGatewayAuthChoice(flag(args, '--compat-bearer')
|
|
1090
1112
|
? 'authorization-bearer-compat'
|
|
1091
|
-
: readOption(args, '--gateway-auth', readOption(args, '--gateway-auth-transport', '
|
|
1113
|
+
: readOption(args, '--gateway-auth', readOption(args, '--gateway-auth-transport', 'bearer-compat'))));
|
|
1092
1114
|
let keychain = flag(args, '--keychain');
|
|
1093
1115
|
if (flag(args, '--api-key-stdin'))
|
|
1094
1116
|
apiKey = (await readStdin()).trim();
|
|
@@ -1112,13 +1134,13 @@ async function codexLbSetupOptions(args = []) {
|
|
|
1112
1134
|
host ||= (await ask('1. codex-lb domain or base URL?\n Example: lb.example.com or https://lb.example.com/backend-api/codex\n> ')).trim();
|
|
1113
1135
|
apiKey ||= (await askHidden('2. API key?\n Input hidden. Value will be stored securely and never printed.\n> ')).trim();
|
|
1114
1136
|
apiKeySource = 'hidden_prompt';
|
|
1115
|
-
desktopMode = normalizeDesktopSetupMode((await ask('3. Configure which mode? [desktop-full
|
|
1116
|
-
|| '
|
|
1137
|
+
desktopMode = normalizeDesktopSetupMode((await ask('3. Configure which mode? [cli-only/desktop-full] (cli-only)\n> ')).trim()
|
|
1138
|
+
|| 'cli-only');
|
|
1117
1139
|
if (desktopMode === 'desktop-native-bridge') {
|
|
1118
|
-
gatewayAuthTransport = parseCodexLbGatewayAuthTransport(normalizeGatewayAuthChoice((await ask('4. Gateway key transport? [custom-header
|
|
1140
|
+
gatewayAuthTransport = parseCodexLbGatewayAuthTransport(normalizeGatewayAuthChoice((await ask('4. Gateway key transport? [bearer-compat/custom-header] (bearer-compat)\n> ')).trim()));
|
|
1119
1141
|
}
|
|
1120
1142
|
else {
|
|
1121
|
-
gatewayAuthTransport = '
|
|
1143
|
+
gatewayAuthTransport = 'authorization-bearer-compat';
|
|
1122
1144
|
}
|
|
1123
1145
|
writeEnvFile = parseYesNo(await ask('5. Write shell env loader to ~/.codex/sks-codex-lb.env? [Y/n] '), true);
|
|
1124
1146
|
keychain = false;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -580,7 +580,7 @@ export async function codexLbRoutingTruthForStatus(status, deps = {}) {
|
|
|
580
580
|
const baseUrl = loaded.base_url || status.base_url || null;
|
|
581
581
|
const configuredHost = publicUrlHost(baseUrl);
|
|
582
582
|
const authTransport = mode === 'cli-provider'
|
|
583
|
-
? '
|
|
583
|
+
? 'authorization-bearer'
|
|
584
584
|
: doctorBridgeRoutingTransport(status, receipt);
|
|
585
585
|
const contextReceipt = await readCodexLbRoutingTruthReceipt({
|
|
586
586
|
...receiptOptions,
|
|
@@ -609,7 +609,7 @@ export async function codexLbRoutingTruthForStatus(status, deps = {}) {
|
|
|
609
609
|
baseUrl: status.base_url || null,
|
|
610
610
|
apiKey: null,
|
|
611
611
|
authTransport: mode === 'cli-provider'
|
|
612
|
-
? '
|
|
612
|
+
? 'authorization-bearer'
|
|
613
613
|
: doctorBridgeRoutingTransport(status, receipt),
|
|
614
614
|
measure: false
|
|
615
615
|
}, {
|
|
@@ -627,9 +627,9 @@ function doctorBridgeRoutingTransport(status, receipt) {
|
|
|
627
627
|
|| receipt.auth_transport === 'x-codex-lb-api-key')) {
|
|
628
628
|
return receipt.auth_transport;
|
|
629
629
|
}
|
|
630
|
-
return status?.gateway_auth_transport === '
|
|
631
|
-
? '
|
|
632
|
-
: '
|
|
630
|
+
return status?.gateway_auth_transport === 'x-codex-lb-api-key'
|
|
631
|
+
? 'x-codex-lb-api-key'
|
|
632
|
+
: 'authorization-bearer';
|
|
633
633
|
}
|
|
634
634
|
function publicUrlHost(value) {
|
|
635
635
|
try {
|
package/dist/core/codex-app.js
CHANGED
|
@@ -943,8 +943,8 @@ export async function codexProviderModelUiStatus(opts = {}) {
|
|
|
943
943
|
const codexLbCliProviderContractOk = codexLbProviderPresent
|
|
944
944
|
&& hasTomlString(codexLbProvider, 'name', 'codex-lb')
|
|
945
945
|
&& hasTomlString(codexLbProvider, 'wire_api', 'responses')
|
|
946
|
-
&& /
|
|
947
|
-
&& !/
|
|
946
|
+
&& /(?:^|\n)\s*env_key\s*=\s*"CODEX_LB_API_KEY"\s*(?:#.*)?(?=\n|$)/.test(codexLbProvider)
|
|
947
|
+
&& !/X-Codex-LB-API-Key/.test(codexLbProvider)
|
|
948
948
|
&& hasTomlBoolean(codexLbProvider, 'supports_websockets', true)
|
|
949
949
|
&& hasTomlBoolean(codexLbProvider, 'requires_openai_auth', false);
|
|
950
950
|
const codexLbSelectedDefault = /(?:^|\n)\s*model_provider\s*=\s*"codex-lb"\s*(?:#.*)?(?=\n|$)/.test(topLevelToml(globalConfig));
|
|
@@ -1185,9 +1185,9 @@ function inferCodexLbDesktopModeForUi(config) {
|
|
|
1185
1185
|
: 'disabled';
|
|
1186
1186
|
}
|
|
1187
1187
|
function normalizeCodexLbGatewayAuthTransportForUi(value) {
|
|
1188
|
-
return value === '
|
|
1189
|
-
? '
|
|
1190
|
-
: '
|
|
1188
|
+
return value === 'x-codex-lb-api-key'
|
|
1189
|
+
? 'x-codex-lb-api-key'
|
|
1190
|
+
: 'authorization-bearer-compat';
|
|
1191
1191
|
}
|
|
1192
1192
|
async function findDefaultPluginSource(plugin, { home, configText }) {
|
|
1193
1193
|
const cached = await findPluginCache(plugin.name, { home });
|
|
@@ -595,7 +595,7 @@ async function ensurePythonCodexLbConfig(env, config) {
|
|
|
595
595
|
'name = "codex-lb"',
|
|
596
596
|
`base_url = ${tomlQuote(lbBaseUrl)}`,
|
|
597
597
|
'wire_api = "responses"',
|
|
598
|
-
'
|
|
598
|
+
'env_key = "CODEX_LB_API_KEY"',
|
|
599
599
|
'supports_websockets = true',
|
|
600
600
|
'requires_openai_auth = false',
|
|
601
601
|
''
|
|
@@ -58,10 +58,10 @@ export async function readCodexLbModelCatalog(opts = {}) {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
const fetchImpl = opts.fetchImpl || fetch;
|
|
61
|
-
const gatewayAuthTransport = opts.gatewayAuthTransport ||
|
|
62
|
-
const gatewayHeaders = gatewayAuthTransport === '
|
|
63
|
-
? {
|
|
64
|
-
: {
|
|
61
|
+
const gatewayAuthTransport = opts.gatewayAuthTransport || DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT;
|
|
62
|
+
const gatewayHeaders = gatewayAuthTransport === 'x-codex-lb-api-key'
|
|
63
|
+
? { 'X-Codex-LB-API-Key': loaded.secret_api_key }
|
|
64
|
+
: { Authorization: `Bearer ${loaded.secret_api_key}` };
|
|
65
65
|
try {
|
|
66
66
|
const response = await fetchImpl(`${loaded.base_url}/models`, {
|
|
67
67
|
headers: gatewayHeaders,
|
|
@@ -119,11 +119,11 @@ export async function codexLbDesktopStatusV2(options = {}) {
|
|
|
119
119
|
const routingActive = configuredRoutingActive
|
|
120
120
|
&& codexLbRoutingTruthIsActive(routingTruth);
|
|
121
121
|
const diagnosticOk = uniqueBlockers.length === 0 && !overallBlocked;
|
|
122
|
-
const effectiveGatewayAuthTransport = context.mode === '
|
|
123
|
-
?
|
|
124
|
-
: context.mode === '
|
|
125
|
-
?
|
|
126
|
-
:
|
|
122
|
+
const effectiveGatewayAuthTransport = context.mode === 'disabled'
|
|
123
|
+
? null
|
|
124
|
+
: context.mode === 'cli-provider'
|
|
125
|
+
? 'authorization-bearer-compat'
|
|
126
|
+
: context.gatewayAuthTransport;
|
|
127
127
|
return {
|
|
128
128
|
schema: CODEX_LB_STATUS_SCHEMA_V2,
|
|
129
129
|
ok: diagnosticOk && routingActive,
|
|
@@ -194,7 +194,8 @@ export async function activateCodexLbDesktopMode(input) {
|
|
|
194
194
|
mode: input.mode,
|
|
195
195
|
identity_plane: 'unchanged',
|
|
196
196
|
routing_plane: 'unchanged',
|
|
197
|
-
gateway_auth_transport:
|
|
197
|
+
gateway_auth_transport: input.gatewayAuthTransport
|
|
198
|
+
|| DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT,
|
|
198
199
|
oauth_preserved: true,
|
|
199
200
|
bridge_started: false,
|
|
200
201
|
config_committed: false,
|
|
@@ -538,12 +539,13 @@ export async function disableCodexLbDesktopRouting(input = {}) {
|
|
|
538
539
|
export async function configureCodexLbCliMode(input = {}) {
|
|
539
540
|
const context = await loadDesktopContext(input);
|
|
540
541
|
const selectedBefore = topLevelTomlString(context.config, 'model_provider') === 'codex-lb';
|
|
542
|
+
const cliAuthTransport = routingTruthAuthTransport('cli-provider', context.gatewayAuthTransport);
|
|
541
543
|
const routingTruth = await measureAndWriteCodexLbRoutingTruth({
|
|
542
544
|
mode: 'cli-provider',
|
|
543
545
|
selected: true,
|
|
544
546
|
baseUrl: context.loadedEnv.base_url,
|
|
545
547
|
apiKey: context.loadedEnv.secret_api_key,
|
|
546
|
-
authTransport:
|
|
548
|
+
authTransport: cliAuthTransport,
|
|
547
549
|
...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}),
|
|
548
550
|
...(input.capabilityTimeoutMs ? { timeoutMs: input.capabilityTimeoutMs } : {})
|
|
549
551
|
}, {
|
|
@@ -725,6 +727,7 @@ export async function buildCodexLbDesktopCapabilities(input = {}) {
|
|
|
725
727
|
baseUrl: target.baseUrl,
|
|
726
728
|
apiKey: context.loadedEnv.secret_api_key || '',
|
|
727
729
|
model,
|
|
730
|
+
gatewayAuthTransport: target.effectiveTransport,
|
|
728
731
|
fetchImpl: input.fetchImpl || globalThis.fetch,
|
|
729
732
|
...(envTimeout > 0 ? { timeoutMs: envTimeout } : {})
|
|
730
733
|
});
|
|
@@ -752,11 +755,11 @@ export async function buildCodexLbDesktopCapabilities(input = {}) {
|
|
|
752
755
|
oauthPreserved: context.oauthPresent,
|
|
753
756
|
manifest,
|
|
754
757
|
gatewayAuth: {
|
|
755
|
-
transport: cliPlane ? '
|
|
758
|
+
transport: cliPlane ? 'authorization-bearer-compat' : context.gatewayAuthTransport,
|
|
756
759
|
configured: context.loadedEnv.configured,
|
|
757
760
|
observed: gatewayObserved,
|
|
758
761
|
fixture,
|
|
759
|
-
legacyCompatibilityExplicit:
|
|
762
|
+
legacyCompatibilityExplicit: false,
|
|
760
763
|
blockers: [
|
|
761
764
|
...(gatewayAuthRejected
|
|
762
765
|
? [`codex_lb_gateway_auth_rejected_for_transport:${target?.effectiveTransport || context.gatewayAuthTransport}`]
|
|
@@ -1189,8 +1192,8 @@ function providerStatus(config, mode) {
|
|
|
1189
1192
|
const cliContractOk = Boolean(provider)
|
|
1190
1193
|
&& hasTomlString(provider, 'name', 'codex-lb')
|
|
1191
1194
|
&& hasTomlString(provider, 'wire_api', 'responses')
|
|
1192
|
-
&& /
|
|
1193
|
-
&& !/
|
|
1195
|
+
&& /(?:^|\n)\s*env_key\s*=\s*"CODEX_LB_API_KEY"\s*$/m.test(provider)
|
|
1196
|
+
&& !/"X-Codex-LB-API-Key"\s*=\s*"CODEX_LB_API_KEY"/.test(provider)
|
|
1194
1197
|
&& hasTomlBoolean(provider, 'requires_openai_auth', false)
|
|
1195
1198
|
&& hasTomlBoolean(provider, 'supports_websockets', true);
|
|
1196
1199
|
if (mode === 'cli-provider') {
|
|
@@ -1477,9 +1480,10 @@ function capabilityTarget(context) {
|
|
|
1477
1480
|
effectiveTransport: context.gatewayAuthTransport
|
|
1478
1481
|
};
|
|
1479
1482
|
}
|
|
1480
|
-
const effectiveTransport = context.mode
|
|
1483
|
+
const effectiveTransport = context.mode !== 'cli-provider'
|
|
1484
|
+
&& context.gatewayAuthTransport === 'x-codex-lb-api-key'
|
|
1481
1485
|
? 'x-codex-lb-api-key'
|
|
1482
|
-
:
|
|
1486
|
+
: 'authorization-bearer-compat';
|
|
1483
1487
|
const headers = effectiveTransport === 'x-codex-lb-api-key'
|
|
1484
1488
|
? { 'X-Codex-LB-API-Key': context.loadedEnv.secret_api_key }
|
|
1485
1489
|
: { Authorization: `Bearer ${context.loadedEnv.secret_api_key}` };
|
|
@@ -1651,7 +1655,20 @@ function statusGuidance(mode, configured, blockers) {
|
|
|
1651
1655
|
`The gateway rejected the configured auth transport (${rejectedTransport}).`,
|
|
1652
1656
|
rejectedTransport === 'x-codex-lb-api-key'
|
|
1653
1657
|
? 'If the gateway expects an Authorization header, re-run setup with: --gateway-auth bearer-compat'
|
|
1654
|
-
: 'If the gateway expects the custom header,
|
|
1658
|
+
: 'If the gateway expects the custom header, use Desktop Bridge mode with: --gateway-auth custom-header (the atomic CLI provider is always Authorization: Bearer)'
|
|
1659
|
+
];
|
|
1660
|
+
}
|
|
1661
|
+
if (blockers.includes('codex_lb_auth_rejected')) {
|
|
1662
|
+
return [
|
|
1663
|
+
'The gateway rejected the API key (HTTP 401/403) over Authorization: Bearer.',
|
|
1664
|
+
'Reconnect the Codex LB credential in Center, or re-run: sks codex-lb setup --host <domain> --api-key-stdin --yes',
|
|
1665
|
+
'Then re-verify with: sks codex-lb connect-test'
|
|
1666
|
+
];
|
|
1667
|
+
}
|
|
1668
|
+
if (blockers.includes('codex_lb_endpoint_unreachable')) {
|
|
1669
|
+
return [
|
|
1670
|
+
'The gateway host could not be reached (network, DNS, or TLS).',
|
|
1671
|
+
'Verify the host, then re-run: sks codex-lb connect-test'
|
|
1655
1672
|
];
|
|
1656
1673
|
}
|
|
1657
1674
|
if (mode === 'desktop-native-bridge') {
|
|
@@ -1697,11 +1714,11 @@ function desktopCompatModeUnavailable(mode) {
|
|
|
1697
1714
|
return mode === 'desktop-dual-auth-compat';
|
|
1698
1715
|
}
|
|
1699
1716
|
function routingTruthAuthTransport(mode, gatewayAuthTransport) {
|
|
1700
|
-
if (mode
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1717
|
+
if (mode !== 'desktop-native-bridge')
|
|
1718
|
+
return 'authorization-bearer';
|
|
1719
|
+
return gatewayAuthTransport === 'x-codex-lb-api-key'
|
|
1720
|
+
? 'x-codex-lb-api-key'
|
|
1721
|
+
: 'authorization-bearer';
|
|
1705
1722
|
}
|
|
1706
1723
|
function publicHost(baseUrl) {
|
|
1707
1724
|
try {
|
|
@@ -9,7 +9,7 @@ export const CODEX_LB_GATEWAY_AUTH_TRANSPORTS = [
|
|
|
9
9
|
'authorization-bearer-compat'
|
|
10
10
|
];
|
|
11
11
|
export const DEFAULT_CODEX_LB_DESKTOP_MODE = 'desktop-native-bridge';
|
|
12
|
-
export const DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT = '
|
|
12
|
+
export const DEFAULT_CODEX_LB_GATEWAY_AUTH_TRANSPORT = 'authorization-bearer-compat';
|
|
13
13
|
export function parseCodexLbDesktopMode(value) {
|
|
14
14
|
const normalized = String(value ?? '').trim();
|
|
15
15
|
if (CODEX_LB_DESKTOP_MODES.includes(normalized)) {
|
|
@@ -48,28 +48,23 @@ export function runBridgeProbe(input) {
|
|
|
48
48
|
}
|
|
49
49
|
export function gatewayAuthTransportEvidence(input) {
|
|
50
50
|
const transport = input.transport || 'unknown';
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
&& input.legacyCompatibilityExplicit === true;
|
|
51
|
+
const preferredBearer = transport === 'authorization-bearer-compat'
|
|
52
|
+
|| transport === 'authorization-bearer';
|
|
53
|
+
const customHeader = transport === 'x-codex-lb-api-key';
|
|
55
54
|
return probeEvidence({
|
|
56
55
|
configured: input.configured,
|
|
57
56
|
attempted: input.observed,
|
|
58
|
-
verified: input.observed === true && (
|
|
57
|
+
verified: input.observed === true && (preferredBearer || customHeader),
|
|
59
58
|
fixture: input.fixture,
|
|
60
59
|
source: input.observed ? 'transport' : 'config',
|
|
61
|
-
blockers: [
|
|
62
|
-
|
|
63
|
-
...(transport === 'authorization-bearer-compat' && !input.legacyCompatibilityExplicit
|
|
64
|
-
? ['legacy_gateway_auth_compatibility_not_explicit']
|
|
65
|
-
: [])
|
|
66
|
-
],
|
|
67
|
-
warnings: legacyExplicit ? ['legacy_authorization_bearer_compatibility_active'] : [],
|
|
60
|
+
blockers: [...(input.blockers || [])],
|
|
61
|
+
warnings: customHeader ? ['custom_x_codex_lb_api_key_transport_active'] : [],
|
|
68
62
|
evidence: {
|
|
69
63
|
configured_gateway_auth_transport: transport,
|
|
70
|
-
preferred_custom_header:
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
preferred_custom_header: customHeader,
|
|
65
|
+
preferred_authorization_bearer: preferredBearer,
|
|
66
|
+
standard_authorization_bearer: transport === 'authorization-bearer',
|
|
67
|
+
legacy_authorization_bearer_compatibility: false,
|
|
73
68
|
silent_fallback: false,
|
|
74
69
|
observed: input.observed === true
|
|
75
70
|
}
|
|
@@ -18,10 +18,13 @@ export async function probeCodexLbCliImageGeneration(input) {
|
|
|
18
18
|
}
|
|
19
19
|
const endpoint = `${String(input.baseUrl || '').replace(/\/+$/, '')}/responses`;
|
|
20
20
|
const timeoutMs = input.timeoutMs || DEFAULT_CLI_IMAGE_PROBE_TIMEOUT_MS;
|
|
21
|
-
const
|
|
21
|
+
const authHeaders = input.gatewayAuthTransport === 'x-codex-lb-api-key'
|
|
22
|
+
? { 'X-Codex-LB-API-Key': input.apiKey }
|
|
23
|
+
: { authorization: `Bearer ${input.apiKey}` };
|
|
24
|
+
const forced = await postImageProbe(fetchImpl, endpoint, authHeaders, input.model, timeoutMs, true);
|
|
22
25
|
const forcedToolChoiceRejected = !forced.ok && /tool_choice/.test(String(forced.errorText || ''));
|
|
23
26
|
const attempt = forcedToolChoiceRejected
|
|
24
|
-
? await postImageProbe(fetchImpl, endpoint,
|
|
27
|
+
? await postImageProbe(fetchImpl, endpoint, authHeaders, input.model, timeoutMs, false)
|
|
25
28
|
: forced;
|
|
26
29
|
if (attempt.timedOut) {
|
|
27
30
|
return result({
|
|
@@ -69,14 +72,14 @@ export async function probeCodexLbCliImageGeneration(input) {
|
|
|
69
72
|
error: null
|
|
70
73
|
});
|
|
71
74
|
}
|
|
72
|
-
async function postImageProbe(fetchImpl, endpoint,
|
|
75
|
+
async function postImageProbe(fetchImpl, endpoint, authHeaders, model, timeoutMs, forceTool) {
|
|
73
76
|
const controller = new AbortController();
|
|
74
77
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
75
78
|
try {
|
|
76
79
|
const response = await fetchImpl(endpoint, {
|
|
77
80
|
method: 'POST',
|
|
78
81
|
headers: {
|
|
79
|
-
|
|
82
|
+
...authHeaders,
|
|
80
83
|
'content-type': 'application/json'
|
|
81
84
|
},
|
|
82
85
|
body: JSON.stringify({
|
|
@@ -599,7 +599,7 @@ export async function generateGptImage2CalloutReview(input, opts = {}) {
|
|
|
599
599
|
codexLb: allowCodexLbApiFallback ? opts.openai?.codexLb || capability?.codex_lb || null : null,
|
|
600
600
|
codexLbTarget,
|
|
601
601
|
codexLbAuthTransport: capability?.codex_lb?.cli_contract === true
|
|
602
|
-
? '
|
|
602
|
+
? 'authorization-bearer'
|
|
603
603
|
: codexLbTarget?.auth_transport,
|
|
604
604
|
allowCodexLbApiFallback
|
|
605
605
|
};
|
|
@@ -689,9 +689,7 @@ function codexLbImagesApiAuth(opts, codexLb, target) {
|
|
|
689
689
|
return {
|
|
690
690
|
apiKey: codexLbKey || null,
|
|
691
691
|
auth_source: envKey,
|
|
692
|
-
auth_transport: opts.codexLbAuthTransport || target?.auth_transport ||
|
|
693
|
-
? 'x-codex-lb-api-key'
|
|
694
|
-
: 'authorization-bearer'),
|
|
692
|
+
auth_transport: opts.codexLbAuthTransport || target?.auth_transport || 'authorization-bearer',
|
|
695
693
|
api_key_source: target?.api_key_source || null,
|
|
696
694
|
responses_model: responsesImagegenModel(opts) || target?.model || '',
|
|
697
695
|
responses_model_source: responsesImagegenModel(opts) ? 'explicit' : target?.model_source || null,
|
|
@@ -109,12 +109,12 @@ async function detectCodexLbImagegenAuth(opts = {}, env = process.env) {
|
|
|
109
109
|
const requiresOpenAiAuth = tomlBoolean(block, 'requires_openai_auth');
|
|
110
110
|
const bearerEnvKey = tomlString(block, 'env_key');
|
|
111
111
|
const gatewayHeaderEnvKey = tomlEnvHttpHeader(block, 'X-Codex-LB-API-Key');
|
|
112
|
-
const envKey =
|
|
112
|
+
const envKey = bearerEnvKey || gatewayHeaderEnvKey;
|
|
113
113
|
const cliContract = providerConfigured
|
|
114
114
|
&& tomlString(block, 'name') === 'codex-lb'
|
|
115
115
|
&& tomlString(block, 'wire_api') === 'responses'
|
|
116
|
-
&&
|
|
117
|
-
&& !
|
|
116
|
+
&& bearerEnvKey === 'CODEX_LB_API_KEY'
|
|
117
|
+
&& !gatewayHeaderEnvKey
|
|
118
118
|
&& requiresOpenAiAuth === false;
|
|
119
119
|
const baseUrl = tomlString(block, 'base_url') || String(env.CODEX_LB_BASE_URL || '').trim();
|
|
120
120
|
const envPath = opts.codexLbEnvPath || path.join(codexHome, 'sks-codex-lb.env');
|
|
@@ -147,9 +147,7 @@ async function detectCodexLbImagegenAuth(opts = {}, env = process.env) {
|
|
|
147
147
|
receiptPath: routingTruthReceiptPath
|
|
148
148
|
}).catch(() => null);
|
|
149
149
|
const configuredHost = publicUrlHost(baseUrl);
|
|
150
|
-
const routingAuthTransport =
|
|
151
|
-
? 'x-codex-lb-api-key'
|
|
152
|
-
: 'authorization-bearer';
|
|
150
|
+
const routingAuthTransport = 'authorization-bearer';
|
|
153
151
|
const routingTruthContextMatches = routingTruth?.selected === selected
|
|
154
152
|
&& routingTruth?.configured_host === configuredHost
|
|
155
153
|
&& routingTruth?.auth_transport === routingAuthTransport;
|
package/dist/core/init.js
CHANGED
|
@@ -695,7 +695,7 @@ export async function initProject(root, opts = {}) {
|
|
|
695
695
|
next = shouldSelectCodexLb
|
|
696
696
|
? upsertTopLevelTomlString(next, 'model_provider', 'codex-lb')
|
|
697
697
|
: removeTopLevelTomlKeyIfValue(next, 'model_provider', 'codex-lb');
|
|
698
|
-
next = upsertTomlTable(next, 'model_providers.codex-lb', `[model_providers.codex-lb]\nname = "codex-lb"\nbase_url = "${baseUrl}"\nwire_api = "responses"\
|
|
698
|
+
next = upsertTomlTable(next, 'model_providers.codex-lb', `[model_providers.codex-lb]\nname = "codex-lb"\nbase_url = "${baseUrl}"\nwire_api = "responses"\nenv_key = "CODEX_LB_API_KEY"\nsupports_websockets = true\nrequires_openai_auth = false`);
|
|
699
699
|
return `${next.trim()}\n`;
|
|
700
700
|
}
|
|
701
701
|
function parseCodexLbEnvKey(text = '') {
|
|
@@ -14,8 +14,8 @@ export async function resolveProviderContext(input = {}) {
|
|
|
14
14
|
const codexLbEnvKey = codexLbGatewayHeaderEnvKey || codexLbBearerEnvKey;
|
|
15
15
|
const codexLbRequiresOpenAiAuth = codexLbProviderRequiresOpenAiAuth(configText);
|
|
16
16
|
const codexLbProviderValid = codexLbProviderBlockPresent
|
|
17
|
-
&&
|
|
18
|
-
&&
|
|
17
|
+
&& codexLbBearerEnvKey === 'CODEX_LB_API_KEY'
|
|
18
|
+
&& codexLbGatewayHeaderEnvKey === null
|
|
19
19
|
&& codexLbRequiresOpenAiAuth === false;
|
|
20
20
|
const openaiKey = Boolean(String(env.OPENAI_API_KEY || '').trim());
|
|
21
21
|
const lbKey = Boolean(String((codexLbEnvKey ? env[codexLbEnvKey] : env.CODEX_LB_API_KEY) || env.CODEX_LB_API_KEY || '').trim());
|
package/dist/core/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '8.1.
|
|
1
|
+
export const PACKAGE_VERSION = '8.1.2';
|
|
@@ -79,6 +79,7 @@ extension ProvidersViewController {
|
|
|
79
79
|
_ = self.operations.update(snapshot, state: proved ? .succeeded : .failed, stage: "complete", progress: 1, summary: proved ? "Codex LB connection proved" : "Codex LB connection proof failed")
|
|
80
80
|
if let proof = proof, result.code == 0 {
|
|
81
81
|
self.codexLbSelectedNow = true
|
|
82
|
+
self.codexLbProvedNow = true
|
|
82
83
|
self.cliProviderStatus.stringValue = "Codex LB mode selected · connection proved by one live response."
|
|
83
84
|
ControlKit.setBadge(self.activeProviderBadge, text: "Codex LB · active · model \(proof.model) · \(proof.latencyMs) ms", tone: .ok)
|
|
84
85
|
self.renderCliConnectStages(
|
|
@@ -91,14 +92,16 @@ extension ProvidersViewController {
|
|
|
91
92
|
)
|
|
92
93
|
return
|
|
93
94
|
}
|
|
95
|
+
self.codexLbProvedNow = false
|
|
94
96
|
let selected = self.codexLbSelectedNow
|
|
97
|
+
let code = self.publicFailureCode(parsed)
|
|
95
98
|
let failure = parsed.map(CodexLbConnectTestTruth.validationFailure(from:))
|
|
96
99
|
?? "structured connect-test JSON was not returned"
|
|
97
100
|
self.cliProviderStatus.stringValue = selected
|
|
98
|
-
? "Codex LB mode is selected, but connection proof failed · \(failure)."
|
|
99
|
-
: "Codex LB connection proof failed and provider selection is unconfirmed · \(failure)."
|
|
101
|
+
? "Codex LB mode is selected, but connection proof failed · \(code) · \(failure)."
|
|
102
|
+
: "Codex LB connection proof failed and provider selection is unconfirmed · \(code) · \(failure)."
|
|
100
103
|
if selected {
|
|
101
|
-
ControlKit.setBadge(self.activeProviderBadge, text: "Codex LB · selected · connection proof failed", tone: .warning)
|
|
104
|
+
ControlKit.setBadge(self.activeProviderBadge, text: "Codex LB · selected · \(code) · connection proof failed", tone: .warning)
|
|
102
105
|
}
|
|
103
106
|
self.renderCliConnectStages(
|
|
104
107
|
progress: 3,
|
|
@@ -106,8 +109,8 @@ extension ProvidersViewController {
|
|
|
106
109
|
request: "completed without valid proof",
|
|
107
110
|
response: "failed",
|
|
108
111
|
result: selected
|
|
109
|
-
? "Mode is selected, but connection proof failed · \(failure). Use Run Connect Test to retry."
|
|
110
|
-
: "Connection proof failed · \(failure). Confirm the provider, then retry.",
|
|
112
|
+
? "\(code) · Mode is selected, but connection proof failed · \(failure). Use Run Connect Test to retry."
|
|
113
|
+
: "\(code) · Connection proof failed · \(failure). Confirm the provider, then retry.",
|
|
111
114
|
tone: .systemRed
|
|
112
115
|
)
|
|
113
116
|
}
|
|
@@ -4,7 +4,7 @@ extension ProvidersViewController {
|
|
|
4
4
|
func makeActiveProviderCard() -> NSBox {
|
|
5
5
|
NativeView.card(
|
|
6
6
|
title: "Active Provider",
|
|
7
|
-
subtitle: "
|
|
7
|
+
subtitle: "Live provider path. Codex LB uses Authorization: Bearer. Switch with Use Codex LB, OpenRouter Activate, or Advanced · Use ChatGPT OAuth Only.",
|
|
8
8
|
views: [activeProviderBadge, oauthCredentialStatus]
|
|
9
9
|
)
|
|
10
10
|
}
|
|
@@ -22,12 +22,17 @@ extension ProvidersViewController {
|
|
|
22
22
|
ControlKit.setBadge(
|
|
23
23
|
activeProviderBadge,
|
|
24
24
|
text: chatgptOauthPresentNow
|
|
25
|
-
? "Codex LB
|
|
26
|
-
: "Codex LB
|
|
25
|
+
? "Codex LB · Desktop Bridge · ChatGPT OAuth + built-in OpenAI"
|
|
26
|
+
: "Codex LB · Desktop Bridge · ChatGPT OAuth missing — run codex login",
|
|
27
27
|
tone: chatgptOauthPresentNow ? .ok : .warning
|
|
28
28
|
)
|
|
29
29
|
} else if codexLbSelectedNow {
|
|
30
|
-
|
|
30
|
+
// Measured CLI selection is not legacy migration (that badge comes only from refresh snapshot markers).
|
|
31
|
+
if codexLbProvedNow {
|
|
32
|
+
ControlKit.setBadge(activeProviderBadge, text: "Codex LB · CLI · verified", tone: .ok)
|
|
33
|
+
} else {
|
|
34
|
+
ControlKit.setBadge(activeProviderBadge, text: "Codex LB · CLI · selected · connection unproved", tone: .warning)
|
|
35
|
+
}
|
|
31
36
|
} else if chatgptOauthPresentNow {
|
|
32
37
|
ControlKit.setBadge(activeProviderBadge, text: "ChatGPT OAuth mode · built-in OpenAI models", tone: .neutral)
|
|
33
38
|
} else {
|
|
@@ -129,7 +134,7 @@ extension ProvidersViewController {
|
|
|
129
134
|
let summary = self.describeOpenRouterStatus(json)
|
|
130
135
|
self.openRouterStatus.stringValue = result.code == 0
|
|
131
136
|
? summary
|
|
132
|
-
: "\(summary) \(self.structuredPublicDetail(json, fallback: result.output))"
|
|
137
|
+
: "\(summary) \(self.structuredPublicDetail(json, fallback: result.output, codePrefix: "E-OR"))"
|
|
133
138
|
}
|
|
134
139
|
self.openRouterCredentialStatus.textColor = keyValidated ? .secondaryLabelColor : .systemOrange
|
|
135
140
|
if !keyValidated { self.clearOpenRouterModels(reason: "Credential not validated · model and child-agent lists are hidden. Choose Reconnect, then Refresh Models.") }
|
|
@@ -170,7 +175,7 @@ extension ProvidersViewController {
|
|
|
170
175
|
return
|
|
171
176
|
}
|
|
172
177
|
guard result.code == 0, json["ok"] as? Bool == true else {
|
|
173
|
-
self.clearOpenRouterModels(reason: "Catalog unavailable · \(self.structuredPublicDetail(json, fallback: result.output)) No model list is exposed.")
|
|
178
|
+
self.clearOpenRouterModels(reason: "Catalog unavailable · \(self.structuredPublicDetail(json, fallback: result.output, codePrefix: "E-OR")) No model list is exposed.")
|
|
174
179
|
return
|
|
175
180
|
}
|
|
176
181
|
guard json["authenticated"] as? Bool == true else {
|
|
@@ -264,7 +269,8 @@ extension ProvidersViewController {
|
|
|
264
269
|
credential: .openRouterApiKey,
|
|
265
270
|
statusLabel: openRouterStatus,
|
|
266
271
|
successSummary: "OpenRouter key saved",
|
|
267
|
-
failSummary: "OpenRouter key save failed"
|
|
272
|
+
failSummary: "OpenRouter key save failed",
|
|
273
|
+
codePrefix: "E-OR"
|
|
268
274
|
)
|
|
269
275
|
}
|
|
270
276
|
|
|
@@ -289,7 +295,7 @@ extension ProvidersViewController {
|
|
|
289
295
|
let json = self.json(result.output)
|
|
290
296
|
let ok = result.code == 0 && json?["ok"] as? Bool == true
|
|
291
297
|
let status = json?["status"] as? String ?? (ok ? "connected" : "failed")
|
|
292
|
-
let detail = self.structuredPublicDetail(json, fallback: result.output)
|
|
298
|
+
let detail = self.structuredPublicDetail(json, fallback: result.output, codePrefix: "E-OR")
|
|
293
299
|
_ = self.operations.update(snapshot, state: ok ? .succeeded : .failed, stage: "complete", progress: 1, summary: ok ? "OpenRouter connection ready" : "OpenRouter connection needs action")
|
|
294
300
|
self.openRouterStatus.stringValue = ok
|
|
295
301
|
? "Connection test passed · \(model) · \(status). Next: activate it if you want it as the main model."
|
|
@@ -347,7 +353,7 @@ extension ProvidersViewController {
|
|
|
347
353
|
let json = self.json(result.output)
|
|
348
354
|
let ok = result.code == 0 && json?["ok"] as? Bool == true
|
|
349
355
|
let status = json?["status"] as? String ?? (ok ? "restored" : "failed")
|
|
350
|
-
let detail = self.structuredPublicDetail(json, fallback: result.output)
|
|
356
|
+
let detail = self.structuredPublicDetail(json, fallback: result.output, codePrefix: "E-OR")
|
|
351
357
|
// Routing rolls back before the sidebar retag, so a blocked retag is
|
|
352
358
|
// partial success: the snapshot is kept and re-running finishes it.
|
|
353
359
|
let sidebarBlocked = status.hasPrefix("restored_sidebar")
|
|
@@ -415,7 +421,7 @@ extension ProvidersViewController {
|
|
|
415
421
|
} else if configApplied {
|
|
416
422
|
self.openRouterStatus.stringValue = "Configuration saved · main model \(model) is selected, but Codex App did not restart. Next: reopen Codex App, then verify status."
|
|
417
423
|
} else {
|
|
418
|
-
self.openRouterStatus.stringValue = "Activation incomplete · requested \(model), observed \(activeModel ?? "unknown") · \(self.structuredPublicDetail(activationJson, fallback: activation.output))"
|
|
424
|
+
self.openRouterStatus.stringValue = "Activation incomplete · requested \(model), observed \(activeModel ?? "unknown") · \(self.structuredPublicDetail(activationJson, fallback: activation.output, codePrefix: "E-OR"))"
|
|
419
425
|
}
|
|
420
426
|
self.refreshOpenRouterStatus()
|
|
421
427
|
}
|
|
@@ -426,14 +432,60 @@ extension ProvidersViewController {
|
|
|
426
432
|
openRouterModelField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
427
433
|
}
|
|
428
434
|
|
|
429
|
-
|
|
435
|
+
/// One shared failure format: stable code · HTTP · transport · public message · Next: one action.
|
|
436
|
+
/// `codePrefix` keeps codes honest per surface (E-LB / E-OR); the neutral default next-step keeps OpenRouter failures from advertising Codex LB recovery actions.
|
|
437
|
+
func structuredPublicDetail(_ json: [String: Any]?, fallback: String, codePrefix: String = "E-LB", fallbackNext: String = "review the key, model id, and network, then retry") -> String {
|
|
438
|
+
let code = publicFailureCode(json, codePrefix: codePrefix)
|
|
439
|
+
let http = publicHttpStatus(json).map { "HTTP \($0)" }
|
|
440
|
+
let transport = publicAuthTransport(json)
|
|
441
|
+
let error = publicError(json)
|
|
442
|
+
var parts: [String] = [code]
|
|
443
|
+
if let http { parts.append(http) }
|
|
444
|
+
if let transport { parts.append("transport \(transport)") }
|
|
445
|
+
if let error, !error.isEmpty { parts.append(error) }
|
|
430
446
|
if let blockers = json?["blockers"] as? [String], !blockers.isEmpty {
|
|
431
|
-
|
|
432
|
-
|
|
447
|
+
parts.append(blockers.prefix(2).joined(separator: ", "))
|
|
448
|
+
}
|
|
449
|
+
let next = firstGuidance(json) ?? (json?["hint"] as? String) ?? fallbackNext
|
|
450
|
+
return "\(parts.joined(separator: " · ")). Next: \(next)."
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
func publicFailureCode(_ json: [String: Any]?, codePrefix: String = "E-LB") -> String {
|
|
454
|
+
let blockers = json?["blockers"] as? [String] ?? []
|
|
455
|
+
let truth = json?["routing_truth"] as? [String: Any]
|
|
456
|
+
let truthBlockers = truth?["blockers"] as? [String] ?? []
|
|
457
|
+
let all = blockers + truthBlockers
|
|
458
|
+
if all.contains(where: { $0.contains("auth_rejected") || $0.contains("gateway_auth_rejected") }) {
|
|
459
|
+
return "\(codePrefix)-AUTH"
|
|
460
|
+
}
|
|
461
|
+
if all.contains(where: { $0.contains("unreachable") }) { return "\(codePrefix)-UNREACHABLE" }
|
|
462
|
+
if all.contains(where: { $0.contains("legacy") && $0.contains("migration") }) { return "\(codePrefix)-LEGACY-MIGRATE" }
|
|
463
|
+
if let http = publicHttpStatus(json), http >= 400 { return "\(codePrefix)-HTTP-\(http)" }
|
|
464
|
+
if json == nil || json?["ok"] as? Bool == false { return "\(codePrefix)-FAILED" }
|
|
465
|
+
return "\(codePrefix)-INFO"
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
func publicHttpStatus(_ json: [String: Any]?) -> Int? {
|
|
469
|
+
if let value = json?["http_status"] as? Int { return value }
|
|
470
|
+
if let value = (json?["http_status"] as? NSNumber)?.intValue { return value }
|
|
471
|
+
let truth = json?["routing_truth"] as? [String: Any]
|
|
472
|
+
if let value = truth?["http_status"] as? Int { return value }
|
|
473
|
+
return (truth?["http_status"] as? NSNumber)?.intValue
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
func publicAuthTransport(_ json: [String: Any]?) -> String? {
|
|
477
|
+
if let value = json?["gateway_auth_transport"] as? String, !value.isEmpty { return value }
|
|
478
|
+
if let value = json?["auth_transport"] as? String, !value.isEmpty { return value }
|
|
479
|
+
let truth = json?["routing_truth"] as? [String: Any]
|
|
480
|
+
if let value = truth?["auth_transport"] as? String, !value.isEmpty { return value }
|
|
481
|
+
return nil
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
func firstGuidance(_ json: [String: Any]?) -> String? {
|
|
485
|
+
if let rows = json?["guidance"] as? [String] {
|
|
486
|
+
return rows.first { !$0.isEmpty }
|
|
433
487
|
}
|
|
434
|
-
|
|
435
|
-
if let hint = json?["hint"] as? String, !hint.isEmpty { return "Next: \(hint)." }
|
|
436
|
-
return "Next: \(NativeView.redactPreview(fallback))"
|
|
488
|
+
return nil
|
|
437
489
|
}
|
|
438
490
|
|
|
439
491
|
func publicError(_ json: [String: Any]?) -> String? {
|
|
@@ -198,7 +198,7 @@ extension ProvidersViewController {
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
func promptForSecretKey(window: NSWindow, sheetTitle: String, sheetMessage: String, placeholder: String, args: [String], kind: String, title: String, credential: SKSKeychainCredential, statusLabel: NSTextField, successSummary: String, failSummary: String) {
|
|
201
|
+
func promptForSecretKey(window: NSWindow, sheetTitle: String, sheetMessage: String, placeholder: String, args: [String], kind: String, title: String, credential: SKSKeychainCredential, statusLabel: NSTextField, successSummary: String, failSummary: String, codePrefix: String = "E-LB") {
|
|
202
202
|
AlertFactory.textSheet(window: window, title: sheetTitle, message: sheetMessage, secure: true, placeholder: placeholder) { [weak self] key in
|
|
203
203
|
guard let self = self, let key = key else { return }
|
|
204
204
|
guard !self.busy else { statusLabel.stringValue = "Another provider action is already running."; return }
|
|
@@ -229,7 +229,7 @@ extension ProvidersViewController {
|
|
|
229
229
|
}
|
|
230
230
|
statusLabel.stringValue = "Configuration saved, but Keychain was not applied · \(reason). Authentication Required: choose Reconnect after repairing Keychain."
|
|
231
231
|
} else {
|
|
232
|
-
statusLabel.stringValue = "\(failSummary) · \(self.structuredPublicDetail(parsed, fallback: result.output))"
|
|
232
|
+
statusLabel.stringValue = "\(failSummary) · \(self.structuredPublicDetail(parsed, fallback: result.output, codePrefix: codePrefix))"
|
|
233
233
|
}
|
|
234
234
|
self.refresh()
|
|
235
235
|
}
|
|
@@ -288,6 +288,7 @@ extension ProvidersViewController {
|
|
|
288
288
|
let bridgeSelected = expectedMode == "desktop-native-bridge"
|
|
289
289
|
self.desktopFullRoutingNow = false
|
|
290
290
|
self.codexLbSelectedNow = false
|
|
291
|
+
self.codexLbProvedNow = false
|
|
291
292
|
self.chatgptOauthPresentNow = routing?.chatgptOauthPresent ?? self.chatgptOauthPresentNow
|
|
292
293
|
self.renderActiveProviderSummary()
|
|
293
294
|
if bridgeSelected { self.renderMeasuredRoutingBadge(nil, routeExpected: true) }
|
|
@@ -55,6 +55,9 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
55
55
|
var capabilityRows: [String: NSTextField] = [:]
|
|
56
56
|
var desktopFullRoutingNow = false
|
|
57
57
|
var codexLbSelectedNow = false
|
|
58
|
+
// True only after a measured verified route or a live connection proof, so
|
|
59
|
+
// unrelated status probes never downgrade a proven badge to "unproved".
|
|
60
|
+
var codexLbProvedNow = false
|
|
58
61
|
var chatgptOauthPresentNow = false
|
|
59
62
|
var openRouterSelectedNow = false
|
|
60
63
|
var openRouterCredentialValidatedNow = false
|
|
@@ -115,17 +118,11 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
115
118
|
providerApplyStatus.setAccessibilityIdentifier("sks-center-provider-apply-status")
|
|
116
119
|
providerApplyStatus.setAccessibilityLabel("Provider apply stage receipts")
|
|
117
120
|
actionButtons = [enableDesktop, verifyDesktop, disableDesktop, configureCli, reconnectOpenRouter, openCodexSignIn, useCli, testCli, copyCli, fastOn, fastOff]
|
|
118
|
-
let desktop = NativeView.card(
|
|
119
|
-
title: "Advanced · Desktop Bridge / ChatGPT Identity Mode",
|
|
120
|
-
subtitle: "Optional bridge mode: keeps ChatGPT OAuth identity and the built-in OpenAI provider while routing model traffic through the local codex-lb bridge. Use ChatGPT OAuth Only removes bridge routing without touching login.",
|
|
121
|
-
views: [providerStatus, ControlKit.actionRow([enableDesktop, verifyDesktop], trailing: [disableDesktop])]
|
|
122
|
-
)
|
|
123
121
|
let cli = NativeView.card(
|
|
124
122
|
title: "Codex LB · Credentials & Primary Provider",
|
|
125
|
-
subtitle: "Saves
|
|
123
|
+
subtitle: "Saves host + API key, then Use Codex LB selects the atomic CLI provider (Authorization: Bearer via CODEX_LB_API_KEY). No transport picker.",
|
|
126
124
|
views: [
|
|
127
125
|
cliProviderStatus, codexLbKeychainStatus,
|
|
128
|
-
NativeView.detail("One-off command: \(cliLaunchCommand)"),
|
|
129
126
|
ControlKit.actionRow([configureCli, useCli]),
|
|
130
127
|
makeCliConnectProgressView(),
|
|
131
128
|
ControlKit.actionRow([testCli, copyCli])
|
|
@@ -142,19 +139,24 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
142
139
|
)
|
|
143
140
|
view = NativeView.page([
|
|
144
141
|
NativeView.row([NativeView.title("Providers & Models"), globalSpinner]),
|
|
145
|
-
NativeView.detail("
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
views: [codexLbKeychainStatus, openRouterKeychainStatus, oauthCredentialStatus, NativeView.row([configureCli, reconnectOpenRouter, openCodexSignIn])]
|
|
150
|
-
),
|
|
142
|
+
NativeView.detail("SKS selects one provider path: Codex LB (Authorization: Bearer), OpenRouter, or ChatGPT OAuth. Secrets go through stdin and are never written to operation logs."),
|
|
143
|
+
makeActiveProviderCard(),
|
|
144
|
+
cli,
|
|
145
|
+
makeOpenRouterCard(),
|
|
151
146
|
NativeView.card(
|
|
152
|
-
title: "
|
|
153
|
-
subtitle: "
|
|
154
|
-
views: [
|
|
147
|
+
title: "Advanced",
|
|
148
|
+
subtitle: "You rarely need this section. Desktop Bridge is only for keeping ChatGPT sign-in while routing through codex-lb (or a custom-header-only gateway) — if Use Codex LB above works, skip it.",
|
|
149
|
+
views: [
|
|
150
|
+
oauthCredentialStatus,
|
|
151
|
+
NativeView.row([openCodexSignIn]),
|
|
152
|
+
providerStatus,
|
|
153
|
+
ControlKit.actionRow([enableDesktop, verifyDesktop], trailing: [disableDesktop]),
|
|
154
|
+
providerApplyStatus,
|
|
155
|
+
makeCapabilityMatrixCard()
|
|
156
|
+
]
|
|
155
157
|
),
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
makeRoleModelsCard(),
|
|
159
|
+
fast
|
|
158
160
|
])
|
|
159
161
|
}
|
|
160
162
|
func refreshOnAppear() { refresh() }
|
|
@@ -211,8 +213,20 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
211
213
|
: "Authentication Required: ChatGPT OAuth is not connected. Choose Open Codex sign-in; no login window was opened automatically."
|
|
212
214
|
self.oauthCredentialStatus.textColor = snapshot.chatgptOauthPresent ? .secondaryLabelColor : .systemOrange
|
|
213
215
|
self.renderCatalogSyncStatus(json)
|
|
214
|
-
self.
|
|
215
|
-
|
|
216
|
+
self.codexLbProvedNow = snapshot.mode == "cli-provider" && measuredRoute?.active == true
|
|
217
|
+
// Single decision ladder — exactly one badge writer per refresh.
|
|
218
|
+
if snapshot.legacyCodexLbSelected {
|
|
219
|
+
ControlKit.setBadge(self.activeProviderBadge, text: "E-LB-LEGACY-MIGRATE · run migrate-legacy-desktop", tone: .warning)
|
|
220
|
+
} else if snapshot.mode == "cli-provider", let route = measuredRoute, route.selected, !route.active {
|
|
221
|
+
let authFail = route.status == "auth_rejected" || route.blockers.contains(where: { $0.contains("auth") })
|
|
222
|
+
let code = authFail ? "E-LB-AUTH" : "E-LB-DEGRADED"
|
|
223
|
+
let http = route.httpStatus.map { "HTTP \($0)" } ?? route.status
|
|
224
|
+
ControlKit.setBadge(self.activeProviderBadge, text: "Codex LB · CLI · \(code) · \(http)", tone: .warning)
|
|
225
|
+
} else if snapshot.desktopFullRouting {
|
|
226
|
+
self.renderMeasuredRoutingBadge(measuredRoute, routeExpected: true)
|
|
227
|
+
} else {
|
|
228
|
+
self.renderActiveProviderSummary()
|
|
229
|
+
}
|
|
216
230
|
if !self.busy {
|
|
217
231
|
self.providerStatus.stringValue = self.describeDesktopStatus(snapshot)
|
|
218
232
|
self.cliProviderStatus.stringValue = self.describeCliStatus(snapshot, measuredRoute: measuredRoute)
|
|
@@ -242,7 +256,10 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
242
256
|
let oauth = snapshot.chatgptOauthPresent
|
|
243
257
|
? "ChatGPT OAuth present"
|
|
244
258
|
: "ChatGPT OAuth missing — sign in with ChatGPT"
|
|
245
|
-
return "
|
|
259
|
+
return "Desktop Bridge mode: enabled · \(oauth) · built-in OpenAI via bridge. Run Verify Capabilities for evidence."
|
|
260
|
+
}
|
|
261
|
+
if snapshot.mode == "cli-provider" {
|
|
262
|
+
return "Desktop Bridge: off · the Codex LB CLI provider path handles routing, so the bridge is not used. Desktop Bridge Mode stays an optional alternative."
|
|
246
263
|
}
|
|
247
264
|
if snapshot.mode == "disabled" {
|
|
248
265
|
return snapshot.chatgptOauthPresent
|
|
@@ -357,23 +374,9 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
357
374
|
}
|
|
358
375
|
@objc private func setDomainAndKey() {
|
|
359
376
|
guard let window = view.window else { return }
|
|
360
|
-
AlertFactory.textSheet(window: window, title: "Codex LB Domain", message: "Enter a hostname or full base URL. https:// is optional — SKS adds https:// and /backend-api/codex when missing.\nExamples: lb.example.com or https://lb.example.com", secure: false, placeholder: "https://lb.example.com") { [weak self] host in
|
|
377
|
+
AlertFactory.textSheet(window: window, title: "Codex LB Domain", message: "Enter a hostname or full base URL. https:// is optional — SKS adds https:// and /backend-api/codex when missing.\nExamples: lb.example.com or https://lb.example.com\n\nSKS always sends the API key as Authorization: Bearer (Codex env_key). There is no custom-header transport picker.", secure: false, placeholder: "https://lb.example.com") { [weak self] host in
|
|
361
378
|
guard let self = self, let host = host else { return }
|
|
362
|
-
|
|
363
|
-
// that wants `Authorization: Bearer` rejects the custom header with 401,
|
|
364
|
-
// which Desktop surfaces as "codex-lb auth not recognized".
|
|
365
|
-
AlertFactory.choiceSheet(
|
|
366
|
-
window: window,
|
|
367
|
-
title: "Codex LB Gateway Key Transport",
|
|
368
|
-
message: "How does this codex-lb gateway expect the API key? Choose Authorization bearer if the gateway answers 401 \"Missing API key in Authorization header\" to the custom header.",
|
|
369
|
-
choices: [
|
|
370
|
-
("custom-header", "X-Codex-LB-API-Key custom header (default)"),
|
|
371
|
-
("bearer-compat", "Authorization: Bearer compatibility")
|
|
372
|
-
]
|
|
373
|
-
) { [weak self] transport in
|
|
374
|
-
guard let self = self, let transport = transport else { return }
|
|
375
|
-
self.promptForKey(window: window, args: ["codex-lb", "setup", "--host", host, "--gateway-auth", transport, "--api-key-stdin", "--yes", "--write-env-file", "--json"], kind: "codex-lb-center-setup", title: "Save Codex LB credentials")
|
|
376
|
-
}
|
|
379
|
+
self.promptForKey(window: window, args: ["codex-lb", "setup", "--host", host, "--gateway-auth", "bearer-compat", "--api-key-stdin", "--yes", "--write-env-file", "--json"], kind: "codex-lb-center-setup", title: "Save Codex LB credentials")
|
|
377
380
|
}
|
|
378
381
|
}
|
|
379
382
|
private func promptForKey(window: NSWindow, args: [String], kind: String, title: String) {
|
|
@@ -448,20 +451,23 @@ final class ProvidersViewController: NSViewController, ControlCenterPage, NSText
|
|
|
448
451
|
_ = self.operations.update(snapshot, state: ok ? .succeeded : .failed, stage: "complete", progress: 1, summary: ok ? "CLI provider configured" : "CLI provider configuration needs action")
|
|
449
452
|
guard ok else {
|
|
450
453
|
self.setBusy(false)
|
|
451
|
-
|
|
454
|
+
// structuredPublicDetail already leads with the stable code.
|
|
455
|
+
let detail = self.structuredPublicDetail(parsed, fallback: result.output, fallbackNext: "Reconnect Codex LB credential, then Use Codex LB / Run Connect Test")
|
|
456
|
+
let code = self.publicFailureCode(parsed)
|
|
452
457
|
self.cliProviderStatus.stringValue = "CLI provider was not confirmed · \(detail)"
|
|
453
458
|
self.renderCliConnectStages(
|
|
454
459
|
progress: 1,
|
|
455
|
-
configuration: "failed",
|
|
460
|
+
configuration: "failed · \(code)",
|
|
456
461
|
request: "not run",
|
|
457
462
|
response: "not received",
|
|
458
|
-
result:
|
|
463
|
+
result: detail,
|
|
459
464
|
tone: .systemRed
|
|
460
465
|
)
|
|
461
466
|
return
|
|
462
467
|
}
|
|
463
468
|
self.setBusy(false)
|
|
464
469
|
self.codexLbSelectedNow = true
|
|
470
|
+
self.codexLbProvedNow = false
|
|
465
471
|
self.desktopFullRoutingNow = false
|
|
466
472
|
self.openRouterSelectedNow = false
|
|
467
473
|
self.routerSelectedNow = false
|
|
@@ -16,7 +16,7 @@ await fs.writeFile(path.join(home, '.codex', 'config.toml'), [
|
|
|
16
16
|
'name = "codex-lb"',
|
|
17
17
|
'base_url = "https://lb.example.test/backend-api/codex"',
|
|
18
18
|
'wire_api = "responses"',
|
|
19
|
-
'
|
|
19
|
+
'env_key = "CODEX_LB_API_KEY"',
|
|
20
20
|
'supports_websockets = true',
|
|
21
21
|
'requires_openai_auth = false',
|
|
22
22
|
''
|
|
@@ -44,7 +44,7 @@ const requestedOnly = await runFastCheck({
|
|
|
44
44
|
const ok = chain.ok === true
|
|
45
45
|
&& chain.status === 'fast_verified'
|
|
46
46
|
&& calls[0]?.body?.service_tier === 'priority'
|
|
47
|
-
&& calls.every((call) => call.authorization ===
|
|
47
|
+
&& calls.every((call) => call.authorization === 'Bearer sk-fixture' && call.gatewayKey === null)
|
|
48
48
|
&& requestedOnly.ok === false
|
|
49
49
|
&& requestedOnly.status === 'fast_requested_but_actual_unverified'
|
|
50
50
|
&& requestedOnly.blockers.includes('codex_lb_actual_fast_service_tier_unverified');
|
|
@@ -52,7 +52,7 @@ console.log(JSON.stringify({
|
|
|
52
52
|
schema: 'sks.codex-lb-fast-mode-truth-check.v1',
|
|
53
53
|
ok,
|
|
54
54
|
priority_request_sent: calls[0]?.body?.service_tier === 'priority',
|
|
55
|
-
|
|
55
|
+
bearer_transport_verified: calls.every((call) => call.authorization === 'Bearer sk-fixture' && call.gatewayKey === null),
|
|
56
56
|
verified_case: chain,
|
|
57
57
|
requested_only_case: requestedOnly,
|
|
58
58
|
blockers: ok ? [] : ['codex_lb_fast_mode_truth_check_failed']
|
|
@@ -124,9 +124,9 @@ function assertConfig(text, label, options = {}) {
|
|
|
124
124
|
...(hasTable(text, 'model_providers.codex-lb') ? [] : ['codex_lb_provider_table_missing']),
|
|
125
125
|
...(tableKey(text, 'model_providers.codex-lb', 'name') === 'codex-lb' ? [] : ['cli_provider_name_not_codex_lb']),
|
|
126
126
|
...(tableKey(text, 'model_providers.codex-lb', 'requires_openai_auth') === 'false' ? [] : ['cli_provider_requires_openai_auth_not_false']),
|
|
127
|
-
...(
|
|
127
|
+
...(tableKey(text, 'model_providers.codex-lb', 'env_key') === 'CODEX_LB_API_KEY'
|
|
128
128
|
? []
|
|
129
|
-
: ['
|
|
129
|
+
: ['cli_provider_env_key_missing']),
|
|
130
130
|
...(topLevelKey(text, 'model_provider') === 'openai' ? [] : ['desktop_model_provider_must_be_openai']),
|
|
131
131
|
...(expectBridge && topLevelKey(text, 'openai_base_url') !== bridgeBaseUrl ? ['native_bridge_base_url_missing'] : []),
|
|
132
132
|
...(!expectBridge && topLevelKey(text, 'openai_base_url') ? ['disabled_routing_still_has_openai_base_url'] : []),
|
|
@@ -141,13 +141,6 @@ function hasLegacyFastModeTables(text) {
|
|
|
141
141
|
function hasTable(text, table) {
|
|
142
142
|
return new RegExp(`(^|\\n)\\[${escapeRegExp(table)}\\](?=\\n|$)`).test(text);
|
|
143
143
|
}
|
|
144
|
-
function tableEnvHttpHeader(text, table, header) {
|
|
145
|
-
const tableMatch = text.match(new RegExp(`(^|\\n)\\[${escapeRegExp(table)}\\]([\\s\\S]*?)(?=\\n\\[[^\\]]+\\]|\\s*$)`));
|
|
146
|
-
const body = tableMatch?.[2] || '';
|
|
147
|
-
const match = body.match(/(?:^|\n)\s*env_http_headers\s*=\s*\{([^}]*)\}/);
|
|
148
|
-
const entries = match?.[1] || '';
|
|
149
|
-
return entries.match(new RegExp(`"${escapeRegExp(header)}"\\s*=\\s*"([^"]+)"`))?.[1] || '';
|
|
150
|
-
}
|
|
151
144
|
function tableKey(text, table, key) {
|
|
152
145
|
const match = text.match(new RegExp(`(^|\\n)\\[${escapeRegExp(table)}\\]([\\s\\S]*?)(?=\\n\\[[^\\]]+\\]|\\s*$)`));
|
|
153
146
|
const block = match?.[2] || '';
|
|
@@ -152,9 +152,9 @@ function assertFastProfile(text, label, expectBridge = true) {
|
|
|
152
152
|
...(parsed.model_provider === 'openai' ? [] : ['desktop_model_provider_must_be_openai']),
|
|
153
153
|
...(provider.name === 'codex-lb' ? [] : ['codex_lb_cli_provider_name_mismatch']),
|
|
154
154
|
...(provider.requires_openai_auth === false ? [] : ['codex_lb_cli_requires_openai_auth_not_false']),
|
|
155
|
-
...(provider.
|
|
155
|
+
...(provider.env_key === 'CODEX_LB_API_KEY'
|
|
156
156
|
? []
|
|
157
|
-
: ['
|
|
157
|
+
: ['codex_lb_cli_env_key_missing']),
|
|
158
158
|
...(provider.wire_api === 'responses' ? [] : ['codex_lb_wire_api_not_responses']),
|
|
159
159
|
...(expectBridge && parsed.openai_base_url === bridgeBaseUrl ? [] : expectBridge ? ['native_bridge_base_url_missing'] : []),
|
|
160
160
|
...(!expectBridge && parsed.openai_base_url !== undefined ? ['disabled_routing_still_has_openai_base_url'] : []),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "8.1.
|
|
4
|
+
"version": "8.1.2",
|
|
5
5
|
"description": "Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|