@bitkyc08/opencodex 2.7.23 → 2.7.24
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.ko.md +37 -7
- package/README.md +52 -11
- package/README.zh-CN.md +36 -7
- package/bin/ocx.mjs +5 -3
- package/gui/dist/assets/index-BzhyTAco.js +40 -0
- package/gui/dist/assets/index-Dq3eZ1cU.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/opencode.svg +1 -1
- package/package.json +5 -2
- package/src/adapters/anthropic-image-normalize.ts +70 -29
- package/src/adapters/cursor/transport-retry.ts +20 -1
- package/src/adapters/run-turn-queue.ts +40 -0
- package/src/codex/auth-api.ts +10 -1
- package/src/codex/auth-context.ts +33 -7
- package/src/codex/catalog.ts +357 -23
- package/src/codex/routing.ts +10 -4
- package/src/combos/failover.ts +102 -0
- package/src/combos/index.ts +37 -0
- package/src/combos/request.ts +31 -0
- package/src/combos/resolve.ts +171 -0
- package/src/combos/types.ts +203 -0
- package/src/config.ts +280 -11
- package/src/lib/errors.ts +86 -24
- package/src/lib/upstream-retry.ts +8 -4
- package/src/oauth/index.ts +7 -1
- package/src/oauth/key-providers.ts +2 -32
- package/src/oauth/login-cli.ts +4 -3
- package/src/oauth/token-guardian.ts +38 -3
- package/src/providers/derive.ts +27 -2
- package/src/providers/kiro-models.ts +8 -3
- package/src/providers/label.ts +3 -1
- package/src/providers/openai-sidecar.ts +94 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +283 -0
- package/src/providers/openai-virtual-models.ts +82 -0
- package/src/providers/quota.ts +344 -24
- package/src/providers/registry.ts +112 -20
- package/src/reasoning-effort.ts +12 -11
- package/src/router.ts +80 -36
- package/src/server/auth-cors.ts +85 -9
- package/src/server/images.ts +31 -75
- package/src/server/index.ts +45 -86
- package/src/server/management-api.ts +273 -21
- package/src/server/request-log.ts +221 -20
- package/src/server/responses.ts +594 -75
- package/src/server/search.ts +22 -37
- package/src/types.ts +49 -1
- package/src/update/index.ts +50 -6
- package/src/update/job.ts +21 -4
- package/src/usage/log.ts +124 -1
- package/src/usage/summary.ts +147 -56
- package/src/vision/index.ts +20 -19
- package/src/web-search/index.ts +15 -17
- package/gui/dist/assets/index-Bk_GgFrh.css +0 -1
- package/gui/dist/assets/index-DQjt6Hly.js +0 -40
package/src/codex/catalog.ts
CHANGED
|
@@ -4,17 +4,28 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from
|
|
|
4
4
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { atomicWriteFile, expandUserPath, getConfigDir, websocketsEnabled } from "../config";
|
|
6
6
|
import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "./paths";
|
|
7
|
-
import { DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCached, isModelsFetchCoolingDown, markModelsFetchFailure, setCached } from "./model-cache";
|
|
7
|
+
import { clearModelCache, DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCached, isModelsFetchCoolingDown, markModelsFetchFailure, setCached } from "./model-cache";
|
|
8
8
|
import { buildModelsRequest, resolveModelsAuthToken } from "../oauth";
|
|
9
9
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
10
10
|
import { modelInList } from "../types";
|
|
11
|
-
import { CODEX_REASONING_LEVELS, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../reasoning-effort";
|
|
11
|
+
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../reasoning-effort";
|
|
12
12
|
import { getJawcodeModelMetadata, getJawcodeModelMetadataCaseInsensitive, listJawcodeModelMetadata, resolveJawcodeProvider } from "../generated/jawcode-model-metadata";
|
|
13
13
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../providers/derive";
|
|
14
|
+
import { getProviderRegistryEntry } from "../providers/registry";
|
|
14
15
|
import { applyProviderContextCap, providerContextCap } from "../providers/context-cap";
|
|
15
16
|
import { CODEX_GPT5_IDENTITY_LINE } from "../adapters/identity";
|
|
16
17
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../adapters/cursor/discovery";
|
|
17
18
|
import { fetchCursorUsableModels } from "../adapters/cursor/live-models";
|
|
19
|
+
import { OPENAI_API_PROVIDER_ID } from "../providers/openai-tiers";
|
|
20
|
+
import {
|
|
21
|
+
COMBO_NAMESPACE,
|
|
22
|
+
comboModelId,
|
|
23
|
+
getCombo,
|
|
24
|
+
listComboIds,
|
|
25
|
+
targetKey,
|
|
26
|
+
} from "../combos";
|
|
27
|
+
import type { NormalizedComboConfig } from "../combos/types";
|
|
28
|
+
import { redactSecretString } from "../lib/redact";
|
|
18
29
|
import upstreamModelsSnapshot from "./data/upstream-models.json";
|
|
19
30
|
|
|
20
31
|
const BUNDLED_CATALOG_CACHE_MS = 60_000;
|
|
@@ -292,13 +303,16 @@ export interface CatalogModel {
|
|
|
292
303
|
provider: string;
|
|
293
304
|
owned_by?: string;
|
|
294
305
|
reasoningEfforts?: string[];
|
|
306
|
+
defaultReasoningEffort?: string;
|
|
295
307
|
contextWindow?: number;
|
|
308
|
+
maxInputTokens?: number;
|
|
296
309
|
contextCap?: number;
|
|
297
310
|
contextCapped?: boolean;
|
|
298
311
|
inputModalities?: string[];
|
|
299
312
|
/** Provider opted into parallel tool calls (OcxProviderConfig.parallelToolCalls). */
|
|
300
313
|
parallelToolCalls?: boolean;
|
|
301
314
|
}
|
|
315
|
+
|
|
302
316
|
type RawEntry = Record<string, unknown>;
|
|
303
317
|
type RawCatalog = { models?: RawEntry[]; [k: string]: unknown };
|
|
304
318
|
const JAWCODE_CATALOG_AUGMENT_PROVIDERS = new Set(["opencode-go"]);
|
|
@@ -423,7 +437,10 @@ function applyNativeOpenAiContextOverride(entry: RawEntry): void {
|
|
|
423
437
|
}
|
|
424
438
|
}
|
|
425
439
|
|
|
426
|
-
function ensureStrictCatalogFields(
|
|
440
|
+
function ensureStrictCatalogFields(
|
|
441
|
+
entry: RawEntry,
|
|
442
|
+
options: { preserveExactInputModalities?: boolean } = {},
|
|
443
|
+
): RawEntry {
|
|
427
444
|
if (typeof entry.supports_reasoning_summaries !== "boolean") entry.supports_reasoning_summaries = true;
|
|
428
445
|
if (typeof entry.default_reasoning_summary !== "string") entry.default_reasoning_summary = "none";
|
|
429
446
|
if (typeof entry.support_verbosity !== "boolean") entry.support_verbosity = true;
|
|
@@ -435,7 +452,9 @@ function ensureStrictCatalogFields(entry: RawEntry): RawEntry {
|
|
|
435
452
|
if (typeof entry.supports_parallel_tool_calls !== "boolean") entry.supports_parallel_tool_calls = true;
|
|
436
453
|
if (typeof entry.supports_image_detail_original !== "boolean") entry.supports_image_detail_original = false;
|
|
437
454
|
if (!Array.isArray(entry.experimental_supported_tools)) entry.experimental_supported_tools = [];
|
|
438
|
-
if (!Array.isArray(entry.input_modalities)
|
|
455
|
+
if (!Array.isArray(entry.input_modalities) && !options.preserveExactInputModalities) {
|
|
456
|
+
entry.input_modalities = ["text"];
|
|
457
|
+
}
|
|
439
458
|
const contextWindow = typeof entry.context_window === "number" && entry.context_window > 0 ? entry.context_window : 128000;
|
|
440
459
|
entry.context_window = contextWindow;
|
|
441
460
|
if (
|
|
@@ -713,14 +732,22 @@ function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel): void
|
|
|
713
732
|
if (typeof model.contextWindow === "number" && model.contextWindow > 0) {
|
|
714
733
|
entry.context_window = model.contextWindow;
|
|
715
734
|
entry.max_context_window = model.contextWindow;
|
|
716
|
-
entry.auto_compact_token_limit = Math.
|
|
735
|
+
entry.auto_compact_token_limit = Math.min(
|
|
736
|
+
Math.floor(model.contextWindow * 0.9),
|
|
737
|
+
model.maxInputTokens ?? Number.POSITIVE_INFINITY,
|
|
738
|
+
);
|
|
717
739
|
}
|
|
718
740
|
if (Array.isArray(model.inputModalities) && model.inputModalities.length > 0) {
|
|
719
741
|
entry.input_modalities = model.inputModalities;
|
|
720
742
|
}
|
|
721
743
|
}
|
|
722
744
|
|
|
723
|
-
function applyReasoningLevels(
|
|
745
|
+
function applyReasoningLevels(
|
|
746
|
+
entry: RawEntry,
|
|
747
|
+
effortsOverride?: string[],
|
|
748
|
+
defaultOverride?: string,
|
|
749
|
+
preserveExact = false,
|
|
750
|
+
): void {
|
|
724
751
|
let efforts = sanitizeCodexReasoningEfforts(effortsOverride) ?? ROUTED_REASONING_LEVELS.map(l => l.effort);
|
|
725
752
|
// Mock top tiers (user decision 260709): every reasoning-capable model advertises `max`
|
|
726
753
|
// even when the provider ladder stops lower — subagent spawns pass `max` DIRECTLY
|
|
@@ -728,7 +755,7 @@ function applyReasoningLevels(entry: RawEntry, effortsOverride?: string[]): void
|
|
|
728
755
|
// so a missing max rung hard-fails spawn_agent effort overrides. The wire stays honest:
|
|
729
756
|
// routed adapters clamp via clampToSupportedCodexEffort and natives via
|
|
730
757
|
// nativeEffortClamp (max -> the model's real top rung).
|
|
731
|
-
if (efforts.length > 0) {
|
|
758
|
+
if (!preserveExact && efforts.length > 0) {
|
|
732
759
|
const additions: string[] = [];
|
|
733
760
|
if (!efforts.includes("max")) additions.push("max");
|
|
734
761
|
if (!efforts.includes("ultra")) additions.push("ultra");
|
|
@@ -749,7 +776,9 @@ function applyReasoningLevels(entry: RawEntry, effortsOverride?: string[]): void
|
|
|
749
776
|
delete entry.default_reasoning_level;
|
|
750
777
|
return;
|
|
751
778
|
}
|
|
752
|
-
entry.default_reasoning_level =
|
|
779
|
+
entry.default_reasoning_level = defaultOverride && efforts.includes(defaultOverride)
|
|
780
|
+
? defaultOverride
|
|
781
|
+
: efforts.includes("medium") ? "medium" : efforts.includes("high") ? "high" : efforts[0];
|
|
753
782
|
}
|
|
754
783
|
|
|
755
784
|
function isGpt56NativeSlug(slug: string): boolean {
|
|
@@ -814,7 +843,22 @@ function finishUpstreamNativeEntry(clone: RawEntry, priority: number): RawEntry
|
|
|
814
843
|
return ensureStrictCatalogFields(normalizeServiceTiers(clone));
|
|
815
844
|
}
|
|
816
845
|
|
|
817
|
-
function
|
|
846
|
+
function isExactComboCatalogModel(
|
|
847
|
+
model: CatalogModel | undefined,
|
|
848
|
+
exactComboSlugs: ReadonlySet<string>,
|
|
849
|
+
): boolean {
|
|
850
|
+
return model !== undefined && exactComboSlugs.has(`${model.provider}/${model.id}`);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function deriveEntry(
|
|
854
|
+
template: RawEntry | null,
|
|
855
|
+
slug: string,
|
|
856
|
+
desc: string,
|
|
857
|
+
priority: number,
|
|
858
|
+
model?: CatalogModel,
|
|
859
|
+
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
860
|
+
): RawEntry {
|
|
861
|
+
const preserveExact = isExactComboCatalogModel(model, exactComboSlugs);
|
|
818
862
|
if (!slug.includes("/")) {
|
|
819
863
|
// Supported native slug covered by the upstream snapshot: use the REAL entry (exact
|
|
820
864
|
// reasoning ladder — e.g. luna has no ultra — default effort, identity, model_messages)
|
|
@@ -843,7 +887,7 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
|
|
|
843
887
|
`You are a coding agent powered by the ${modelName} model. Do not claim to be GPT-5 or made by OpenAI.`,
|
|
844
888
|
);
|
|
845
889
|
}
|
|
846
|
-
applyReasoningLevels(e, model?.reasoningEfforts);
|
|
890
|
+
applyReasoningLevels(e, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact);
|
|
847
891
|
normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true);
|
|
848
892
|
applyJawcodeCatalogMetadata(e, slug, model?.contextCap);
|
|
849
893
|
applyCatalogModelMetadata(e, model);
|
|
@@ -863,7 +907,9 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
|
|
|
863
907
|
delete e.supports_websockets;
|
|
864
908
|
}
|
|
865
909
|
}
|
|
866
|
-
return ensureStrictCatalogFields(normalizeServiceTiers(e)
|
|
910
|
+
return ensureStrictCatalogFields(normalizeServiceTiers(e), {
|
|
911
|
+
preserveExactInputModalities: preserveExact,
|
|
912
|
+
});
|
|
867
913
|
}
|
|
868
914
|
// Fallback when no template is available (best-effort; strict parser may need more).
|
|
869
915
|
const entry: RawEntry = {
|
|
@@ -872,7 +918,9 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
|
|
|
872
918
|
priority, base_instructions: "You are a helpful coding assistant.",
|
|
873
919
|
...(slug.includes("/") ? { web_search_tool_type: "text_and_image", supports_search_tool: true } : {}),
|
|
874
920
|
};
|
|
875
|
-
if (slug.includes("/"))
|
|
921
|
+
if (slug.includes("/")) {
|
|
922
|
+
applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact);
|
|
923
|
+
}
|
|
876
924
|
else {
|
|
877
925
|
applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
|
|
878
926
|
if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(entry);
|
|
@@ -880,7 +928,9 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
|
|
|
880
928
|
applyJawcodeCatalogMetadata(entry, slug, model?.contextCap);
|
|
881
929
|
applyCatalogModelMetadata(entry, model);
|
|
882
930
|
applyNativeOpenAiContextOverride(entry);
|
|
883
|
-
return ensureStrictCatalogFields(normalizeServiceTiers(entry)
|
|
931
|
+
return ensureStrictCatalogFields(normalizeServiceTiers(entry), {
|
|
932
|
+
preserveExactInputModalities: preserveExact,
|
|
933
|
+
});
|
|
884
934
|
}
|
|
885
935
|
|
|
886
936
|
/**
|
|
@@ -888,7 +938,15 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
|
|
|
888
938
|
* catalog sync and the proxy `/v1/models?client_version` branch.
|
|
889
939
|
* Native gpt slugs stay bare; routed models are namespaced `<provider>/<model>`.
|
|
890
940
|
*/
|
|
891
|
-
export function buildCatalogEntries(
|
|
941
|
+
export function buildCatalogEntries(
|
|
942
|
+
template: RawEntry | null,
|
|
943
|
+
gptSlugs: string[],
|
|
944
|
+
goModels: CatalogModel[],
|
|
945
|
+
featured?: string[],
|
|
946
|
+
wsEnabled = false,
|
|
947
|
+
multiAgentMode: MultiAgentMode = "default",
|
|
948
|
+
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
949
|
+
): RawEntry[] {
|
|
892
950
|
// Codex's models-manager sorts by `priority` ASC and advertises the first 5 picker-visible
|
|
893
951
|
// models to spawn_agent (sort_by_key(priority) + MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT=5). Catalog
|
|
894
952
|
// ARRAY order is discarded — so "featuring" a model = giving it the LOWEST priority (0..N-1) so
|
|
@@ -902,7 +960,14 @@ export function buildCatalogEntries(template: RawEntry | null, gptSlugs: string[
|
|
|
902
960
|
}
|
|
903
961
|
for (const m of goModels) {
|
|
904
962
|
const slug = `${m.provider}/${m.id}`;
|
|
905
|
-
const e = deriveEntry(
|
|
963
|
+
const e = deriveEntry(
|
|
964
|
+
template,
|
|
965
|
+
slug,
|
|
966
|
+
`Routed via opencodex → ${m.provider} (${m.owned_by ?? m.provider}).`,
|
|
967
|
+
5,
|
|
968
|
+
m,
|
|
969
|
+
exactComboSlugs,
|
|
970
|
+
);
|
|
906
971
|
if (rank.has(slug)) e.priority = rank.get(slug)!;
|
|
907
972
|
out.push(e);
|
|
908
973
|
}
|
|
@@ -1018,9 +1083,15 @@ function configuredInputModalities(prov: OcxProviderConfig, id: string): string[
|
|
|
1018
1083
|
return Array.isArray(modalities) && modalities.length > 0 ? [...modalities] : undefined;
|
|
1019
1084
|
}
|
|
1020
1085
|
|
|
1086
|
+
function configuredMaxInputTokens(prov: OcxProviderConfig, id: string): number | undefined {
|
|
1087
|
+
const configured = modelRecordValue(prov.modelMaxInputTokens, id);
|
|
1088
|
+
return typeof configured === "number" && configured > 0 ? configured : undefined;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1021
1091
|
export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, model: CatalogModel, providerCap?: number): CatalogModel {
|
|
1022
1092
|
void name;
|
|
1023
1093
|
const configuredCap = configuredContextWindow(prov, model.id);
|
|
1094
|
+
const configuredMaxInput = configuredMaxInputTokens(prov, model.id);
|
|
1024
1095
|
let inputModalities = configuredInputModalities(prov, model.id);
|
|
1025
1096
|
// Vision-sidecar coverage: `noVisionModels` marks models whose images the PROXY describes
|
|
1026
1097
|
// (src/vision/index.ts). The catalog must still advertise image input for them — the Codex app
|
|
@@ -1031,6 +1102,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
|
|
|
1031
1102
|
inputModalities = base.includes("image") ? [...base] : [...base, "image"];
|
|
1032
1103
|
}
|
|
1033
1104
|
const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
|
|
1105
|
+
const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort;
|
|
1034
1106
|
const hinted = {
|
|
1035
1107
|
...model,
|
|
1036
1108
|
...(configuredCap !== undefined
|
|
@@ -1042,6 +1114,14 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
|
|
|
1042
1114
|
: {}),
|
|
1043
1115
|
...(inputModalities ? { inputModalities } : {}),
|
|
1044
1116
|
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
|
|
1117
|
+
...(configuredMaxInput !== undefined
|
|
1118
|
+
? {
|
|
1119
|
+
maxInputTokens: typeof model.maxInputTokens === "number" && model.maxInputTokens > 0
|
|
1120
|
+
? Math.min(model.maxInputTokens, configuredMaxInput)
|
|
1121
|
+
: configuredMaxInput,
|
|
1122
|
+
}
|
|
1123
|
+
: {}),
|
|
1124
|
+
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
1045
1125
|
// Default-on for openai-chat providers (explicit false opts out); other adapters
|
|
1046
1126
|
// advertise only on explicit opt-in.
|
|
1047
1127
|
...(prov.parallelToolCalls === true || (prov.adapter === "openai-chat" && prov.parallelToolCalls !== false)
|
|
@@ -1077,6 +1157,29 @@ export function isDatedVariantId(liveId: string, configuredId: string): boolean
|
|
|
1077
1157
|
// Same-signature dedupe: Codex polls /v1/models frequently, and an unchanged drop list
|
|
1078
1158
|
// repeated on every poll is pure noise. Warn once per provider until the id set changes.
|
|
1079
1159
|
const lastDropWarnSignature = new Map<string, string>();
|
|
1160
|
+
// These managed providers intentionally carry compatibility fallback ids while treating their
|
|
1161
|
+
// authenticated live catalogs as canonical. A non-empty live response already hides stale ids;
|
|
1162
|
+
// repeating that expected reconciliation on every startup only adds noise.
|
|
1163
|
+
const QUIET_AUTHORITATIVE_CATALOG_PROVIDERS = new Set(["kimi", "xai"]);
|
|
1164
|
+
// Direct OAuth chat-completions probes on 260718 confirmed these account-scoped ids still work
|
|
1165
|
+
// even though the providers omit them from `/models`. Preserve only the proven compatibility
|
|
1166
|
+
// ids; unknown configured ids and xAI's chat-incompatible multi-agent model remain hidden.
|
|
1167
|
+
const CALLABLE_CONFIGURED_COMPATIBILITY_MODELS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
1168
|
+
kimi: new Set([
|
|
1169
|
+
"k3[1m]",
|
|
1170
|
+
"kimi-k2.7-code",
|
|
1171
|
+
"kimi-k2.7-code-highspeed",
|
|
1172
|
+
"kimi-k2.6",
|
|
1173
|
+
"kimi-k2.5",
|
|
1174
|
+
]),
|
|
1175
|
+
xai: new Set([
|
|
1176
|
+
"grok-4.3",
|
|
1177
|
+
"grok-4.20-0309-reasoning",
|
|
1178
|
+
"grok-4.20-0309-non-reasoning",
|
|
1179
|
+
"grok-build-0.1",
|
|
1180
|
+
"grok-composer-2.5-fast",
|
|
1181
|
+
]),
|
|
1182
|
+
};
|
|
1080
1183
|
function warnDroppedConfiguredIdsOnce(name: string, droppedConfiguredIds: string[]): void {
|
|
1081
1184
|
const signature = [...droppedConfiguredIds].sort().join(",");
|
|
1082
1185
|
if (lastDropWarnSignature.get(name) === signature) return;
|
|
@@ -1211,11 +1314,13 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
|
|
|
1211
1314
|
droppedConfiguredIds.push(m.id);
|
|
1212
1315
|
}
|
|
1213
1316
|
}
|
|
1214
|
-
if (live.length === 0) {
|
|
1317
|
+
if (live.length === 0 && name !== OPENAI_API_PROVIDER_ID) {
|
|
1215
1318
|
console.warn(
|
|
1216
1319
|
`[opencodex] Provider model discovery for "${name}" returned an authoritative empty catalog; ${droppedConfiguredIds.length > 0 ? `dropping configured model ids: ${droppedConfiguredIds.join(", ")}` : "no models will be exposed"}.`,
|
|
1217
1320
|
);
|
|
1218
|
-
} else if (droppedConfiguredIds.length > 0
|
|
1321
|
+
} else if (droppedConfiguredIds.length > 0
|
|
1322
|
+
&& name !== OPENAI_API_PROVIDER_ID
|
|
1323
|
+
&& !QUIET_AUTHORITATIVE_CATALOG_PROVIDERS.has(name)) {
|
|
1219
1324
|
warnDroppedConfiguredIdsOnce(name, droppedConfiguredIds);
|
|
1220
1325
|
}
|
|
1221
1326
|
setCached(name, live);
|
|
@@ -1233,6 +1338,7 @@ function shouldExposeProviderModel(providerName: string, modelId: string): boole
|
|
|
1233
1338
|
}
|
|
1234
1339
|
|
|
1235
1340
|
function shouldRetainConfiguredProviderModel(providerName: string, modelId: string): boolean {
|
|
1341
|
+
if (CALLABLE_CONFIGURED_COMPATIBILITY_MODELS[providerName]?.has(modelId)) return true;
|
|
1236
1342
|
if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
|
|
1237
1343
|
return false;
|
|
1238
1344
|
}
|
|
@@ -1285,15 +1391,223 @@ export async function gatherRoutedModels(config: OcxConfig): Promise<CatalogMode
|
|
|
1285
1391
|
const lists = await Promise.all(
|
|
1286
1392
|
activeProviders.map(([name, prov]) => fetchProviderModels(name, prov, ttlMs, providerContextCap(config, name))),
|
|
1287
1393
|
);
|
|
1288
|
-
const
|
|
1394
|
+
const apiAugmented = augmentRoutedModelsWithRegistryOpenAiApiRows(lists.flat(), config);
|
|
1395
|
+
const all = augmentRoutedModelsWithJawcodeMetadata(apiAugmented, activeProviders.map(([name]) => name), config.providers, config)
|
|
1289
1396
|
// Drop image/video generation models (e.g. Grok image/video) by default. Cursor's static catalog
|
|
1290
1397
|
// intentionally mirrors Cursor's public model table, including Gemini image preview, so the
|
|
1291
1398
|
// exposure decision goes through shouldExposeRoutedModel (single choke point).
|
|
1292
1399
|
.filter(shouldExposeRoutedModel);
|
|
1400
|
+
const memberByKey = new Map(all.map(model => [`${model.provider}/${model.id}`, model]));
|
|
1401
|
+
for (const id of listComboIds(config)) {
|
|
1402
|
+
const combo = getCombo(config, id);
|
|
1403
|
+
if (!combo) continue;
|
|
1404
|
+
const members = combo.targets
|
|
1405
|
+
.map(target => memberByKey.get(targetKey(target)))
|
|
1406
|
+
.filter((member): member is CatalogModel => member !== undefined);
|
|
1407
|
+
const derived = deriveComboCatalogModel(id, combo, members);
|
|
1408
|
+
if (derived) all.push(derived);
|
|
1409
|
+
else warnUncataloguedComboOnce(id, combo, members);
|
|
1410
|
+
}
|
|
1293
1411
|
all.sort((a, b) => (a.provider === b.provider ? a.id.localeCompare(b.id) : a.provider.localeCompare(b.provider)));
|
|
1294
1412
|
return all;
|
|
1295
1413
|
}
|
|
1296
1414
|
|
|
1415
|
+
const openAiApiCollisionWarnings = new Set<string>();
|
|
1416
|
+
const comboCatalogWarningSignatures = new Map<string, string>();
|
|
1417
|
+
|
|
1418
|
+
function intersectStrings(values: readonly string[][]): string[] {
|
|
1419
|
+
if (values.length === 0) return [];
|
|
1420
|
+
const rest = values.slice(1).map(value => new Set(value));
|
|
1421
|
+
return [...new Set(values[0])].filter(value => rest.every(set => set.has(value)));
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function effectiveComboDefault(
|
|
1425
|
+
configured: string | undefined,
|
|
1426
|
+
common: readonly string[],
|
|
1427
|
+
): string | undefined {
|
|
1428
|
+
if (configured && common.includes(configured)) return configured;
|
|
1429
|
+
const requestedRank = codexEffortRank(configured ?? "medium");
|
|
1430
|
+
const ranked = common
|
|
1431
|
+
.map(effort => ({ effort, rank: codexEffortRank(effort) }))
|
|
1432
|
+
.filter(item => item.rank >= 0)
|
|
1433
|
+
.sort((a, b) => a.rank - b.rank);
|
|
1434
|
+
if (ranked.length === 0) return undefined;
|
|
1435
|
+
const atOrBelow = ranked.filter(item => item.rank <= requestedRank);
|
|
1436
|
+
return atOrBelow.at(-1)?.effort ?? ranked[0]!.effort;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
export function deriveComboCatalogModel(
|
|
1440
|
+
id: string,
|
|
1441
|
+
combo: NormalizedComboConfig,
|
|
1442
|
+
members: readonly CatalogModel[],
|
|
1443
|
+
): CatalogModel | null {
|
|
1444
|
+
if (combo.targets.length === 0) return null;
|
|
1445
|
+
if (new Set(combo.targets.map(targetKey)).size !== combo.targets.length) return null;
|
|
1446
|
+
if (members.length !== combo.targets.length) return null;
|
|
1447
|
+
if (!members.every((member, index) => (
|
|
1448
|
+
`${member.provider}/${member.id}` === targetKey(combo.targets[index]!)
|
|
1449
|
+
))) return null;
|
|
1450
|
+
const contexts = members.map(member => member.contextWindow);
|
|
1451
|
+
if (contexts.some(value => typeof value !== "number" || value <= 0)) return null;
|
|
1452
|
+
|
|
1453
|
+
const inputModalities = intersectStrings(
|
|
1454
|
+
members.map(member => member.inputModalities ?? ["text"]),
|
|
1455
|
+
);
|
|
1456
|
+
if (inputModalities.length === 0) return null;
|
|
1457
|
+
const reasoningEfforts = intersectStrings(
|
|
1458
|
+
members.map(member => member.reasoningEfforts ?? []),
|
|
1459
|
+
);
|
|
1460
|
+
const contextWindow = Math.min(...contexts as number[]);
|
|
1461
|
+
const maxInputTokens = Math.min(
|
|
1462
|
+
...members.map(member => member.maxInputTokens ?? member.contextWindow!),
|
|
1463
|
+
);
|
|
1464
|
+
const defaultReasoningEffort = effectiveComboDefault(
|
|
1465
|
+
combo.defaultEffort,
|
|
1466
|
+
reasoningEfforts,
|
|
1467
|
+
);
|
|
1468
|
+
|
|
1469
|
+
return {
|
|
1470
|
+
provider: COMBO_NAMESPACE,
|
|
1471
|
+
id,
|
|
1472
|
+
owned_by: COMBO_NAMESPACE,
|
|
1473
|
+
contextWindow,
|
|
1474
|
+
maxInputTokens,
|
|
1475
|
+
inputModalities,
|
|
1476
|
+
reasoningEfforts,
|
|
1477
|
+
...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
|
|
1478
|
+
...(members.every(member => member.parallelToolCalls === true)
|
|
1479
|
+
? { parallelToolCalls: true }
|
|
1480
|
+
: {}),
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function safeCatalogWarningLabel(value: string): string {
|
|
1485
|
+
return redactSecretString(value)
|
|
1486
|
+
.replace(/[\u0000-\u001f\u007f]/g, "?")
|
|
1487
|
+
.slice(0, 200);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function comboCatalogWarningSignature(
|
|
1491
|
+
combo: NormalizedComboConfig,
|
|
1492
|
+
members: readonly CatalogModel[],
|
|
1493
|
+
): string {
|
|
1494
|
+
const discovered = new Map<string, CatalogModel>(members.map(member => [
|
|
1495
|
+
`${member.provider}/${member.id}`,
|
|
1496
|
+
member,
|
|
1497
|
+
] as const));
|
|
1498
|
+
return JSON.stringify(combo.targets.map(target => {
|
|
1499
|
+
const key = targetKey(target);
|
|
1500
|
+
const member = discovered.get(key);
|
|
1501
|
+
return {
|
|
1502
|
+
key,
|
|
1503
|
+
contextWindow: member?.contextWindow ?? null,
|
|
1504
|
+
maxInputTokens: member?.maxInputTokens ?? null,
|
|
1505
|
+
inputModalities: [...new Set(member?.inputModalities ?? [])].sort(),
|
|
1506
|
+
reasoningEfforts: [...new Set(member?.reasoningEfforts ?? [])].sort(),
|
|
1507
|
+
parallelToolCalls: member?.parallelToolCalls === true,
|
|
1508
|
+
};
|
|
1509
|
+
}).sort((a, b) => a.key.localeCompare(b.key)));
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function warnUncataloguedComboOnce(
|
|
1513
|
+
id: string,
|
|
1514
|
+
combo: NormalizedComboConfig,
|
|
1515
|
+
members: readonly CatalogModel[],
|
|
1516
|
+
): void {
|
|
1517
|
+
const signature = comboCatalogWarningSignature(combo, members);
|
|
1518
|
+
if (comboCatalogWarningSignatures.get(id) === signature) return;
|
|
1519
|
+
comboCatalogWarningSignatures.set(id, signature);
|
|
1520
|
+
const targets = combo.targets
|
|
1521
|
+
.map(target => safeCatalogWarningLabel(targetKey(target)))
|
|
1522
|
+
.sort((a, b) => a.localeCompare(b));
|
|
1523
|
+
console.warn(
|
|
1524
|
+
`[opencodex] Combo "${safeCatalogWarningLabel(id)}" is omitted from the catalog because member capabilities are incomplete: ${targets.join(", ")}.`,
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
export function exactComboCatalogSlugs(config: Pick<OcxConfig, "combos">): Set<string> {
|
|
1529
|
+
return new Set(listComboIds(config).map(comboModelId));
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function normalizedOpenAiApiSignature(model: CatalogModel): string {
|
|
1533
|
+
const normalized = {
|
|
1534
|
+
provider: model.provider,
|
|
1535
|
+
id: model.id,
|
|
1536
|
+
contextWindow: model.contextWindow ?? null,
|
|
1537
|
+
maxInputTokens: model.maxInputTokens ?? null,
|
|
1538
|
+
inputModalities: [...new Set(model.inputModalities ?? [])].sort(),
|
|
1539
|
+
reasoningEfforts: [...new Set(model.reasoningEfforts ?? [])].sort(),
|
|
1540
|
+
ownedBy: model.owned_by ?? null,
|
|
1541
|
+
};
|
|
1542
|
+
return JSON.stringify(normalized);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
export function resetOpenAiApiCatalogWarningStateForTests(): void {
|
|
1546
|
+
openAiApiCollisionWarnings.clear();
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/** Test-only reset for every process-global catalog cache/warning owner. */
|
|
1550
|
+
export function resetCatalogRuntimeStateForTests(): void {
|
|
1551
|
+
bundledCatalogCache = null;
|
|
1552
|
+
lastDropWarnSignature.clear();
|
|
1553
|
+
openAiApiCollisionWarnings.clear();
|
|
1554
|
+
comboCatalogWarningSignatures.clear();
|
|
1555
|
+
clearModelCache();
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
export function augmentRoutedModelsWithRegistryOpenAiApiRows(
|
|
1559
|
+
models: CatalogModel[],
|
|
1560
|
+
config: OcxConfig,
|
|
1561
|
+
): CatalogModel[] {
|
|
1562
|
+
const configured = config.providers[OPENAI_API_PROVIDER_ID];
|
|
1563
|
+
if (!configured || configured.disabled === true) return models;
|
|
1564
|
+
const entry = getProviderRegistryEntry(OPENAI_API_PROVIDER_ID);
|
|
1565
|
+
if (!entry?.models) return models;
|
|
1566
|
+
|
|
1567
|
+
const existingById = new Map(
|
|
1568
|
+
models.filter(model => model.provider === OPENAI_API_PROVIDER_ID).map(model => [model.id, model]),
|
|
1569
|
+
);
|
|
1570
|
+
const trustedRows = entry.models.map((id): CatalogModel => {
|
|
1571
|
+
const officialContext = entry.modelContextWindows?.[id];
|
|
1572
|
+
const officialMaxInput = entry.modelMaxInputTokens?.[id];
|
|
1573
|
+
const userContext = configured.modelContextWindows?.[id] ?? configured.contextWindow;
|
|
1574
|
+
const userMaxInput = configured.modelMaxInputTokens?.[id];
|
|
1575
|
+
const providerCap = providerContextCap(config, OPENAI_API_PROVIDER_ID);
|
|
1576
|
+
const contextWindow = typeof officialContext === "number"
|
|
1577
|
+
? Math.min(officialContext, userContext ?? officialContext, providerCap ?? officialContext)
|
|
1578
|
+
: undefined;
|
|
1579
|
+
const maxInputTokens = typeof officialMaxInput === "number"
|
|
1580
|
+
? Math.min(officialMaxInput, userMaxInput ?? officialMaxInput)
|
|
1581
|
+
: undefined;
|
|
1582
|
+
return {
|
|
1583
|
+
provider: OPENAI_API_PROVIDER_ID,
|
|
1584
|
+
id,
|
|
1585
|
+
owned_by: OPENAI_API_PROVIDER_ID,
|
|
1586
|
+
...(contextWindow ? { contextWindow } : {}),
|
|
1587
|
+
...(maxInputTokens ? { maxInputTokens } : {}),
|
|
1588
|
+
...(entry.modelInputModalities?.[id] ? { inputModalities: [...entry.modelInputModalities[id]!] } : {}),
|
|
1589
|
+
...(entry.modelReasoningEfforts?.[id] ? { reasoningEfforts: [...entry.modelReasoningEfforts[id]!] } : {}),
|
|
1590
|
+
};
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
for (const trusted of trustedRows) {
|
|
1594
|
+
const live = existingById.get(trusted.id);
|
|
1595
|
+
if (!live) continue;
|
|
1596
|
+
const liveSignature = normalizedOpenAiApiSignature(live);
|
|
1597
|
+
const trustedSignature = normalizedOpenAiApiSignature(trusted);
|
|
1598
|
+
if (liveSignature === trustedSignature) continue;
|
|
1599
|
+
const warningKey = `${trusted.provider}/${trusted.id}\n${liveSignature}\n${trustedSignature}`;
|
|
1600
|
+
if (openAiApiCollisionWarnings.has(warningKey)) continue;
|
|
1601
|
+
openAiApiCollisionWarnings.add(warningKey);
|
|
1602
|
+
console.warn(`[opencodex] replacing conflicting live OpenAI API metadata for ${trusted.provider}/${trusted.id} with trusted registry metadata`);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
return [
|
|
1606
|
+
...models.filter(model => model.provider !== OPENAI_API_PROVIDER_ID),
|
|
1607
|
+
...trustedRows,
|
|
1608
|
+
];
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1297
1611
|
export function augmentRoutedModelsWithJawcodeMetadata(
|
|
1298
1612
|
models: CatalogModel[],
|
|
1299
1613
|
providerNames: string[],
|
|
@@ -1360,6 +1674,8 @@ export function mergeCatalogEntriesForSync(
|
|
|
1360
1674
|
return slash > 0 ? [slug.slice(0, slash)] : [];
|
|
1361
1675
|
})),
|
|
1362
1676
|
multiAgentMode: MultiAgentMode = "default",
|
|
1677
|
+
exactComboSlugs: ReadonlySet<string> = new Set(),
|
|
1678
|
+
hasPhysicalComboProvider = false,
|
|
1363
1679
|
): RawEntry[] {
|
|
1364
1680
|
const rank = new Map(featured.map((slug, i) => [slug, i] as const));
|
|
1365
1681
|
const native = catalogModels
|
|
@@ -1413,13 +1729,15 @@ export function mergeCatalogEntriesForSync(
|
|
|
1413
1729
|
native.push(deriveEntry(template ? JSON.parse(JSON.stringify(template)) : null, slug, "OpenAI native model (Codex OAuth passthrough).", priority));
|
|
1414
1730
|
}
|
|
1415
1731
|
|
|
1732
|
+
const freshSlugs = new Set(
|
|
1733
|
+
routedEntries.flatMap(entry => typeof entry.slug === "string" ? [entry.slug] : []),
|
|
1734
|
+
);
|
|
1416
1735
|
let finalRoutedEntries = routedEntries;
|
|
1417
1736
|
const preservingExistingRouted = routedEntries.length === 0
|
|
1418
1737
|
&& catalogModels.some(m => typeof m.slug === "string" && (m.slug as string).includes("/"));
|
|
1419
1738
|
if (preservingExistingRouted) {
|
|
1420
1739
|
finalRoutedEntries = catalogModels.filter(m => typeof m.slug === "string" && (m.slug as string).includes("/"));
|
|
1421
1740
|
} else {
|
|
1422
|
-
const freshSlugs = new Set(routedEntries.flatMap(entry => typeof entry.slug === "string" ? [entry.slug] : []));
|
|
1423
1741
|
const preservedForeignRouted = catalogModels.filter(m => {
|
|
1424
1742
|
if (typeof m.slug !== "string" || !m.slug.includes("/")) return false;
|
|
1425
1743
|
const provider = m.slug.slice(0, m.slug.indexOf("/"));
|
|
@@ -1427,6 +1745,17 @@ export function mergeCatalogEntriesForSync(
|
|
|
1427
1745
|
});
|
|
1428
1746
|
finalRoutedEntries = [...routedEntries, ...preservedForeignRouted];
|
|
1429
1747
|
}
|
|
1748
|
+
if (!hasPhysicalComboProvider) {
|
|
1749
|
+
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
1750
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1751
|
+
return !slug.startsWith(`${COMBO_NAMESPACE}/`) || freshSlugs.has(slug);
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
finalRoutedEntries = finalRoutedEntries.filter(entry => {
|
|
1755
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
1756
|
+
return !exactComboSlugs.has(slug)
|
|
1757
|
+
|| (Array.isArray(entry.input_modalities) && entry.input_modalities.length > 0);
|
|
1758
|
+
});
|
|
1430
1759
|
// Reapply final catalog policy to rows preserved from disk. Those rows bypass
|
|
1431
1760
|
// gatherRoutedModels, so filtering only the freshly gathered list can resurrect an excluded id.
|
|
1432
1761
|
finalRoutedEntries = finalRoutedEntries.filter(entry =>
|
|
@@ -1439,11 +1768,14 @@ export function mergeCatalogEntriesForSync(
|
|
|
1439
1768
|
const mergedEntries = [...native, ...finalRoutedEntries].map(m => {
|
|
1440
1769
|
const normalized = normalizeServiceTiers(m);
|
|
1441
1770
|
applyNativeOpenAiContextOverride(normalized);
|
|
1442
|
-
const
|
|
1771
|
+
const exactCombo = typeof m.slug === "string" && exactComboSlugs.has(m.slug);
|
|
1772
|
+
const e = ensureStrictCatalogFields(normalized, {
|
|
1773
|
+
preserveExactInputModalities: exactCombo,
|
|
1774
|
+
});
|
|
1443
1775
|
// Mock-max universality (260709): preserved routed entries from disk may predate
|
|
1444
1776
|
// the max rung — ensure it here so subagent max spawns validate on every
|
|
1445
1777
|
// reasoning-capable entry. max only: 5.6 exact ladders (luna: no ultra) stay intact.
|
|
1446
|
-
{
|
|
1778
|
+
if (!exactCombo) {
|
|
1447
1779
|
const levels = Array.isArray(e.supported_reasoning_levels)
|
|
1448
1780
|
? e.supported_reasoning_levels as Array<{ effort?: string }>
|
|
1449
1781
|
: [];
|
|
@@ -1495,7 +1827,9 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{ added: num
|
|
|
1495
1827
|
const featured = config.subagentModels ?? [];
|
|
1496
1828
|
const orderedGoModels = orderForSubagents(enabledGo, featured); // stable tie-break among equal priorities
|
|
1497
1829
|
const multiAgentMode: MultiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
1498
|
-
const
|
|
1830
|
+
const exactComboSlugs = exactComboCatalogSlugs(config);
|
|
1831
|
+
const hasPhysicalComboProvider = Object.hasOwn(config.providers, COMBO_NAMESPACE);
|
|
1832
|
+
const goEntries = buildCatalogEntries(template ? JSON.parse(JSON.stringify(template)) : null, [], orderedGoModels, featured, websocketsEnabled(config), multiAgentMode, exactComboSlugs);
|
|
1499
1833
|
// Keep genuine native entries (gpt-*, codex-*) with their real per-model fields and append
|
|
1500
1834
|
// routed providers as namespaced slugs. Cursor and other adopted providers can expose model ids
|
|
1501
1835
|
// like `gpt-5.5`; those must not delete the native OpenAI/Codex base row.
|
|
@@ -1510,7 +1844,7 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{ added: num
|
|
|
1510
1844
|
// native AND routed so the advertised flag matches the implemented endpoint (phase 120.4) and a
|
|
1511
1845
|
// native template can never leak supports_websockets while the flag is off.
|
|
1512
1846
|
const wsEnabled = websocketsEnabled(config);
|
|
1513
|
-
catalog.models = mergeCatalogEntriesForSync(catalog.models ?? [], goEntries, baseline, featured, wsEnabled, goIds, template, disabledNativeSlugs(config), gatheredProviderNames, multiAgentMode);
|
|
1847
|
+
catalog.models = mergeCatalogEntriesForSync(catalog.models ?? [], goEntries, baseline, featured, wsEnabled, goIds, template, disabledNativeSlugs(config), gatheredProviderNames, multiAgentMode, exactComboSlugs, hasPhysicalComboProvider);
|
|
1514
1848
|
|
|
1515
1849
|
atomicWriteFile(catalogPath, JSON.stringify(catalog, null, 2) + "\n");
|
|
1516
1850
|
return { added: goEntries.length, path: catalogPath };
|
package/src/codex/routing.ts
CHANGED
|
@@ -171,6 +171,7 @@ function isThreadAffinityExpired(entry: ThreadAffinityEntry, now: number): boole
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
function isThreadAffinityGenerationLive(entry: ThreadAffinityEntry): boolean {
|
|
174
|
+
if (entry.accountId === MAIN_CODEX_ACCOUNT_ID) return entry.generation === 0;
|
|
174
175
|
return isCodexAccountGenerationLive(entry.accountId, entry.generation);
|
|
175
176
|
}
|
|
176
177
|
|
|
@@ -196,13 +197,13 @@ function pruneLruThreadAffinities(): void {
|
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
function bindThreadAffinity(threadId: string, accountId: string, now: number): void {
|
|
199
|
-
const record = readCodexAccountRecord(accountId);
|
|
200
|
-
if (!record?.credential || record.deletedAt != null) return;
|
|
200
|
+
const record = accountId === MAIN_CODEX_ACCOUNT_ID ? undefined : readCodexAccountRecord(accountId);
|
|
201
|
+
if (accountId !== MAIN_CODEX_ACCOUNT_ID && (!record?.credential || record.deletedAt != null)) return;
|
|
201
202
|
pruneExpiredThreadAffinities(now);
|
|
202
203
|
const previous = threadAccountMap.get(threadId);
|
|
203
204
|
threadAccountMap.set(threadId, {
|
|
204
205
|
accountId,
|
|
205
|
-
generation: record
|
|
206
|
+
generation: accountId === MAIN_CODEX_ACCOUNT_ID ? 0 : record!.generation,
|
|
206
207
|
createdAt: previous?.createdAt ?? now,
|
|
207
208
|
lastUsedAt: now,
|
|
208
209
|
lastReevalAt: now,
|
|
@@ -380,7 +381,12 @@ export function resolveCodexAccountForThreadDetailed(
|
|
|
380
381
|
threadAccountMap.delete(threadId);
|
|
381
382
|
}
|
|
382
383
|
let active = config.activeCodexAccountId;
|
|
383
|
-
if (!active)
|
|
384
|
+
if (!active) {
|
|
385
|
+
const selected = pickLowestUsageCodexAccount(config, undefined, now);
|
|
386
|
+
if (!selected) return { status: "none" };
|
|
387
|
+
setActiveCodexAccount(config, selected);
|
|
388
|
+
active = selected;
|
|
389
|
+
}
|
|
384
390
|
if (!isCodexAccountSelectable(config, active, now)) {
|
|
385
391
|
const fallback = pickLowestUsageCodexAccount(config, active, now);
|
|
386
392
|
if (fallback) {
|