@bitkyc08/opencodex 2.9.1 → 2.10.0
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 +91 -449
- package/gui/dist/assets/index-OY43ubAq.css +1 -0
- package/gui/dist/assets/index-YwNnKZcL.js +67 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/package.json +1 -1
- package/src/cli/account.ts +3 -5
- package/src/cli/claude-desktop.ts +43 -7
- package/src/cli/doctor.ts +12 -0
- package/src/cli/help.ts +1 -1
- package/src/cli/provider-runtime.ts +7 -0
- package/src/cli/star-prompt.ts +25 -4
- package/src/cli/status.ts +7 -2
- package/src/codex/app-server-processes.ts +299 -54
- package/src/codex/catalog/metadata.ts +9 -11
- package/src/codex/catalog/provider-fetch.ts +10 -10
- package/src/codex/catalog/sync.ts +27 -2
- package/src/codex/catalog.ts +1 -1
- package/src/config.ts +15 -1
- package/src/lib/bun-stream-caps.ts +14 -0
- package/src/oauth/index.ts +48 -3
- package/src/providers/registry.ts +62 -0
- package/src/server/index.ts +39 -3
- package/src/server/management/agent-settings-routes.ts +40 -3
- package/src/server/management/logs-usage-routes.ts +2 -0
- package/src/server/management/provider-routes.ts +4 -1
- package/src/server/management/sidebar-routes.ts +3 -1
- package/src/server/relay-eager.ts +100 -2
- package/src/server/responses/collaboration.ts +21 -0
- package/src/server/responses/core.ts +20 -12
- package/src/service.ts +393 -14
- package/src/storage/cleanup.ts +76 -5
- package/src/storage/policy.ts +6 -1
- package/src/types.ts +2 -0
- package/src/update/index.ts +9 -2
- package/src/update/job.ts +87 -11
- package/gui/dist/assets/index-CHwf3tTD.css +0 -1
- package/gui/dist/assets/index-CuVjugeE.js +0 -67
package/src/oauth/index.ts
CHANGED
|
@@ -629,8 +629,11 @@ export function buildModelsRequest(prov: OcxProviderConfig, apiKey: string | und
|
|
|
629
629
|
* configs on the next `ocx start`, instead of only fresh installs. The live `/models` fetch stays
|
|
630
630
|
* the primary source; this keeps the static fallback (and models-not-in-/models) current.
|
|
631
631
|
*
|
|
632
|
-
* Only touches providers that are registry-managed AND still `authMode: "oauth"
|
|
633
|
-
*
|
|
632
|
+
* Only touches providers that are registry-managed AND still `authMode: "oauth"`. Preset fields
|
|
633
|
+
* are refreshed, while the registry's `liveModels` default is normally filled only when no value
|
|
634
|
+
* is stored. Antigravity has one versioned exception below because its old GUI-generated `true`
|
|
635
|
+
* cannot be distinguished from a hand-written pre-migration `true`. Persists + returns true when
|
|
636
|
+
* anything changed.
|
|
634
637
|
*/
|
|
635
638
|
function cloneProviderField(value: unknown): unknown {
|
|
636
639
|
if (Array.isArray(value)) return [...value];
|
|
@@ -658,11 +661,30 @@ const OAUTH_RECONCILE_FIELDS: (keyof OcxProviderConfig)[] = [
|
|
|
658
661
|
"preserveReasoningContentModels",
|
|
659
662
|
];
|
|
660
663
|
|
|
664
|
+
const GOOGLE_ANTIGRAVITY_PROVIDER = "google-antigravity";
|
|
665
|
+
const GOOGLE_ANTIGRAVITY_STATIC_CATALOG_VERSION = 1 as const;
|
|
666
|
+
|
|
661
667
|
export function reconcileOAuthProviders(config: OcxConfig): boolean {
|
|
662
668
|
let changed = false;
|
|
669
|
+
const migrateAntigravityStaticCatalog =
|
|
670
|
+
config.googleAntigravityStaticCatalogVersion !== GOOGLE_ANTIGRAVITY_STATIC_CATALOG_VERSION;
|
|
663
671
|
for (const [name, prov] of Object.entries(config.providers)) {
|
|
664
672
|
const def = OAUTH_PROVIDERS[name];
|
|
665
|
-
|
|
673
|
+
// Normalize the canonical row before the OAuth-only reconciliation guard. The old GUI and a
|
|
674
|
+
// manual edit both persist the same bare `true`, with no source metadata, so every ambiguous
|
|
675
|
+
// pre-marker value is reset once. A deliberate live-discovery choice can be re-enabled after
|
|
676
|
+
// the marker and is then preserved. Do this before the guard so omitted/non-OAuth authMode
|
|
677
|
+
// rows do not get stamped without actually receiving the new static default.
|
|
678
|
+
if (name === GOOGLE_ANTIGRAVITY_PROVIDER && migrateAntigravityStaticCatalog && prov.liveModels !== false) {
|
|
679
|
+
prov.liveModels = false;
|
|
680
|
+
changed = true;
|
|
681
|
+
}
|
|
682
|
+
// During the one-time Antigravity static-catalog migration, also refresh preset catalog
|
|
683
|
+
// fields when authMode is omitted or non-oauth. Otherwise liveModels flips to static while
|
|
684
|
+
// a stale models[] remains the published catalog forever.
|
|
685
|
+
const migrateAntigravityCatalogFields =
|
|
686
|
+
name === GOOGLE_ANTIGRAVITY_PROVIDER && migrateAntigravityStaticCatalog;
|
|
687
|
+
if (!def || (prov.authMode !== "oauth" && !migrateAntigravityCatalogFields)) continue;
|
|
666
688
|
const preset = def.providerConfig;
|
|
667
689
|
for (const field of OAUTH_RECONCILE_FIELDS) {
|
|
668
690
|
if (JSON.stringify(prov[field]) === JSON.stringify(preset[field])) continue;
|
|
@@ -673,12 +695,23 @@ export function reconcileOAuthProviders(config: OcxConfig): boolean {
|
|
|
673
695
|
}
|
|
674
696
|
changed = true;
|
|
675
697
|
}
|
|
698
|
+
// Before this marker existed, the GUI materialized an omitted `liveModels` as `true` on any
|
|
699
|
+
// settings save. Since persisted values have no provenance, the pre-guard normalization above
|
|
700
|
+
// intentionally resets all pre-marker `true` values once. Later choices are version-bounded.
|
|
701
|
+
if (prov.liveModels === undefined && preset.liveModels !== undefined) {
|
|
702
|
+
prov.liveModels = preset.liveModels;
|
|
703
|
+
changed = true;
|
|
704
|
+
}
|
|
676
705
|
// Heal a defaultModel that no longer exists in the refreshed list (e.g. a deprecated snapshot).
|
|
677
706
|
if (prov.defaultModel && preset.defaultModel && !(prov.models ?? []).includes(prov.defaultModel)) {
|
|
678
707
|
prov.defaultModel = preset.defaultModel;
|
|
679
708
|
changed = true;
|
|
680
709
|
}
|
|
681
710
|
}
|
|
711
|
+
if (migrateAntigravityStaticCatalog) {
|
|
712
|
+
config.googleAntigravityStaticCatalogVersion = GOOGLE_ANTIGRAVITY_STATIC_CATALOG_VERSION;
|
|
713
|
+
changed = true;
|
|
714
|
+
}
|
|
682
715
|
if (changed) saveConfig(config);
|
|
683
716
|
return changed;
|
|
684
717
|
}
|
|
@@ -739,6 +772,15 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
|
|
|
739
772
|
if (namespaceCollision) throw new Error(namespaceCollision);
|
|
740
773
|
const existing = config.providers[provider];
|
|
741
774
|
const next: OcxProviderConfig = { ...def.providerConfig };
|
|
775
|
+
// `liveModels` is a user-facing provider toggle. A registry default seeds new rows, but an
|
|
776
|
+
// explicit post-migration choice must survive re-login and the latest-config upsert. Old GUI
|
|
777
|
+
// saves and manual edits left identical pre-marker `true` values, so that ambiguous state is
|
|
778
|
+
// reset once; users who deliberately forced discovery can re-enable it after migration.
|
|
779
|
+
const preserveExistingLiveModels = provider !== GOOGLE_ANTIGRAVITY_PROVIDER
|
|
780
|
+
|| config.googleAntigravityStaticCatalogVersion === GOOGLE_ANTIGRAVITY_STATIC_CATALOG_VERSION;
|
|
781
|
+
if (preserveExistingLiveModels && typeof existing?.liveModels === "boolean") {
|
|
782
|
+
next.liveModels = existing.liveModels;
|
|
783
|
+
}
|
|
742
784
|
if (existing && getProviderRegistryEntry(provider)?.allowKeyAuthOverride === true) {
|
|
743
785
|
// Shared sanitizeApiKeyValue trim / no-CRLF checks from api-key pool writes.
|
|
744
786
|
let storedApiKey = sanitizeApiKeyValue(existing.apiKey);
|
|
@@ -762,6 +804,9 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
|
|
|
762
804
|
}
|
|
763
805
|
}
|
|
764
806
|
config.providers[provider] = next;
|
|
807
|
+
if (provider === GOOGLE_ANTIGRAVITY_PROVIDER) {
|
|
808
|
+
config.googleAntigravityStaticCatalogVersion = GOOGLE_ANTIGRAVITY_STATIC_CATALOG_VERSION;
|
|
809
|
+
}
|
|
765
810
|
}
|
|
766
811
|
|
|
767
812
|
interface RunLoginDeps {
|
|
@@ -507,6 +507,43 @@ const NEURALWATT_REASONING_HISTORY_MODELS = [
|
|
|
507
507
|
"kimi-k2.6", "kimi-k2.7-code",
|
|
508
508
|
"qwen3.5-397b", "qwen3.6-35b",
|
|
509
509
|
];
|
|
510
|
+
|
|
511
|
+
// 260728 Baseten Model APIs: `/v1/models` owns the live lineup, while these hints
|
|
512
|
+
// describe only capabilities that Baseten documents per slug. Unlisted live models
|
|
513
|
+
// intentionally inherit the empty provider ladder instead of being advertised with
|
|
514
|
+
// opencodex's generic reasoning defaults. Audio is omitted because the current proxy
|
|
515
|
+
// request model does not carry OpenAI `audio_url` parts.
|
|
516
|
+
// Evidence: https://docs.baseten.co/inference/model-apis/reasoning
|
|
517
|
+
// https://docs.baseten.co/inference/model-apis/vision
|
|
518
|
+
const BASETEN_FULL_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
519
|
+
const BASETEN_MODEL_REASONING_EFFORTS: Record<string, string[]> = {
|
|
520
|
+
"deepseek-ai/DeepSeek-V4-Pro": BASETEN_FULL_REASONING_EFFORTS,
|
|
521
|
+
"thinkingmachines/inkling": BASETEN_FULL_REASONING_EFFORTS,
|
|
522
|
+
"openai/gpt-oss-120b": BASETEN_FULL_REASONING_EFFORTS,
|
|
523
|
+
"moonshotai/Kimi-K3": ["low", "high", "max"],
|
|
524
|
+
"zai-org/GLM-5.2": ["high", "max"],
|
|
525
|
+
"zai-org/GLM-5.2-Fast": ["high", "max"],
|
|
526
|
+
};
|
|
527
|
+
const BASETEN_MODEL_REASONING_EFFORT_MAP: Record<string, Record<string, string>> = {
|
|
528
|
+
"deepseek-ai/DeepSeek-V4-Pro": { none: "none", minimal: "minimal" },
|
|
529
|
+
"thinkingmachines/inkling": { none: "none", minimal: "minimal" },
|
|
530
|
+
"openai/gpt-oss-120b": { none: "none", minimal: "minimal" },
|
|
531
|
+
"moonshotai/Kimi-K3": { none: "none" },
|
|
532
|
+
"zai-org/GLM-5.2": { none: "none" },
|
|
533
|
+
"zai-org/GLM-5.2-Fast": { none: "none" },
|
|
534
|
+
};
|
|
535
|
+
const BASETEN_MODEL_DEFAULT_REASONING_EFFORTS: Record<string, string> = {
|
|
536
|
+
"deepseek-ai/DeepSeek-V4-Pro": "medium",
|
|
537
|
+
"thinkingmachines/inkling": "high",
|
|
538
|
+
"openai/gpt-oss-120b": "medium",
|
|
539
|
+
"moonshotai/Kimi-K3": "max",
|
|
540
|
+
};
|
|
541
|
+
const BASETEN_MODEL_INPUT_MODALITIES: Record<string, string[]> = {
|
|
542
|
+
"thinkingmachines/inkling": ["text", "image"],
|
|
543
|
+
"moonshotai/Kimi-K2.6": ["text", "image"],
|
|
544
|
+
"moonshotai/Kimi-K2.7-Code": ["text", "image"],
|
|
545
|
+
"moonshotai/Kimi-K3": ["text", "image"],
|
|
546
|
+
};
|
|
510
547
|
const UMANS_MODELS = [
|
|
511
548
|
"umans-coder",
|
|
512
549
|
"umans-kimi-k2.7",
|
|
@@ -983,6 +1020,31 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
983
1020
|
},
|
|
984
1021
|
note: "Serverless text and vision-language chat models only; Hyperbolic's separate image, audio, and GPU endpoints are out of scope.",
|
|
985
1022
|
},
|
|
1023
|
+
{
|
|
1024
|
+
id: "baseten",
|
|
1025
|
+
label: "Baseten Model APIs",
|
|
1026
|
+
baseUrl: "https://inference.baseten.co/v1",
|
|
1027
|
+
adapter: "openai-chat",
|
|
1028
|
+
authKind: "key",
|
|
1029
|
+
dashboardUrl: "https://app.baseten.co/settings/api_keys",
|
|
1030
|
+
liveModels: true,
|
|
1031
|
+
preserveCustomDestination: true,
|
|
1032
|
+
// Baseten's Chat Completions contract documents parallel_tool_calls as default-on.
|
|
1033
|
+
parallelToolCalls: true,
|
|
1034
|
+
// Baseten says models outside its reasoning table do not support reasoning. Keep
|
|
1035
|
+
// unknown/new live slugs conservative until an official-docs registry refresh proves it.
|
|
1036
|
+
reasoningEfforts: [],
|
|
1037
|
+
modelReasoningEfforts: BASETEN_MODEL_REASONING_EFFORTS,
|
|
1038
|
+
modelReasoningEffortMap: BASETEN_MODEL_REASONING_EFFORT_MAP,
|
|
1039
|
+
modelDefaultReasoningEfforts: BASETEN_MODEL_DEFAULT_REASONING_EFFORTS,
|
|
1040
|
+
modelInputModalities: BASETEN_MODEL_INPUT_MODALITIES,
|
|
1041
|
+
modelDiscovery: {
|
|
1042
|
+
path: "models",
|
|
1043
|
+
maxResponseBytes: 1_048_576,
|
|
1044
|
+
maxModels: 256,
|
|
1045
|
+
},
|
|
1046
|
+
note: "Shared Model APIs only (personal API key, or team key with Call Model APIs access); dedicated Truss predict endpoints are outside this preset.",
|
|
1047
|
+
},
|
|
986
1048
|
// FREEZE 2026-07-10: exact serverless ids remain auth-gated/unverified. Evidence: devlog/_plan/260710_provider_hardening/003_research_aggregators.md.
|
|
987
1049
|
{ id: "together", label: "Together", baseUrl: "https://api.together.xyz/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://api.together.xyz/settings/api-keys" },
|
|
988
1050
|
{ id: "fireworks", label: "Fireworks", baseUrl: "https://api.fireworks.ai/inference/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://fireworks.ai/account/api-keys" },
|
package/src/server/index.ts
CHANGED
|
@@ -474,7 +474,7 @@ export function startServer(port?: number) {
|
|
|
474
474
|
}
|
|
475
475
|
throw error;
|
|
476
476
|
}
|
|
477
|
-
const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
477
|
+
const { applyNativeVisibility, buildCatalogEntries, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, orderForSubagents, filterCatalogVisibleModels, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
478
478
|
const nativeSlugs = nativeOpenAiSlugs();
|
|
479
479
|
const goEnabled = filterCatalogVisibleModels(goModels, config);
|
|
480
480
|
const goOrdered = orderForSubagents(goEnabled, config.subagentModels);
|
|
@@ -524,9 +524,45 @@ export function startServer(port?: number) {
|
|
|
524
524
|
}
|
|
525
525
|
// OpenAI list shape: native gpt bare + routed models namespaced "<provider>/<id>"
|
|
526
526
|
// (pure availability list — disabled natives are omitted entirely).
|
|
527
|
+
// Grok Build discovers models through this endpoint too, and its model picker only
|
|
528
|
+
// enables /effort for entries that advertise the reasoning ladder in the Grok model
|
|
529
|
+
// catalog shape (supports_reasoning_effort + reasoning_efforts[]). The Codex catalog
|
|
530
|
+
// branch above already carries the same ladders, so mirror them here — native rows
|
|
531
|
+
// from the upstream snapshot, routed rows from the configured provider tiers. The
|
|
532
|
+
// default uses the same canonical fallback as the Codex catalog resolver
|
|
533
|
+
// (configured default, then medium, then high, then the first tier). Extra fields
|
|
534
|
+
// are ignored by plain OpenAI clients.
|
|
535
|
+
const grokEffortOption = (value: string, isDefault: boolean) => ({
|
|
536
|
+
value,
|
|
537
|
+
label: `${value[0].toUpperCase()}${value.slice(1)} Effort`,
|
|
538
|
+
...(isDefault ? { default: true } : {}),
|
|
539
|
+
});
|
|
540
|
+
const grokEffortFields = (efforts: string[], configuredDefault?: string) => {
|
|
541
|
+
if (efforts.length === 0) return {};
|
|
542
|
+
const defaultEffort = configuredDefault && efforts.includes(configuredDefault)
|
|
543
|
+
? configuredDefault
|
|
544
|
+
: efforts.includes("medium") ? "medium" : efforts.includes("high") ? "high" : efforts[0];
|
|
545
|
+
return {
|
|
546
|
+
supports_reasoning_effort: true,
|
|
547
|
+
reasoning_effort: defaultEffort,
|
|
548
|
+
reasoning_efforts: efforts.map(effort => grokEffortOption(effort, effort === defaultEffort)),
|
|
549
|
+
};
|
|
550
|
+
};
|
|
527
551
|
const data = [
|
|
528
|
-
...visibleNativeSlugs(config).map(id => ({
|
|
529
|
-
|
|
552
|
+
...visibleNativeSlugs(config).map(id => ({
|
|
553
|
+
id,
|
|
554
|
+
object: "model",
|
|
555
|
+
created: 0,
|
|
556
|
+
owned_by: "openai",
|
|
557
|
+
...grokEffortFields(nativeReasoningEfforts(id), nativeDefaultReasoningEffort(id)),
|
|
558
|
+
})),
|
|
559
|
+
...uniqueCatalogModelsForRawPublicList(goOrdered).map(m => ({
|
|
560
|
+
id: m.alias ?? `${m.provider}/${m.id}`,
|
|
561
|
+
object: "model",
|
|
562
|
+
created: 0,
|
|
563
|
+
owned_by: m.owned_by ?? m.provider,
|
|
564
|
+
...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
|
|
565
|
+
})),
|
|
530
566
|
];
|
|
531
567
|
return jsonResponse({ object: "list", data }, 200, req, config);
|
|
532
568
|
}
|
|
@@ -460,7 +460,11 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
460
460
|
...listCatalogNativeSlugs().filter(ns => !disabled.has(ns)),
|
|
461
461
|
...visibleRouted,
|
|
462
462
|
];
|
|
463
|
-
|
|
463
|
+
// #857: let CLI/GUI show when a running Codex app-server keeps an older
|
|
464
|
+
// in-memory catalog than the one on disk.
|
|
465
|
+
const { collectCodexAppServerCatalogState } = await import("../../codex/app-server-processes");
|
|
466
|
+
const catalogState = collectCodexAppServerCatalogState();
|
|
467
|
+
return jsonResponse({ chosen: config.subagentModels ?? [], available, catalogState });
|
|
464
468
|
}
|
|
465
469
|
if (url.pathname === "/api/subagent-models" && req.method === "PUT") {
|
|
466
470
|
let body: { models?: unknown };
|
|
@@ -646,7 +650,40 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
646
650
|
}
|
|
647
651
|
if (url.pathname === "/api/claude-desktop/apply" && req.method === "POST") {
|
|
648
652
|
try {
|
|
649
|
-
|
|
653
|
+
// #859: the CLI delegates here so the registry is built in the serving
|
|
654
|
+
// process. Accept an optional mode; default stays static for back-compat.
|
|
655
|
+
let mode: "static" | "hybrid" | "discovery" = "static";
|
|
656
|
+
const rawBody = await req.text();
|
|
657
|
+
let parsed: unknown;
|
|
658
|
+
if (rawBody.trim()) {
|
|
659
|
+
try {
|
|
660
|
+
parsed = JSON.parse(rawBody);
|
|
661
|
+
} catch {
|
|
662
|
+
return jsonResponse({ error: "invalid JSON body" }, 400);
|
|
663
|
+
}
|
|
664
|
+
const requested = (parsed as { mode?: unknown } | null)?.mode;
|
|
665
|
+
if (requested !== undefined) {
|
|
666
|
+
if (requested === "static" || requested === "hybrid" || requested === "discovery") {
|
|
667
|
+
mode = requested;
|
|
668
|
+
} else {
|
|
669
|
+
return jsonResponse({ error: "mode must be static, hybrid, or discovery" }, 400);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// #859: a delegated CLI apply carries the profile it just saved — the
|
|
674
|
+
// daemon's own config can be older, and building state from it would
|
|
675
|
+
// apply (and persist) the stale profile over the newer one.
|
|
676
|
+
const bodyProfile = (parsed as { profile?: unknown } | null)?.profile;
|
|
677
|
+
let profileOverride: Parameters<typeof buildClaudeDesktopState>[1];
|
|
678
|
+
if (bodyProfile !== undefined) {
|
|
679
|
+
const { parseDesktopProfile } = await import("../../claude/desktop-profile");
|
|
680
|
+
try {
|
|
681
|
+
profileOverride = parseDesktopProfile(bodyProfile);
|
|
682
|
+
} catch (error) {
|
|
683
|
+
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const state = await buildClaudeDesktopState(config, profileOverride);
|
|
650
687
|
config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: state.profile };
|
|
651
688
|
saveConfigPreservingClaudeCode(config);
|
|
652
689
|
const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
|
|
@@ -662,7 +699,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
662
699
|
[...desktopVisibleNativeSlugs(config)],
|
|
663
700
|
routed,
|
|
664
701
|
config.apiKeys?.[0]?.key,
|
|
665
|
-
|
|
702
|
+
mode,
|
|
666
703
|
state.profile,
|
|
667
704
|
);
|
|
668
705
|
if (!result.written) return jsonResponse({ error: result.reason ?? "Claude Desktop apply failed", saved: true, path: result.path }, 500);
|
|
@@ -321,6 +321,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
321
321
|
result.error === "codex_busy"
|
|
322
322
|
|| result.error === "stale_preview"
|
|
323
323
|
|| result.error === "referenced_history"
|
|
324
|
+
|| result.error === "pinned_thread"
|
|
324
325
|
|| result.error === "storage_mutation_busy"
|
|
325
326
|
|| result.error === "restore_pending_overlap"
|
|
326
327
|
? 409
|
|
@@ -333,6 +334,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
333
334
|
stale_preview: "Archived files changed since preview — run Preview again.",
|
|
334
335
|
restore_pending_overlap: "Selected archives overlap an incomplete trash restore — finish or retry restore first.",
|
|
335
336
|
referenced_history: "Selected archives are still referenced by forked or paginated history.",
|
|
337
|
+
pinned_thread: "Selected archives include a pinned thread — unpin it in Codex before cleanup.",
|
|
336
338
|
invalid_digest: "Preview digest is missing or invalid.",
|
|
337
339
|
invalid_mode: "mode must be quarantine or permanent.",
|
|
338
340
|
fs_failed: "Filesystem cleanup failed. Some changes may already be applied — check CODEX_HOME/.trash and any recovery path in the response.",
|
|
@@ -365,7 +365,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
365
365
|
});
|
|
366
366
|
}
|
|
367
367
|
if (prov.liveModels === false) {
|
|
368
|
-
|
|
368
|
+
// A static catalog has no live discovery endpoint to test. This is neither
|
|
369
|
+
// positive connectivity evidence nor an outage, and it must stay before
|
|
370
|
+
// credential resolution/network access for providers such as Antigravity.
|
|
371
|
+
return jsonResponse({ applicable: false, reason: "static_catalog", latencyMs: 0 });
|
|
369
372
|
}
|
|
370
373
|
const { resolveModelsAuthToken, buildModelsRequest } = await import("../../oauth");
|
|
371
374
|
const apiKey = await resolveModelsAuthToken(name, prov);
|
|
@@ -68,7 +68,9 @@ export async function handleSidebarRoutes(ctx: ManagementContext): Promise<Respo
|
|
|
68
68
|
message:
|
|
69
69
|
`Refused: agent session detected (${agentDrivenMarkers().slice(0, 3).join(", ")}) and this request `
|
|
70
70
|
+ `carries no dashboard session. Starring writes to the user's own GitHub account, so ask the user `
|
|
71
|
-
+ `directly and
|
|
71
|
+
+ `directly as a required Yes/No choice — not a soft aside, and no answer is not a No, so re-ask `
|
|
72
|
+
+ `it unchanged while it is open — and only if they say yes run: `
|
|
73
|
+
+ `gh api -X PUT /user/starred/${STAR_REPO}`,
|
|
72
74
|
}, 403);
|
|
73
75
|
}
|
|
74
76
|
const result = await starRepository();
|
|
@@ -25,10 +25,24 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { buildFailedTailPayload } from "./relay";
|
|
28
|
+
import {
|
|
29
|
+
nextSseBlock,
|
|
30
|
+
replaceSseDataPayload,
|
|
31
|
+
sseDataPayload,
|
|
32
|
+
type SsePayloadRewrite,
|
|
33
|
+
} from "./sse-payload-rewrite";
|
|
34
|
+
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
28
35
|
|
|
29
36
|
export type EagerRelayHooks = {
|
|
30
37
|
/** Feed one upstream chunk through SSE inspection (createSseInspector.feed). */
|
|
31
38
|
inspectChunk: (chunk: Uint8Array) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Optional inline client-facing payload rewrite, framed to complete SSE
|
|
41
|
+
* blocks inside the single reader. This is what lets win32 rewrite traffic
|
|
42
|
+
* (image_gen restore, item-id repair) use this relay instead of the
|
|
43
|
+
* Bun#32111-unsafe tee()+JS-pull chain (#864).
|
|
44
|
+
*/
|
|
45
|
+
rewritePayload?: SsePayloadRewrite;
|
|
32
46
|
/** Flush inspection at upstream end (createSseInspector.finish). */
|
|
33
47
|
finishInspection: () => void;
|
|
34
48
|
/** Drop inspector-owned frame/item state during producer teardown. */
|
|
@@ -46,6 +60,8 @@ export type EagerRelayHooks = {
|
|
|
46
60
|
export type EagerRelayOptions = {
|
|
47
61
|
/** Bounded client queue in bytes; producer pauses above it. Default 8 MiB. */
|
|
48
62
|
maxQueueBytes?: number;
|
|
63
|
+
/** Transient-budget owner for the inline-rewrite frame buffer. */
|
|
64
|
+
rewriteBudget?: TranslatorBudget;
|
|
49
65
|
/** Post-cancel discard-drain wall-clock bound. Default 15 000 ms. */
|
|
50
66
|
postCancelDrainMs?: number;
|
|
51
67
|
/** Post-cancel discard-drain byte bound. Default 32 MiB. */
|
|
@@ -76,6 +92,72 @@ export function relaySseEagerBounded(
|
|
|
76
92
|
const now = opts?.now ?? Date.now;
|
|
77
93
|
|
|
78
94
|
const reader = body.getReader();
|
|
95
|
+
const rewrite = hooks.rewritePayload;
|
|
96
|
+
const rewriteDecoder = rewrite ? new TextDecoder() : null;
|
|
97
|
+
const rewriteEncoder = rewrite ? new TextEncoder() : null;
|
|
98
|
+
const rewriteBudget = opts?.rewriteBudget;
|
|
99
|
+
let frameBuffer = "";
|
|
100
|
+
let frameBufferBytes = 0;
|
|
101
|
+
/** Frame complete SSE blocks and rewrite each block's data payload in place. */
|
|
102
|
+
const rewriteOutbound = (value: Uint8Array): Uint8Array => {
|
|
103
|
+
let out = "";
|
|
104
|
+
const fragment = rewriteDecoder!.decode(value, { stream: true });
|
|
105
|
+
if (rewriteBudget) {
|
|
106
|
+
const nextBytes = frameBufferBytes + rewriteEncoder!.encode(fragment).byteLength;
|
|
107
|
+
const reservation = rewriteBudget.reserveTransient(nextBytes, { kind: "live_transient" });
|
|
108
|
+
try {
|
|
109
|
+
frameBuffer += fragment;
|
|
110
|
+
reservation.commitRetained();
|
|
111
|
+
rewriteBudget.releaseRetained(frameBufferBytes, { kind: "live_transient" });
|
|
112
|
+
frameBufferBytes = nextBytes;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
reservation.release();
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
frameBuffer += fragment;
|
|
119
|
+
frameBufferBytes += value.byteLength;
|
|
120
|
+
}
|
|
121
|
+
for (;;) {
|
|
122
|
+
const next = nextSseBlock(frameBuffer);
|
|
123
|
+
if (!next) break;
|
|
124
|
+
const payload = sseDataPayload(next.block);
|
|
125
|
+
const rewrittenPayload = payload === null ? null : rewrite!(payload);
|
|
126
|
+
// Replace only on an actual change: replaceSseDataPayload collapses
|
|
127
|
+
// multi-data-line events and normalizes newline style even when the
|
|
128
|
+
// payload is identical, which corrupts valid streams.
|
|
129
|
+
const block = payload !== null && rewrittenPayload !== payload
|
|
130
|
+
? replaceSseDataPayload(next.block, rewrittenPayload!)
|
|
131
|
+
: next.block;
|
|
132
|
+
out += block + next.delimiter;
|
|
133
|
+
frameBuffer = next.rest;
|
|
134
|
+
}
|
|
135
|
+
if (rewriteBudget) {
|
|
136
|
+
const remaining = rewriteEncoder!.encode(frameBuffer).byteLength;
|
|
137
|
+
rewriteBudget.releaseRetained(frameBufferBytes - remaining, { kind: "live_transient" });
|
|
138
|
+
frameBufferBytes = remaining;
|
|
139
|
+
} else {
|
|
140
|
+
frameBufferBytes = rewriteEncoder!.encode(frameBuffer).byteLength;
|
|
141
|
+
}
|
|
142
|
+
return rewriteEncoder!.encode(out);
|
|
143
|
+
};
|
|
144
|
+
/** Flush any trailing partial block at upstream end (rewrite applied, matching the pull relay). */
|
|
145
|
+
const flushRewriteTail = (): Uint8Array => {
|
|
146
|
+
if (!rewrite) return new Uint8Array(0);
|
|
147
|
+
// Decoder-flushed bytes logically follow everything already decoded.
|
|
148
|
+
let tail = frameBuffer + rewriteDecoder!.decode();
|
|
149
|
+
const payload = sseDataPayload(tail);
|
|
150
|
+
if (payload !== null) {
|
|
151
|
+
const rewrittenPayload = rewrite(payload);
|
|
152
|
+
if (rewrittenPayload !== payload) tail = replaceSseDataPayload(tail, rewrittenPayload);
|
|
153
|
+
}
|
|
154
|
+
frameBuffer = "";
|
|
155
|
+
if (rewriteBudget && frameBufferBytes > 0) {
|
|
156
|
+
rewriteBudget.releaseRetained(frameBufferBytes, { kind: "live_transient" });
|
|
157
|
+
}
|
|
158
|
+
frameBufferBytes = 0;
|
|
159
|
+
return rewriteEncoder!.encode(tail);
|
|
160
|
+
};
|
|
79
161
|
let queuedBytes = 0;
|
|
80
162
|
let cancelled = false;
|
|
81
163
|
let done = false;
|
|
@@ -126,6 +208,13 @@ export function relaySseEagerBounded(
|
|
|
126
208
|
const { done: upstreamDone, value } = result;
|
|
127
209
|
if (upstreamDone) {
|
|
128
210
|
hooks.finishInspection();
|
|
211
|
+
if (rewrite) {
|
|
212
|
+
const tail = flushRewriteTail();
|
|
213
|
+
if (tail.byteLength > 0 && !cancelled) {
|
|
214
|
+
queuedBytes += tail.byteLength;
|
|
215
|
+
try { controllerRef?.enqueue(tail); } catch { /* client already gone */ }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
129
218
|
if (!hooks.sawTerminal() && !cancelled && !upstream.signal.aborted) {
|
|
130
219
|
syntheticKind = "incomplete";
|
|
131
220
|
}
|
|
@@ -141,9 +230,11 @@ export function relaySseEagerBounded(
|
|
|
141
230
|
}
|
|
142
231
|
continue;
|
|
143
232
|
}
|
|
144
|
-
|
|
233
|
+
const outbound = rewrite ? rewriteOutbound(value) : value;
|
|
234
|
+
if (outbound.byteLength === 0) continue;
|
|
235
|
+
queuedBytes += outbound.byteLength;
|
|
145
236
|
try {
|
|
146
|
-
controllerRef?.enqueue(
|
|
237
|
+
controllerRef?.enqueue(outbound);
|
|
147
238
|
} catch {
|
|
148
239
|
// Controller already torn down (client went away without cancel()).
|
|
149
240
|
cancelled = true;
|
|
@@ -174,6 +265,13 @@ export function relaySseEagerBounded(
|
|
|
174
265
|
}
|
|
175
266
|
}
|
|
176
267
|
} finally {
|
|
268
|
+
// Release any retained rewrite-buffer bytes on every teardown path
|
|
269
|
+
// (error, cancel, upstream abort) — consumption/EOF release alone
|
|
270
|
+
// leaves them charged.
|
|
271
|
+
if (rewriteBudget && frameBufferBytes > 0) {
|
|
272
|
+
try { rewriteBudget.releaseRetained(frameBufferBytes, { kind: "live_transient" }); } catch { /* teardown must not throw */ }
|
|
273
|
+
frameBufferBytes = 0;
|
|
274
|
+
}
|
|
177
275
|
if (syntheticKind) hooks.onSynthetic(syntheticKind);
|
|
178
276
|
if (cancelled && !hooks.sawTerminal()) {
|
|
179
277
|
hooks.onClientCancel();
|
|
@@ -183,6 +183,18 @@ export interface MultiAgentGuidanceDeps {
|
|
|
183
183
|
configuredModels: readonly string[],
|
|
184
184
|
surface: SpawnAgentSurface,
|
|
185
185
|
) => EffectiveSubagentRoster | Promise<EffectiveSubagentRoster>;
|
|
186
|
+
collectCatalogState?: () => { state: "fresh" | "stale" | "not_running" | "unknown" };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function defaultCollectCatalogState(): Promise<{ state: "fresh" | "stale" | "not_running" | "unknown" }> {
|
|
190
|
+
// Explicit override for tests and diagnostics: process state is global and
|
|
191
|
+
// would otherwise leak the host machine's app-server into hermetic tests.
|
|
192
|
+
const override = process.env.OPENCODEX_APP_SERVER_CATALOG_STATE_OVERRIDE;
|
|
193
|
+
if (override === "fresh" || override === "stale" || override === "not_running" || override === "unknown") {
|
|
194
|
+
return { state: override };
|
|
195
|
+
}
|
|
196
|
+
const { collectCodexAppServerCatalogState } = await import("../../codex/app-server-processes");
|
|
197
|
+
return collectCodexAppServerCatalogState();
|
|
186
198
|
}
|
|
187
199
|
|
|
188
200
|
|
|
@@ -214,6 +226,15 @@ export async function multiAgentGuidanceText(
|
|
|
214
226
|
if (surface === null) return null;
|
|
215
227
|
|
|
216
228
|
if (surface === "v2") {
|
|
229
|
+
// #857: the disk catalog may be newer than the running app-server's
|
|
230
|
+
// in-memory copy. Advertising preferred models or a roster the running
|
|
231
|
+
// Codex cannot actually spawn makes spawn_agent reject the override, so
|
|
232
|
+
// suppress positive model claims while the state is stale or unknown.
|
|
233
|
+
const catalogState = await (deps.collectCatalogState ?? defaultCollectCatalogState)();
|
|
234
|
+
if (catalogState.state === "stale" || catalogState.state === "unknown") {
|
|
235
|
+
return "<multi_agent_mode>The model catalog changed after Codex started; do not set "
|
|
236
|
+
+ "model or reasoning_effort overrides until Codex restarts.</multi_agent_mode>";
|
|
237
|
+
}
|
|
217
238
|
// codex-rs supplies the Proactive text on v2; the proxy only adds model-designation
|
|
218
239
|
// guidance, and only when there is something concrete to designate: a configured
|
|
219
240
|
// injectionModel and/or a roster entry that resolves in the injected catalog.
|
|
@@ -144,7 +144,7 @@ import {
|
|
|
144
144
|
sanitizePassthroughHeaders,
|
|
145
145
|
} from "../relay";
|
|
146
146
|
import { relaySseEagerBounded } from "../relay-eager";
|
|
147
|
-
import { selectEagerPath } from "../../lib/bun-stream-caps";
|
|
147
|
+
import { isWin32EagerRewrite, selectEagerPath } from "../../lib/bun-stream-caps";
|
|
148
148
|
import { cancelBodyOnAbort } from "../../lib/abort";
|
|
149
149
|
import {
|
|
150
150
|
createResponsesItemIdPayloadRewrite,
|
|
@@ -1759,12 +1759,23 @@ async function handleResponsesInner(
|
|
|
1759
1759
|
if (isEventStream && upstreamResponse.body) {
|
|
1760
1760
|
const repairConfig = route.provider.responsesItemIdRepair;
|
|
1761
1761
|
const needsClientRewrite = imageGenCallAliases.size > 0 || hasResponsesItemIdRepair(repairConfig);
|
|
1762
|
+
// Compose opt-in payload rewrites into one parse/stringify pass (image-gen restore first).
|
|
1763
|
+
const payloadRewrites = [
|
|
1764
|
+
createImageGenCallRestoreRewrite(imageGenCallAliases),
|
|
1765
|
+
hasResponsesItemIdRepair(repairConfig)
|
|
1766
|
+
? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget)
|
|
1767
|
+
: undefined,
|
|
1768
|
+
].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
|
|
1769
|
+
// #864: win32 rewrite traffic must never enter the tee()+JS-pull chain
|
|
1770
|
+
// (Bun#32111 JS-sink segfault — text frames pass, the terminal block is
|
|
1771
|
+
// lost). The eager single reader applies the same rewrites inline.
|
|
1772
|
+
const win32EagerRewrite = isWin32EagerRewrite(process.platform, needsClientRewrite);
|
|
1762
1773
|
const eagerPath = selectEagerPath(
|
|
1763
1774
|
process.platform,
|
|
1764
1775
|
needsClientRewrite,
|
|
1765
1776
|
config.streamMode ?? "auto",
|
|
1766
1777
|
);
|
|
1767
|
-
if (eagerPath?.useEagerRelay) {
|
|
1778
|
+
if (eagerPath?.useEagerRelay || win32EagerRewrite) {
|
|
1768
1779
|
const turnAc = new AbortController();
|
|
1769
1780
|
linkAbortSignal(upstream, turnAc.signal);
|
|
1770
1781
|
registerTurn(turnAc, options.turnAdmissionLease);
|
|
@@ -1801,6 +1812,9 @@ async function handleResponsesInner(
|
|
|
1801
1812
|
finishInspection: () => inspector.finish(),
|
|
1802
1813
|
disposeInspection: () => inspector.dispose(),
|
|
1803
1814
|
sawTerminal: () => inspector.reported(),
|
|
1815
|
+
...(win32EagerRewrite
|
|
1816
|
+
? { rewritePayload: composeSsePayloadRewrites(...payloadRewrites) }
|
|
1817
|
+
: {}),
|
|
1804
1818
|
onSynthetic: kind => {
|
|
1805
1819
|
if (!reportNativeTerminal) return;
|
|
1806
1820
|
if (kind === "incomplete") {
|
|
@@ -1814,9 +1828,10 @@ async function handleResponsesInner(
|
|
|
1814
1828
|
},
|
|
1815
1829
|
onClientCancel: () => options.onNativePassthroughCancel?.(),
|
|
1816
1830
|
onDone: () => unregisterTurn(turnAc),
|
|
1817
|
-
});
|
|
1818
|
-
// selectEagerPath admits only no-rewrite traffic
|
|
1819
|
-
//
|
|
1831
|
+
}, win32EagerRewrite ? { rewriteBudget: translatorBudget } : undefined);
|
|
1832
|
+
// selectEagerPath admits only no-rewrite traffic on both eligible platforms;
|
|
1833
|
+
// win32 rewrite traffic reaches this relay too, but with the payload rewrite
|
|
1834
|
+
// applied inline — never via an image/item-id JS pull wrapper (#32111, #864).
|
|
1820
1835
|
if (!headers.has("content-type")) headers.set("content-type", "text/event-stream");
|
|
1821
1836
|
return markEagerRelaySseResponse(
|
|
1822
1837
|
markNativePassthroughSseResponse(new Response(eagerBody, {
|
|
@@ -1886,13 +1901,6 @@ async function handleResponsesInner(
|
|
|
1886
1901
|
// win32 must keep the pure native relay (Bun#32111 JS-sink segfault); elsewhere a JS pull
|
|
1887
1902
|
// relay is established practice (relayWithAbort, relaySseWithHeartbeat) and lets a
|
|
1888
1903
|
// mid-stream reset end with a clean response.failed terminal instead of a raw socket error.
|
|
1889
|
-
// Compose opt-in payload rewrites into one parse/stringify pass (image-gen restore first).
|
|
1890
|
-
const payloadRewrites = [
|
|
1891
|
-
createImageGenCallRestoreRewrite(imageGenCallAliases),
|
|
1892
|
-
hasResponsesItemIdRepair(repairConfig)
|
|
1893
|
-
? createResponsesItemIdPayloadRewrite(repairConfig!, translatorBudget)
|
|
1894
|
-
: undefined,
|
|
1895
|
-
].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
|
|
1896
1904
|
const rewrittenBody = payloadRewrites.length > 0
|
|
1897
1905
|
? relaySseWithPayloadRewrite(nativeBody, composeSsePayloadRewrites(...payloadRewrites), translatorBudget)
|
|
1898
1906
|
: nativeBody;
|