@bitkyc08/opencodex 2.7.1 → 2.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,6 @@ import { atomicWriteFile, expandUserPath, getConfigDir, websocketsEnabled } from
6
6
  import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "./paths";
7
7
  import { DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCached, isModelsFetchCoolingDown, markModelsFetchFailure, setCached } from "./model-cache";
8
8
  import { buildModelsRequest, resolveModelsAuthToken } from "../oauth";
9
- import { effectiveGoogleMode } from "../providers/registry";
10
9
  import type { OcxConfig, OcxProviderConfig } from "../types";
11
10
  import { modelInList } from "../types";
12
11
  import { CODEX_REASONING_LEVELS, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../reasoning-effort";
@@ -254,6 +253,30 @@ function shouldUpgradeToUpstreamEntry(entry: RawEntry): boolean {
254
253
  && entry.display_name === entry.slug;
255
254
  }
256
255
 
256
+ /**
257
+ * Reasoning efforts each requested slug advertises in the INJECTED on-disk catalog —
258
+ * the exact list codex-rs validates spawn_agent `reasoning_effort` arguments against
259
+ * (unsupported rungs are then clamped on the wire, nativeEffortClamp/adapters).
260
+ * Slugs missing from the catalog are omitted from the result. Used by the delegation
261
+ * prompt to advertise the featured sub-agent roster with honest effort ladders.
262
+ */
263
+ export function catalogModelEfforts(slugs: readonly string[]): Map<string, string[]> {
264
+ const out = new Map<string, string[]>();
265
+ if (slugs.length === 0) return out;
266
+ const catalog = readCatalog(readCodexCatalogPath());
267
+ if (!catalog) return out;
268
+ const wanted = new Set(slugs);
269
+ for (const entry of catalog.models ?? []) {
270
+ if (typeof entry.slug !== "string" || !wanted.has(entry.slug)) continue;
271
+ const levels = Array.isArray(entry.supported_reasoning_levels)
272
+ ? entry.supported_reasoning_levels as Array<{ effort?: string }>
273
+ : [];
274
+ const efforts = levels.flatMap(l => typeof l.effort === "string" ? [l.effort] : []);
275
+ if (efforts.length > 0) out.set(entry.slug, efforts);
276
+ }
277
+ return out;
278
+ }
279
+
257
280
  /**
258
281
  * The native (passthrough) OpenAI slugs to advertise — the LIVE Codex catalog's own bare slugs when
259
282
  * available, with documented Codex-native additions layered in, else the static fallback above.
@@ -937,23 +960,14 @@ type ProviderModelsApiItem = {
937
960
  };
938
961
  };
939
962
 
940
- /** Generative Language API `models.list` item (`{ models: [...] }`, not OpenAI's `{ data }`). */
941
- type GoogleModelsApiModel = {
942
- name?: string;
943
- inputTokenLimit?: number;
944
- supportedGenerationMethods?: string[];
945
- };
946
-
947
- function googleModelsToApiItems(models: GoogleModelsApiModel[]): ProviderModelsApiItem[] {
948
- return models
949
- // Keep chat-capable models only; a model missing the field stays (defensive).
950
- .filter(m => m.supportedGenerationMethods?.includes("generateContent") ?? true)
951
- .map(m => ({
952
- id: (m.name ?? "").replace(/^models\//, ""),
953
- owned_by: "google",
954
- ...(typeof m.inputTokenLimit === "number" && m.inputTokenLimit > 0 ? { context_length: m.inputTokenLimit } : {}),
955
- }))
956
- .filter(m => m.id.length > 0);
963
+ function isProviderModelsApiItems(value: unknown): value is ProviderModelsApiItem[] {
964
+ return Array.isArray(value) && value.every(item =>
965
+ item !== null
966
+ && typeof item === "object"
967
+ && !Array.isArray(item)
968
+ && typeof (item as { id?: unknown }).id === "string"
969
+ && (item as { id: string }).id.trim().length > 0
970
+ );
957
971
  }
958
972
 
959
973
  function configuredContextWindow(prov: OcxProviderConfig, id: string): number | undefined {
@@ -1045,9 +1059,10 @@ function catalogHintsFromModelsApiItem(providerName: string, item: ProviderModel
1045
1059
 
1046
1060
  /**
1047
1061
  * Fetch a provider's `/models` (openai-chat style) with a TTL cache + stale fallback. Skips
1048
- * forward-auth providers. Fresh cache → no network; live fetch → cache the merged result;
1049
- * fetch failure → last-known-good cache (so a provider blip doesn't drop its models), else the
1050
- * static config list. This is the per-provider half of jawcode's "always latest" resolver.
1062
+ * forward-auth providers. Fresh cache → no network; schema-valid live fetch → cache the
1063
+ * authoritative result; fetch failure or malformed data → last-known-good cache (so a provider
1064
+ * blip doesn't drop its models), else the static config list. This is the per-provider half of
1065
+ * jawcode's "always latest" resolver.
1051
1066
  */
1052
1067
  async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs: number, contextCap?: number): Promise<CatalogModel[]> {
1053
1068
  if (prov.authMode === "forward") return []; // ChatGPT backend has no /models
@@ -1065,13 +1080,16 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
1065
1080
  // plan (e.g. claude-fable-5) drop out instead of failing ERROR_BAD_MODEL_NAME. Fall back to the seed.
1066
1081
  const cachedCursor = getFreshCached(name, ttlMs);
1067
1082
  if (cachedCursor) return applyConfigHintsToCachedModels(name, prov, cachedCursor);
1068
- const liveIds = await fetchCursorUsableModels({ apiKey, baseUrl: prov.baseUrl });
1069
- if (liveIds) {
1070
- const available = filterCursorConfiguredModelsByLiveDiscovery(configured, liveIds);
1083
+ const liveResult = await fetchCursorUsableModels({ apiKey, baseUrl: prov.baseUrl });
1084
+ if (liveResult.ok) {
1085
+ const available = filterCursorConfiguredModelsByLiveDiscovery(configured, liveResult.models);
1071
1086
  const result = available.length > 0 ? available : configured;
1072
1087
  setCached(name, result);
1073
1088
  return result;
1074
1089
  }
1090
+ console.warn(
1091
+ `[opencodex] Cursor model discovery for "${name}" failed [${liveResult.error}]${liveResult.detail ? `: ${liveResult.detail}` : ""}; using stale/static catalog degradation.`,
1092
+ );
1075
1093
  const staleCursor = getStaleCached(name);
1076
1094
  return staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor) : configured;
1077
1095
  }
@@ -1095,10 +1113,19 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
1095
1113
  const stale = getStaleCached(name);
1096
1114
  return stale ? applyConfigHintsToCachedModels(name, prov, stale, contextCap) : configured;
1097
1115
  }
1098
- const json = await res.json() as { data?: ProviderModelsApiItem[]; models?: GoogleModelsApiModel[] };
1099
- const items: ProviderModelsApiItem[] = effectiveGoogleMode(name, prov) === "ai-studio" && Array.isArray(json.models)
1100
- ? googleModelsToApiItems(json.models)
1101
- : (json.data ?? []);
1116
+ const json = await res.json() as unknown;
1117
+ const data = json !== null && typeof json === "object" && !Array.isArray(json)
1118
+ ? (json as { data?: unknown }).data
1119
+ : undefined;
1120
+ if (!isProviderModelsApiItems(data)) {
1121
+ markModelsFetchFailure(name);
1122
+ console.warn(
1123
+ `[opencodex] Provider model discovery for "${name}" returned malformed 2xx data; using stale/static catalog degradation.`,
1124
+ );
1125
+ const stale = getStaleCached(name);
1126
+ return stale ? applyConfigHintsToCachedModels(name, prov, stale, contextCap) : configured;
1127
+ }
1128
+ const items = data;
1102
1129
  const live = items.map(m => applyProviderConfigHints(name, prov, {
1103
1130
  id: m.id,
1104
1131
  provider: name,
@@ -1106,10 +1133,18 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
1106
1133
  ...catalogHintsFromModelsApiItem(name, m),
1107
1134
  }, contextCap));
1108
1135
  const liveIds = new Set(live.map(m => m.id));
1109
- // Merge explicit config additions (e.g. a model not in the provider's /models, like a new endpoint).
1110
- const merged = [...live, ...configured.filter(m => !liveIds.has(m.id))];
1111
- setCached(name, merged);
1112
- return merged;
1136
+ const droppedConfiguredIds = configured.filter(m => !liveIds.has(m.id)).map(m => m.id);
1137
+ if (live.length === 0) {
1138
+ console.warn(
1139
+ `[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"}.`,
1140
+ );
1141
+ } else if (droppedConfiguredIds.length > 0) {
1142
+ console.warn(
1143
+ `[opencodex] Provider model discovery for "${name}" omitted configured model ids; dropping them from the authoritative live catalog: ${droppedConfiguredIds.join(", ")}.`,
1144
+ );
1145
+ }
1146
+ setCached(name, live);
1147
+ return live;
1113
1148
  } catch {
1114
1149
  markModelsFetchFailure(name);
1115
1150
  const stale = getStaleCached(name);
package/src/config.ts CHANGED
@@ -228,8 +228,31 @@ export type ConfigDiagnostics = {
228
228
  config: OcxConfig;
229
229
  source: "default" | "file" | "fallback";
230
230
  error: string | null;
231
+ /** Non-fatal config concerns; absent when there are no warnings. */
232
+ warnings?: string[];
231
233
  };
232
234
 
235
+ function configPlaceholderWarnings(config: OcxConfig): string[] {
236
+ const warnings: string[] = [];
237
+ for (const [name, provider] of Object.entries(config.providers)) {
238
+ const placeholder = provider.baseUrl.match(/\{[^}]*\}/)?.[0];
239
+ if (placeholder) {
240
+ warnings.push(`providers.${name}.baseUrl contains unresolved ${placeholder}; set the real provider URL`);
241
+ }
242
+ }
243
+ return warnings;
244
+ }
245
+
246
+ function validFileConfigDiagnostics(config: OcxConfig): ConfigDiagnostics {
247
+ const warnings = configPlaceholderWarnings(config);
248
+ return {
249
+ config,
250
+ source: "file",
251
+ error: null,
252
+ ...(warnings.length > 0 ? { warnings } : {}),
253
+ };
254
+ }
255
+
233
256
  function mergeConfigDefaults(parsed: unknown): unknown {
234
257
  if (!parsed || typeof parsed !== "object") return parsed;
235
258
  const defaults = getDefaultConfig();
@@ -261,12 +284,12 @@ export function readConfigDiagnostics(): ConfigDiagnostics {
261
284
  const parsed = JSON.parse(raw);
262
285
  const result = configSchema.safeParse(parsed);
263
286
  if (result.success) {
264
- return { config: result.data as OcxConfig, source: "file", error: null };
287
+ return validFileConfigDiagnostics(result.data as OcxConfig);
265
288
  }
266
289
 
267
290
  const retryResult = configSchema.safeParse(mergeConfigDefaults(parsed));
268
291
  if (retryResult.success) {
269
- return { config: retryResult.data as OcxConfig, source: "file", error: null };
292
+ return validFileConfigDiagnostics(retryResult.data as OcxConfig);
270
293
  }
271
294
 
272
295
  return { config: getDefaultConfig(), source: "fallback", error: schemaDiagnosticsError(result.error) };
@@ -248,9 +248,10 @@ export async function resolveModelsAuthToken(name: string, prov: OcxProviderConf
248
248
  * LIVE catalog correctly per adapter. Anthropic is the special case: its endpoint is `/v1/models`
249
249
  * (not `/models`), it needs `anthropic-version`, and it authenticates with `x-api-key` (key) or
250
250
  * `Authorization: Bearer` + the OAuth beta (oauth) — not a bare Bearer. Google (ai-studio mode)
251
- * is the other special case: `x-goog-api-key` + `/v1beta/models`, returning `{ models: [...] }`
252
- * (parsed by the caller). Everyone else uses the OpenAI-style `/models` + Bearer with a
253
- * `{ data: [{ id, owned_by? }] }` response.
251
+ * is the other special case: `x-goog-api-key` + `/v1beta/models`, returning `{ models: [...] }`.
252
+ * The catalog authority gate intentionally degrades that non-OpenAI shape to stale/static data.
253
+ * Everyone else uses the OpenAI-style `/models` + Bearer with a `{ data: [{ id, owned_by? }] }`
254
+ * response.
254
255
  */
255
256
  export function buildModelsRequest(prov: OcxProviderConfig, apiKey: string | undefined, providerName = ""): { url: string; headers: Record<string, string> } {
256
257
  const headers: Record<string, string> = { ...(prov.headers ?? {}) };
@@ -72,7 +72,7 @@ export async function validateApiKey(provider: KeyLoginProvider, key: string): P
72
72
  "x-api-key": key,
73
73
  },
74
74
  body: JSON.stringify({
75
- model: provider.defaultModel ?? "claude-sonnet-4-6",
75
+ model: provider.defaultModel ?? "claude-haiku-4-5",
76
76
  max_tokens: 1,
77
77
  messages: [{ role: "user", content: "ping" }],
78
78
  }),
@@ -57,12 +57,20 @@ function stringField(data: JsonObject, ...keys: string[]): string | undefined {
57
57
  return undefined;
58
58
  }
59
59
 
60
- function parseExpires(value: unknown): number {
60
+ function parseExpires(value: unknown, present: boolean, hasRefreshToken: boolean): number {
61
61
  if (typeof value === "number" && Number.isFinite(value)) return value < 10_000_000_000 ? value * 1000 : value;
62
62
  if (typeof value === "string" && value.length > 0) {
63
63
  const parsed = Date.parse(value);
64
64
  if (Number.isFinite(parsed)) return parsed;
65
65
  }
66
+ if (present) {
67
+ console.warn(
68
+ `[ocx:kiro:credentials] credential expiry is present but unparseable; ${
69
+ hasRefreshToken ? "treating credential as expired" : "using the default TTL because no refresh token is available"
70
+ }`,
71
+ );
72
+ if (hasRefreshToken) return 0;
73
+ }
66
74
  return Date.now() + DEFAULT_EXPIRES_MS;
67
75
  }
68
76
 
@@ -110,10 +118,12 @@ function credentialFromJson(data: JsonObject, source: KiroCredentialSource): Imp
110
118
  const apiRegion = stringField(data, "apiRegion", "api_region") || inferRegionFromProfileArn(profileArn) || ssoRegion;
111
119
  const clientId = stringField(data, "clientId", "client_id");
112
120
  const clientSecret = stringField(data, "clientSecret", "client_secret");
121
+ const refresh = stringField(data, "refreshToken", "refresh_token") || "";
122
+ const expiryPresent = Object.hasOwn(data, "expiresAt") || Object.hasOwn(data, "expires_at");
113
123
  return {
114
124
  access,
115
- refresh: stringField(data, "refreshToken", "refresh_token") || "",
116
- expires: parseExpires(data.expiresAt ?? data.expires_at),
125
+ refresh,
126
+ expires: parseExpires(data.expiresAt ?? data.expires_at, expiryPresent, refresh.length > 0),
117
127
  source,
118
128
  authType: clientId && clientSecret ? "aws_sso_oidc" : "kiro_desktop",
119
129
  ...(profileArn ? { profileArn } : {}),
@@ -67,6 +67,8 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
67
67
  adapter: entry.adapter,
68
68
  baseUrl: entry.baseUrl,
69
69
  authMode: entry.authKind === "local" ? undefined : entry.authKind,
70
+ ...(entry.keyOptional !== undefined ? { keyOptional: entry.keyOptional } : {}),
71
+ ...(entry.modelSuffixBracketStrip !== undefined ? { modelSuffixBracketStrip: entry.modelSuffixBracketStrip } : {}),
70
72
  ...(entry.defaultModel ? { defaultModel: entry.defaultModel } : {}),
71
73
  ...(entry.models ? { models: [...entry.models] } : {}),
72
74
  ...(entry.liveModels !== undefined ? { liveModels: entry.liveModels } : {}),
@@ -189,6 +191,8 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
189
191
  if (!prov.thinkingToggleModels && seed.thinkingToggleModels) prov.thinkingToggleModels = [...seed.thinkingToggleModels];
190
192
  if (!prov.thinkingBudgetModels && seed.thinkingBudgetModels) prov.thinkingBudgetModels = [...seed.thinkingBudgetModels];
191
193
  if (prov.escapeBuiltinToolNames === undefined && seed.escapeBuiltinToolNames !== undefined) prov.escapeBuiltinToolNames = seed.escapeBuiltinToolNames;
194
+ if (prov.keyOptional === undefined && seed.keyOptional !== undefined) prov.keyOptional = seed.keyOptional;
195
+ if (prov.modelSuffixBracketStrip === undefined && seed.modelSuffixBracketStrip !== undefined) prov.modelSuffixBracketStrip = seed.modelSuffixBracketStrip;
192
196
  }
193
197
 
194
198
  export function deriveFeaturedProviderIds(): string[] {
@@ -49,7 +49,7 @@ export const KIRO_MODEL_REASONING_EFFORTS: Record<string, string[]> = Object.fro
49
49
  export function normalizeKiroModelId(id: string): string {
50
50
  let model = id.trim().toLowerCase();
51
51
  model = model.replace(/^kiro\//, "").replace(/^kiro-/, "");
52
- if (model === "auto" || model === "kiro-auto") return "auto";
52
+ if (model === "auto") return "auto";
53
53
 
54
54
  model = model.replace(/-\d{8}$/, "");
55
55
  model = model.replace(/-(low|medium|high|xhigh|max)$/, "");