@bitkyc08/opencodex 2.7.1 → 2.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/gui/dist/assets/index-D7o1qwy-.css +1 -0
- package/gui/dist/assets/index-DT4C-vKW.js +40 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +14 -3
- package/src/adapters/azure.ts +16 -11
- package/src/adapters/cursor/live-models.ts +36 -24
- package/src/adapters/cursor/live-transport.ts +5 -17
- package/src/adapters/cursor/native-exec.ts +11 -7
- package/src/adapters/cursor/transport-retry.ts +1 -4
- package/src/adapters/google.ts +32 -9
- package/src/adapters/kiro-retry.ts +6 -2
- package/src/adapters/kiro.ts +14 -9
- package/src/adapters/openai-chat.ts +38 -28
- package/src/codex/catalog.ts +67 -32
- package/src/config.ts +25 -2
- package/src/oauth/index.ts +4 -3
- package/src/oauth/key-providers.ts +1 -1
- package/src/oauth/kiro-credentials.ts +13 -3
- package/src/providers/derive.ts +4 -0
- package/src/providers/kiro-models.ts +1 -1
- package/src/providers/registry.ts +129 -40
- package/src/responses/parser.ts +12 -0
- package/src/router.ts +13 -1
- package/src/server/management-api.ts +10 -2
- package/src/server/request-log.ts +25 -1
- package/src/server/responses.ts +141 -43
- package/src/types.ts +12 -2
- package/src/update/index.ts +1 -1
- package/gui/dist/assets/index-BUAMcKFd.css +0 -1
- package/gui/dist/assets/index-CtHGtaW8.js +0 -34
package/src/codex/catalog.ts
CHANGED
|
@@ -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
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
|
|
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
|
|
1049
|
-
* fetch failure → last-known-good cache (so a provider
|
|
1050
|
-
* static config list. This is the per-provider half of
|
|
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
|
|
1069
|
-
if (
|
|
1070
|
-
const available = filterCursorConfiguredModelsByLiveDiscovery(configured,
|
|
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
|
|
1099
|
-
const
|
|
1100
|
-
?
|
|
1101
|
-
:
|
|
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
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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
|
|
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
|
|
292
|
+
return validFileConfigDiagnostics(retryResult.data as OcxConfig);
|
|
270
293
|
}
|
|
271
294
|
|
|
272
295
|
return { config: getDefaultConfig(), source: "fallback", error: schemaDiagnosticsError(result.error) };
|
package/src/oauth/index.ts
CHANGED
|
@@ -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
|
-
*
|
|
253
|
-
* `{ data: [{ id, owned_by? }] }`
|
|
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-
|
|
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
|
|
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 } : {}),
|
package/src/providers/derive.ts
CHANGED
|
@@ -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"
|
|
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)$/, "");
|