jeo-code 0.11.1 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/package.json +1 -1
- package/src/ai/model-cache.ts +22 -15
- package/src/ai/model-catalog.ts +20 -11
- package/src/ai/model-discovery.ts +68 -33
- package/src/ai/model-picker.ts +4 -0
- package/src/commands/launch.ts +14 -7
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,17 @@ The README mirrors the latest 5 entries — regenerate with `bun run changelog:s
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.11.2] - 2026-09-30
|
|
13
|
+
_OpenAI model pickers now follow the authenticated API and Codex catalogs, including subscription-only models, without hidden or stale entries._
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Use Codex `visibility` for ChatGPT model discovery; retain visible subscription-only models even when `supported_in_api` is false. Keep modern GPT Codex models in API-key discovery and fetch both catalogs when both credentials are configured.
|
|
17
|
+
- Preserve the full OpenAI model list by default, de-duplicate picker rows, and keep empty or failed live results honest instead of fabricating availability from a static snapshot.
|
|
18
|
+
- Replace account-scoped OpenAI model observations on refresh, migrate old caches, and separate authentication sources and custom endpoints. Use stored account identity for opaque OAuth credentials and the proxy's own API key for custom endpoints.
|
|
19
|
+
|
|
20
|
+
### Validation
|
|
21
|
+
- Regression coverage for visibility, dual credentials, complete lists, account headers, proxy scoping, cache replacement, and authoritative empty catalogs.
|
|
22
|
+
|
|
12
23
|
## [0.11.1] - 2026-08-25
|
|
13
24
|
_Every non-interactive `jeo` run hung forever once Telegram notifications were configured — `echo "..." | jeo`, `jeo -p "..."` in CI, any scripted use. The work completed and the command returned; the process just never exited._
|
|
14
25
|
|
package/README.ja.md
CHANGED
|
@@ -319,10 +319,10 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
319
319
|
|
|
320
320
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
321
321
|
- **[Unreleased]**
|
|
322
|
+
- **[0.11.2]** (2026-09-30) — OpenAI model pickers now follow the authenticated API and Codex catalogs, including subscription-only models, without hidden or stale entries.
|
|
322
323
|
- **[0.11.1]** (2026-08-25) — Every non-interactive `jeo` run hung forever once Telegram notifications were configured — `echo "..." | jeo`, `jeo -p "..."` in CI, any scripted use. The work completed and the command returned; the process just never exited.
|
|
323
324
|
- **[0.11.0]** (2026-08-25) — One bad boundary check was corrupting agent context, subagent fan-out, and the Telegram daemon's kill safety at the same time — and none of the three looked related from the outside.
|
|
324
325
|
- **[0.10.0]** (2026-08-25) — jeo could only be pointed at ONE user-supplied endpoint (`config.openaiBaseUrl`), and doing so rebound the built-in `openai` provider: it stole the `openai/` routing prefix, collided with real OpenAI model ids, and could not speak the Anthropic Messages protocol at all. There was no way to run a company LiteLLM proxy and a self-hosted vLLM box at the same time, or to reach either from a script.
|
|
325
|
-
- **[0.9.17]** (2026-08-18) — Live model discovery already existed (`listProviderModels`/`discoverModels`), but only `jeo auth login openai` ever called it — logging into Anthropic or Antigravity left the account pinned to the maintained static catalog snapshot until the next unrelated live-discovery call happened to run.
|
|
326
326
|
|
|
327
327
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
328
328
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -320,10 +320,10 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
320
320
|
|
|
321
321
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
322
322
|
- **[Unreleased]**
|
|
323
|
+
- **[0.11.2]** (2026-09-30) — OpenAI model pickers now follow the authenticated API and Codex catalogs, including subscription-only models, without hidden or stale entries.
|
|
323
324
|
- **[0.11.1]** (2026-08-25) — Every non-interactive `jeo` run hung forever once Telegram notifications were configured — `echo "..." | jeo`, `jeo -p "..."` in CI, any scripted use. The work completed and the command returned; the process just never exited.
|
|
324
325
|
- **[0.11.0]** (2026-08-25) — One bad boundary check was corrupting agent context, subagent fan-out, and the Telegram daemon's kill safety at the same time — and none of the three looked related from the outside.
|
|
325
326
|
- **[0.10.0]** (2026-08-25) — jeo could only be pointed at ONE user-supplied endpoint (`config.openaiBaseUrl`), and doing so rebound the built-in `openai` provider: it stole the `openai/` routing prefix, collided with real OpenAI model ids, and could not speak the Anthropic Messages protocol at all. There was no way to run a company LiteLLM proxy and a self-hosted vLLM box at the same time, or to reach either from a script.
|
|
326
|
-
- **[0.9.17]** (2026-08-18) — Live model discovery already existed (`listProviderModels`/`discoverModels`), but only `jeo auth login openai` ever called it — logging into Anthropic or Antigravity left the account pinned to the maintained static catalog snapshot until the next unrelated live-discovery call happened to run.
|
|
327
327
|
|
|
328
328
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
329
329
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -327,10 +327,10 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
327
327
|
|
|
328
328
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
329
329
|
- **[Unreleased]**
|
|
330
|
+
- **[0.11.2]** (2026-09-30) — OpenAI model pickers now follow the authenticated API and Codex catalogs, including subscription-only models, without hidden or stale entries.
|
|
330
331
|
- **[0.11.1]** (2026-08-25) — Every non-interactive `jeo` run hung forever once Telegram notifications were configured — `echo "..." | jeo`, `jeo -p "..."` in CI, any scripted use. The work completed and the command returned; the process just never exited.
|
|
331
332
|
- **[0.11.0]** (2026-08-25) — One bad boundary check was corrupting agent context, subagent fan-out, and the Telegram daemon's kill safety at the same time — and none of the three looked related from the outside.
|
|
332
333
|
- **[0.10.0]** (2026-08-25) — jeo could only be pointed at ONE user-supplied endpoint (`config.openaiBaseUrl`), and doing so rebound the built-in `openai` provider: it stole the `openai/` routing prefix, collided with real OpenAI model ids, and could not speak the Anthropic Messages protocol at all. There was no way to run a company LiteLLM proxy and a self-hosted vLLM box at the same time, or to reach either from a script.
|
|
333
|
-
- **[0.9.17]** (2026-08-18) — Live model discovery already existed (`listProviderModels`/`discoverModels`), but only `jeo auth login openai` ever called it — logging into Anthropic or Antigravity left the account pinned to the maintained static catalog snapshot until the next unrelated live-discovery call happened to run.
|
|
334
334
|
|
|
335
335
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
336
336
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -317,10 +317,10 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
317
317
|
|
|
318
318
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
319
319
|
- **[Unreleased]**
|
|
320
|
+
- **[0.11.2]** (2026-09-30) — OpenAI model pickers now follow the authenticated API and Codex catalogs, including subscription-only models, without hidden or stale entries.
|
|
320
321
|
- **[0.11.1]** (2026-08-25) — Every non-interactive `jeo` run hung forever once Telegram notifications were configured — `echo "..." | jeo`, `jeo -p "..."` in CI, any scripted use. The work completed and the command returned; the process just never exited.
|
|
321
322
|
- **[0.11.0]** (2026-08-25) — One bad boundary check was corrupting agent context, subagent fan-out, and the Telegram daemon's kill safety at the same time — and none of the three looked related from the outside.
|
|
322
323
|
- **[0.10.0]** (2026-08-25) — jeo could only be pointed at ONE user-supplied endpoint (`config.openaiBaseUrl`), and doing so rebound the built-in `openai` provider: it stole the `openai/` routing prefix, collided with real OpenAI model ids, and could not speak the Anthropic Messages protocol at all. There was no way to run a company LiteLLM proxy and a self-hosted vLLM box at the same time, or to reach either from a script.
|
|
323
|
-
- **[0.9.17]** (2026-08-18) — Live model discovery already existed (`listProviderModels`/`discoverModels`), but only `jeo auth login openai` ever called it — logging into Anthropic or Antigravity left the account pinned to the maintained static catalog snapshot until the next unrelated live-discovery call happened to run.
|
|
324
324
|
|
|
325
325
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
326
326
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/ai/model-cache.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Persisting the discovered lists (gjc parity) closes both gaps: the next launch
|
|
12
12
|
* rehydrates the account's REAL model set from disk before any network call, and a
|
|
13
13
|
* background refresh keeps it current. The cache is an optimization — a missing,
|
|
14
|
-
* corrupt, or stale file only means
|
|
14
|
+
* corrupt, or stale file only means live discovery must refresh the catalog,
|
|
15
15
|
* never an error.
|
|
16
16
|
*/
|
|
17
17
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
@@ -37,15 +37,16 @@ export interface CachedProviderModels {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export interface ModelCacheFile {
|
|
40
|
-
version:
|
|
40
|
+
version: 3;
|
|
41
41
|
updatedAt: number;
|
|
42
42
|
providers: CachedProviderModels[];
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
// Earlier snapshots could persist hidden or fabricated OpenAI entries.
|
|
46
|
+
const CACHE_VERSION = 3;
|
|
46
47
|
/** Refresh in the background once the cache is older than this (6h). */
|
|
47
48
|
export const MODEL_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
48
|
-
/** Bound
|
|
49
|
+
/** Bound other providers' cache rows; preserve complete authoritative OpenAI lists. */
|
|
49
50
|
const MAX_MODELS_PER_PROVIDER = 500;
|
|
50
51
|
|
|
51
52
|
function cacheDir(): string {
|
|
@@ -62,9 +63,9 @@ export function normalizeCacheEntries(raw: unknown): CachedProviderModels[] {
|
|
|
62
63
|
for (const entry of raw) {
|
|
63
64
|
const row = entry as Partial<CachedProviderModels>;
|
|
64
65
|
if (!row || typeof row.provider !== "string" || !Array.isArray(row.models)) continue;
|
|
65
|
-
const models = row.models.filter((m): m is string => typeof m === "string" && m.trim().length > 0).slice(0, MAX_MODELS_PER_PROVIDER);
|
|
66
|
-
if (models.length === 0) continue;
|
|
67
66
|
const provider = row.provider as ProviderName;
|
|
67
|
+
const models = row.models.filter((m): m is string => typeof m === "string" && m.trim().length > 0).slice(0, provider === "openai" ? undefined : MAX_MODELS_PER_PROVIDER);
|
|
68
|
+
if (models.length === 0 && (provider !== "openai" || row.models.length !== 0)) continue;
|
|
68
69
|
const source: ModelCacheSource =
|
|
69
70
|
row.source === "oauth" || row.source === "api_key" || row.source === "keyless" || row.source === "none" ? row.source : "none";
|
|
70
71
|
const baseUrl = typeof row.baseUrl === "string" ? row.baseUrl.replace(/\/+$/, "") : undefined;
|
|
@@ -111,26 +112,26 @@ export function isModelCacheStale(cache: ModelCacheFile | null, now = Date.now()
|
|
|
111
112
|
*/
|
|
112
113
|
export function mergeCacheEntries(
|
|
113
114
|
previous: readonly CachedProviderModels[],
|
|
114
|
-
results: readonly { provider: ProviderName; models: readonly string[]; ok: boolean; source?: ModelCacheSource; baseUrl?: string; accountId?: string }[],
|
|
115
|
+
results: readonly { provider: ProviderName; models: readonly string[]; ok: boolean; source?: ModelCacheSource; baseUrl?: string; accountId?: string; fallback?: boolean }[],
|
|
115
116
|
): CachedProviderModels[] {
|
|
116
117
|
const byKey = new Map<string, CachedProviderModels>();
|
|
117
|
-
const keyOf = (provider: string, baseUrl?: string, accountId?: string) =>
|
|
118
|
-
`${provider}\u0000${baseUrl ?? ""}\u0000${accountId ?? ""}`;
|
|
118
|
+
const keyOf = (provider: string, baseUrl?: string, accountId?: string, source?: ModelCacheSource) =>
|
|
119
|
+
`${provider}\u0000${baseUrl?.replace(/\/+$/, "") ?? ""}\u0000${accountId ?? ""}\u0000${provider === "openai" ? source ?? "none" : ""}`;
|
|
119
120
|
for (const entry of previous) {
|
|
120
121
|
const isOpenAIOAuth = entry.provider === "openai" && entry.source === "oauth";
|
|
121
122
|
if (isOpenAIOAuth && !entry.accountId) continue;
|
|
122
|
-
byKey.set(keyOf(entry.provider, entry.baseUrl, isOpenAIOAuth ? entry.accountId : undefined), entry);
|
|
123
|
+
byKey.set(keyOf(entry.provider, entry.baseUrl, isOpenAIOAuth ? entry.accountId : undefined, entry.source), entry);
|
|
123
124
|
}
|
|
124
125
|
for (const result of results) {
|
|
125
|
-
if (!result.ok || result.models.length === 0) continue;
|
|
126
|
+
if (!result.ok || result.fallback || (result.models.length === 0 && result.provider !== "openai")) continue;
|
|
126
127
|
const source: ModelCacheSource = result.source ?? "none";
|
|
127
128
|
const isOpenAIOAuth = result.provider === "openai" && source === "oauth";
|
|
128
129
|
const baseUrl = typeof result.baseUrl === "string" ? result.baseUrl.replace(/\/+$/, "") : undefined;
|
|
129
130
|
const accountId = typeof result.accountId === "string" ? result.accountId.trim() : "";
|
|
130
131
|
if (isOpenAIOAuth && !accountId) continue;
|
|
131
|
-
byKey.set(keyOf(result.provider, baseUrl, isOpenAIOAuth ? accountId : undefined), {
|
|
132
|
+
byKey.set(keyOf(result.provider, baseUrl, isOpenAIOAuth ? accountId : undefined, source), {
|
|
132
133
|
provider: result.provider,
|
|
133
|
-
models: [...result.models].slice(0, MAX_MODELS_PER_PROVIDER),
|
|
134
|
+
models: [...result.models].slice(0, result.provider === "openai" ? undefined : MAX_MODELS_PER_PROVIDER),
|
|
134
135
|
source,
|
|
135
136
|
...(baseUrl ? { baseUrl } : {}),
|
|
136
137
|
...(isOpenAIOAuth ? { accountId } : {}),
|
|
@@ -141,11 +142,16 @@ export function mergeCacheEntries(
|
|
|
141
142
|
|
|
142
143
|
/** Persist discovery results (best-effort; never throws, never blocks a turn). */
|
|
143
144
|
export async function writeModelCache(
|
|
144
|
-
results: readonly { provider: ProviderName; models: readonly string[]; ok: boolean; source?: ModelCacheSource; baseUrl?: string; accountId?: string }[],
|
|
145
|
+
results: readonly { provider: ProviderName; models: readonly string[]; ok: boolean; source?: ModelCacheSource; baseUrl?: string; accountId?: string; fallback?: boolean }[],
|
|
145
146
|
): Promise<void> {
|
|
146
147
|
// Same hermeticity rule as saveGlobalConfig: a test run may only write into an
|
|
147
148
|
// explicitly sandboxed JEO_CONFIG_DIR, never the developer's real ~/.jeo.
|
|
148
149
|
if (process.env.NODE_ENV === "test" && !jeoEnv("CONFIG_DIR")) return;
|
|
150
|
+
// A failed/fallback-only refresh must not make an old snapshot appear fresh.
|
|
151
|
+
const hasFreshResult = results.some(result => result.ok && !result.fallback
|
|
152
|
+
&& (result.models.length > 0 || result.provider === "openai")
|
|
153
|
+
&& !(result.provider === "openai" && result.source === "oauth" && !result.accountId?.trim()));
|
|
154
|
+
if (!hasFreshResult) return;
|
|
149
155
|
try {
|
|
150
156
|
const existing = await readModelCache();
|
|
151
157
|
const providers = mergeCacheEntries(existing?.providers ?? [], results);
|
|
@@ -173,9 +179,10 @@ export function applyCachedModels(cache: ModelCacheFile | null, openAIOauthAccou
|
|
|
173
179
|
recordLiveProviderModels(entry.provider, entry.models, {
|
|
174
180
|
source: entry.source,
|
|
175
181
|
baseUrl: entry.baseUrl,
|
|
182
|
+
replace: entry.provider === "openai",
|
|
176
183
|
...(isOpenAIOAuth && entry.accountId ? { accountId: entry.accountId } : {}),
|
|
177
184
|
});
|
|
178
|
-
if (isOpenAIOAuth && entry.accountId) {
|
|
185
|
+
if (isOpenAIOAuth && entry.accountId && !entry.baseUrl) {
|
|
179
186
|
recordLiveCodexModels(entry.models, entry.accountId);
|
|
180
187
|
}
|
|
181
188
|
applied += entry.models.length;
|
package/src/ai/model-catalog.ts
CHANGED
|
@@ -179,9 +179,8 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
|
|
|
179
179
|
|
|
180
180
|
/**
|
|
181
181
|
* OpenAI models the ChatGPT/Codex subscription backend (`codex/responses`) actually
|
|
182
|
-
* serves
|
|
183
|
-
*
|
|
184
|
-
* of the full chat-completions catalog. Verified live against a ChatGPT account.
|
|
182
|
+
* serves in the bundled offline snapshot. A confirmed account-specific model list
|
|
183
|
+
* replaces this snapshot for OAuth eligibility, including a successful empty list.
|
|
185
184
|
*/
|
|
186
185
|
export const CODEX_MODELS: readonly string[] = ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"];
|
|
187
186
|
|
|
@@ -236,7 +235,7 @@ export function recordLiveCodexModels(ids: readonly string[], accountId?: string
|
|
|
236
235
|
const explicitScope = normalizeOpenAIOauthScope(accountId);
|
|
237
236
|
if (explicitScope) setOpenAIOauthAccountScope(explicitScope);
|
|
238
237
|
const scope = explicitScope || EPHEMERAL_OPENAI_OAUTH_SCOPE;
|
|
239
|
-
const bucket =
|
|
238
|
+
const bucket = new Set<string>();
|
|
240
239
|
for (const id of ids) {
|
|
241
240
|
const trimmed = id.trim();
|
|
242
241
|
if (trimmed) bucket.add(trimmed);
|
|
@@ -244,10 +243,11 @@ export function recordLiveCodexModels(ids: readonly string[], accountId?: string
|
|
|
244
243
|
liveCodexModels.set(scope, bucket);
|
|
245
244
|
}
|
|
246
245
|
|
|
247
|
-
/**
|
|
246
|
+
/** A confirmed account catalog is authoritative; use the snapshot before discovery. */
|
|
248
247
|
export function isCodexModel(model: string): boolean {
|
|
249
248
|
const wire = model.startsWith("openai/") ? model.slice(7) : model;
|
|
250
|
-
|
|
249
|
+
const confirmed = liveCodexModels.get(activeOpenAIOauthScope);
|
|
250
|
+
return confirmed ? confirmed.has(wire) : CODEX_MODELS.includes(wire);
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
/** Test-only reset for live Codex observations and the active account scope. */
|
|
@@ -330,9 +330,10 @@ function liveModelRecordKey(
|
|
|
330
330
|
canonical: string,
|
|
331
331
|
baseUrl: string | undefined,
|
|
332
332
|
accountId?: string,
|
|
333
|
+
source?: LiveProviderModelSource,
|
|
333
334
|
): string {
|
|
334
335
|
const normalizedAccountId = provider === "openai" ? normalizeOpenAIOauthScope(accountId) : "";
|
|
335
|
-
return `${provider}\u0000${normalizeBaseUrl(baseUrl) ?? ""}\u0000${normalizedAccountId}\u0000${canonical}`;
|
|
336
|
+
return `${provider}\u0000${normalizeBaseUrl(baseUrl) ?? ""}\u0000${normalizedAccountId}\u0000${provider === "openai" ? source ?? "none" : ""}\u0000${canonical}`;
|
|
336
337
|
}
|
|
337
338
|
|
|
338
339
|
function liveModelFallbackRow(provider: ProviderName, canonical: string, providerModel: string): CatalogModel {
|
|
@@ -351,12 +352,12 @@ function liveModelFallbackRow(provider: ProviderName, canonical: string, provide
|
|
|
351
352
|
};
|
|
352
353
|
}
|
|
353
354
|
|
|
354
|
-
/** Record live ids
|
|
355
|
-
*
|
|
355
|
+
/** Record live ids. Complete OpenAI refreshes may replace their matching scope;
|
|
356
|
+
* partial observations and other providers remain additive. */
|
|
356
357
|
export function recordLiveProviderModels(
|
|
357
358
|
provider: ProviderName,
|
|
358
359
|
ids: readonly string[],
|
|
359
|
-
opts: { source?: LiveProviderModelSource; baseUrl?: string; accountId?: string } = {},
|
|
360
|
+
opts: { source?: LiveProviderModelSource; baseUrl?: string; accountId?: string; replace?: boolean } = {},
|
|
360
361
|
): void {
|
|
361
362
|
const source = opts.source ?? "none";
|
|
362
363
|
const baseUrl = normalizeBaseUrl(opts.baseUrl);
|
|
@@ -366,6 +367,14 @@ export function recordLiveProviderModels(
|
|
|
366
367
|
if (provider === "openai" && source === "oauth" && scopedAccountId) {
|
|
367
368
|
setOpenAIOauthAccountScope(scopedAccountId);
|
|
368
369
|
}
|
|
370
|
+
if (provider === "openai" && opts.replace) {
|
|
371
|
+
for (const [key, record] of liveProviderModels) {
|
|
372
|
+
if (record.row.provider === provider && record.source === source && record.baseUrl === baseUrl
|
|
373
|
+
&& oauthScope(record.accountId) === oauthScope(scopedAccountId)) {
|
|
374
|
+
removeLiveProviderRecord(key, record);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
369
378
|
for (const id of ids) {
|
|
370
379
|
const canonical = liveCanonicalId(provider, id);
|
|
371
380
|
if (!canonical) continue;
|
|
@@ -374,7 +383,7 @@ export function recordLiveProviderModels(
|
|
|
374
383
|
const row = known
|
|
375
384
|
? { ...known, canonical, provider, providerModel }
|
|
376
385
|
: liveModelFallbackRow(provider, canonical, providerModel);
|
|
377
|
-
const recordKey = liveModelRecordKey(provider, canonical, baseUrl, scopedAccountId);
|
|
386
|
+
const recordKey = liveModelRecordKey(provider, canonical, baseUrl, scopedAccountId, source);
|
|
378
387
|
liveProviderModels.set(recordKey, {
|
|
379
388
|
row,
|
|
380
389
|
source,
|
|
@@ -13,7 +13,6 @@ import type { ProviderName } from "./types";
|
|
|
13
13
|
import { PROVIDER_NAMES } from "./provider-status";
|
|
14
14
|
import {
|
|
15
15
|
catalogByProvider,
|
|
16
|
-
CODEX_MODELS,
|
|
17
16
|
KIMI_CODE_MODELS,
|
|
18
17
|
recordLiveCodexModels,
|
|
19
18
|
recordLiveProviderModels,
|
|
@@ -32,6 +31,8 @@ export interface ProviderModelsResult {
|
|
|
32
31
|
source: "oauth" | "api_key" | "keyless" | "none";
|
|
33
32
|
/** Stable OpenAI OAuth account scope for account-specific cache rows. */
|
|
34
33
|
accountId?: string;
|
|
34
|
+
/** Actual custom endpoint queried, for endpoint-scoped persistent caches. */
|
|
35
|
+
baseUrl?: string;
|
|
35
36
|
/** Present on failure: a short, human-readable reason. */
|
|
36
37
|
error?: string;
|
|
37
38
|
/** True when the live endpoint was unusable and ids came from the static catalog. */
|
|
@@ -45,7 +46,7 @@ export interface DiscoveryOptions {
|
|
|
45
46
|
/** Per-request timeout; default 5000ms. */
|
|
46
47
|
timeoutMs?: number;
|
|
47
48
|
signal?: AbortSignal;
|
|
48
|
-
/** Cap
|
|
49
|
+
/** Cap returned ids; OpenAI defaults to the full list, other providers to 100. */
|
|
49
50
|
limit?: number;
|
|
50
51
|
/** Config snapshot used for provider base URLs. */
|
|
51
52
|
config?: Config;
|
|
@@ -54,8 +55,8 @@ export interface DiscoveryOptions {
|
|
|
54
55
|
* the just-stored OAuth credential even when an unrelated `providers.openai`
|
|
55
56
|
* API key is also configured, so the report reflects the account that was
|
|
56
57
|
* JUST logged in rather than silently swapping to the API key. Does not
|
|
57
|
-
* change
|
|
58
|
-
*
|
|
58
|
+
* change single-provider discovery's API-key preference. `discoverModels`
|
|
59
|
+
* queries both OpenAI catalogs when both credentials are configured.
|
|
59
60
|
*/
|
|
60
61
|
preferOAuth?: boolean;
|
|
61
62
|
}
|
|
@@ -107,6 +108,7 @@ export function discoveryRequest(
|
|
|
107
108
|
provider: ProviderName,
|
|
108
109
|
cred: Credential | undefined,
|
|
109
110
|
baseUrl?: string,
|
|
111
|
+
oauthAccountId?: string,
|
|
110
112
|
): { url: string; headers: Record<string, string>; method?: "GET" | "POST"; body?: string } {
|
|
111
113
|
// Catalog-driven compat providers: OpenAI `${base}/models` (Bearer) or Anthropic
|
|
112
114
|
// `${base}/v1/models` (x-api-key). Both return { data: [{ id }] }.
|
|
@@ -125,7 +127,7 @@ export function discoveryRequest(
|
|
|
125
127
|
case "openai": {
|
|
126
128
|
const token = cred?.kind === "oauth" || cred?.kind === "api_key" ? cred.token : "";
|
|
127
129
|
if (cred?.kind === "oauth" && !baseUrl && !process.env.OPENAI_BASE_URL) {
|
|
128
|
-
const accountId = extractChatgptAccountId(token);
|
|
130
|
+
const accountId = extractChatgptAccountId(token) || oauthAccountId;
|
|
129
131
|
const headers: Record<string, string> = {
|
|
130
132
|
Authorization: `Bearer ${token}`,
|
|
131
133
|
"OpenAI-Beta": "responses=experimental",
|
|
@@ -180,11 +182,12 @@ export function discoveryRequest(
|
|
|
180
182
|
|
|
181
183
|
/**
|
|
182
184
|
* OpenAI `/v1/models` lists every model family — embeddings, audio/tts, image, moderation,
|
|
183
|
-
* realtime —
|
|
185
|
+
* realtime — jeo uses chat/completions and Responses. Drop the families that cannot serve a
|
|
184
186
|
* chat turn so pickers never offer a model that fails at call time.
|
|
185
187
|
*/
|
|
186
188
|
function isOpenAiChatModel(id: string): boolean {
|
|
187
|
-
return !/(^|[-/])(text-embedding|embedding|tts|whisper|dall-e|moderation|omni-moderation|davinci|babbage|computer-use|realtime|audio|image|sora|transcribe|instruct
|
|
189
|
+
return !/(^|[-/])(text-embedding|embedding|tts|whisper|dall-e|moderation|omni-moderation|davinci|babbage|computer-use|realtime|audio|image|sora|transcribe|instruct)([-/]|$)/i.test(id)
|
|
190
|
+
&& !/^codex([-/]|$)/i.test(id);
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
/**
|
|
@@ -195,11 +198,11 @@ function isGeminiChatModel(id: string): boolean {
|
|
|
195
198
|
return !/(^|[-/])(embedding|aqa|tts|image|imagen|veo|lyria|nano-banana|deep-research|computer-use|antigravity)([-/]|$)/i.test(id);
|
|
196
199
|
}
|
|
197
200
|
|
|
198
|
-
type CodexModelRow = { slug?: string; id?: string; supported_in_api?: boolean; priority?: number };
|
|
201
|
+
type CodexModelRow = { slug?: string; id?: string; visibility?: string; supported_in_api?: boolean; priority?: number };
|
|
199
202
|
type AntigravityModelRow = { slug?: string; id?: string; name?: string; isInternal?: boolean; model?: string };
|
|
200
203
|
|
|
201
204
|
/** Parse a provider's models response body into normalized, chat-capable model ids. */
|
|
202
|
-
export function parseModelsBody(provider: ProviderName, body: unknown): string[] {
|
|
205
|
+
export function parseModelsBody(provider: ProviderName, body: unknown, opts: { openaiCompatible?: boolean } = {}): string[] {
|
|
203
206
|
const data = body as {
|
|
204
207
|
data?: { id?: string }[];
|
|
205
208
|
models?: ({ name?: string; supportedGenerationMethods?: string[] } & CodexModelRow)[];
|
|
@@ -285,14 +288,16 @@ export function parseModelsBody(provider: ProviderName, body: unknown): string[]
|
|
|
285
288
|
}
|
|
286
289
|
if (provider === "openai" && data.models?.some(m => m.slug || m.id)) {
|
|
287
290
|
return data.models
|
|
288
|
-
|
|
291
|
+
// supported_in_api describes API-key availability, not ChatGPT entitlement.
|
|
292
|
+
// Codex's ChatGPT picker uses visibility, including subscription-only rows.
|
|
293
|
+
.filter(m => m.visibility === undefined || m.visibility === "list")
|
|
289
294
|
.map(m => m.slug ?? m.id ?? "")
|
|
290
295
|
// Review-only entries (e.g. codex-auto-review) are not chat-turn models.
|
|
291
296
|
.filter(id => id && !/(^|[-/])auto-review([-/]|$)/i.test(id));
|
|
292
297
|
}
|
|
293
298
|
// anthropic / openai: { data: [{ id }] }
|
|
294
299
|
const ids = (data.data ?? []).map(m => m.id ?? "").filter(Boolean);
|
|
295
|
-
return provider === "openai" ? ids.filter(isOpenAiChatModel) : ids;
|
|
300
|
+
return provider === "openai" && !opts.openaiCompatible ? ids.filter(isOpenAiChatModel) : ids;
|
|
296
301
|
}
|
|
297
302
|
|
|
298
303
|
/**
|
|
@@ -305,23 +310,23 @@ export function parseModelsBody(provider: ProviderName, body: unknown): string[]
|
|
|
305
310
|
*/
|
|
306
311
|
export function catalogOr(result: ProviderModelsResult): ProviderModelsResult {
|
|
307
312
|
if (result.ok && result.models.length > 0) return result;
|
|
313
|
+
// OpenAI has live list endpoints for both auth modes. Neither a failed request
|
|
314
|
+
// nor an authoritative empty list establishes access to the static snapshot.
|
|
315
|
+
if (result.provider === "openai") return result;
|
|
308
316
|
// Antigravity's available models depend on the Cloud Code Assist agent backend
|
|
309
317
|
// and must never be faked from a hard-coded catalog.
|
|
310
318
|
if (result.provider === "antigravity") return result;
|
|
311
319
|
// No catalog-fallback for oauth-sourced gemini failures: OAuth can LIST but
|
|
312
320
|
// not SERVE gemini models (see listProviderModels' gemini gate).
|
|
313
321
|
if (result.provider === "gemini" && result.source === "oauth") return result;
|
|
314
|
-
// OAuth
|
|
315
|
-
//
|
|
316
|
-
// static catalog too. API-key providers only fall back when the provider has NO
|
|
322
|
+
// Other OAuth providers fall back to their static catalog. API-key providers
|
|
323
|
+
// only fall back when the provider has NO
|
|
317
324
|
// models-list endpoint at all (HTTP 404 / not found) — never for an auth
|
|
318
325
|
// rejection, so a wrong key stays a failure instead of fabricating catalog rows.
|
|
319
326
|
const eligible = result.source === "oauth"
|
|
320
327
|
|| (result.source === "api_key" && isMissingModelsEndpoint(result.error));
|
|
321
328
|
if (!eligible) return result;
|
|
322
|
-
const ids = result.provider === "
|
|
323
|
-
? [...CODEX_MODELS]
|
|
324
|
-
: result.provider === "kimi" && result.source === "oauth"
|
|
329
|
+
const ids = result.provider === "kimi" && result.source === "oauth"
|
|
325
330
|
// Kimi OAuth (Kimi Code subscription) serves ONLY the Kimi Code catalog —
|
|
326
331
|
// moonshot API-platform ids would 404 against api.kimi.com/coding.
|
|
327
332
|
? KIMI_CODE_MODELS.map(id => `kimi/${id}`)
|
|
@@ -345,7 +350,8 @@ export async function listProviderModels(
|
|
|
345
350
|
opts: DiscoveryOptions = {},
|
|
346
351
|
): Promise<ProviderModelsResult> {
|
|
347
352
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
348
|
-
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
353
|
+
const limit = opts.limit ?? (provider === "openai" ? Number.POSITIVE_INFINITY : DEFAULT_LIMIT);
|
|
354
|
+
let baseUrl = opts.baseUrl;
|
|
349
355
|
|
|
350
356
|
let cred: Credential | undefined;
|
|
351
357
|
let oauthAccountId: string | undefined;
|
|
@@ -363,6 +369,7 @@ export async function listProviderModels(
|
|
|
363
369
|
cred = raw;
|
|
364
370
|
source = cred.kind === "oauth" ? "oauth" : cred.kind === "api_key" ? "api_key" : "none";
|
|
365
371
|
const config = opts.config ?? (await readGlobalConfig());
|
|
372
|
+
if (provider === "openai") baseUrl = (baseUrl ?? config.openaiBaseUrl ?? process.env.OPENAI_BASE_URL)?.replace(/\/+$/, "");
|
|
366
373
|
|
|
367
374
|
if (provider === "antigravity") {
|
|
368
375
|
// Antigravity lists models from the LIVE Cloud Code Assist endpoint
|
|
@@ -391,6 +398,12 @@ export async function listProviderModels(
|
|
|
391
398
|
cred = { kind: "api_key", provider: prov, token: config.providers[prov]! };
|
|
392
399
|
source = "api_key";
|
|
393
400
|
}
|
|
401
|
+
if (provider === "openai" && baseUrl && cred.kind === "oauth") {
|
|
402
|
+
// Subscription credentials belong to ChatGPT, not a configured proxy.
|
|
403
|
+
const key = config.providers?.openai;
|
|
404
|
+
cred = key ? { kind: "api_key", provider: "openai", token: key } : { kind: "none", provider: "openai" };
|
|
405
|
+
source = key ? "api_key" : "keyless";
|
|
406
|
+
}
|
|
394
407
|
if (provider === "openai" && source === "oauth" && cred.kind === "oauth") {
|
|
395
408
|
const stored = await getStoredOAuth("openai").catch(() => undefined);
|
|
396
409
|
const jwtAccountId = extractChatgptAccountId(cred.token);
|
|
@@ -406,23 +419,32 @@ export async function listProviderModels(
|
|
|
406
419
|
if (provider === "gemini" && cred.kind === "oauth") {
|
|
407
420
|
return { provider, models: [], ok: false, source, error: "OAuth cannot serve gemini models — set GEMINI_API_KEY, or use antigravity/* (Cloud Code Assist)" };
|
|
408
421
|
}
|
|
409
|
-
const isLocalOpenAi = provider === "openai" && !!
|
|
422
|
+
const isLocalOpenAi = provider === "openai" && !!baseUrl;
|
|
410
423
|
if (source === "none" && !isLocalOpenAi) {
|
|
411
424
|
return { provider, models: [], ok: false, source, error: "not logged in" };
|
|
412
425
|
}
|
|
413
426
|
}
|
|
414
427
|
|
|
415
|
-
const { url, headers, method, body: requestBody } = discoveryRequest(provider, cred,
|
|
428
|
+
const { url, headers, method, body: requestBody } = discoveryRequest(provider, cred, baseUrl, oauthAccountId);
|
|
429
|
+
const scope = { ...(oauthAccountId ? { accountId: oauthAccountId } : {}), ...(baseUrl ? { baseUrl } : {}) };
|
|
416
430
|
const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
417
431
|
const signal = opts.signal ?? AbortSignal.timeout(timeout);
|
|
418
432
|
try {
|
|
419
433
|
const res = await fetchImpl(url, { method: method ?? "GET", headers, body: requestBody, signal });
|
|
420
434
|
if (!res.ok) {
|
|
421
435
|
const reason = res.status === 401 || res.status === 403 ? "auth rejected" : `HTTP ${res.status}`;
|
|
422
|
-
return { provider, models: [], ok: false, source, error: reason };
|
|
436
|
+
return { provider, models: [], ok: false, source, error: reason, ...scope };
|
|
423
437
|
}
|
|
424
438
|
const body = await res.json();
|
|
425
|
-
|
|
439
|
+
if (provider === "openai") {
|
|
440
|
+
const payload = body as { models?: unknown; data?: unknown } | null;
|
|
441
|
+
const rows = Array.isArray(payload?.models) ? payload.models : payload?.data;
|
|
442
|
+
if (!Array.isArray(rows) || rows.some(row => !row || typeof row !== "object"
|
|
443
|
+
|| ![row.slug, row.id].some(id => typeof id === "string" && id.trim().length > 0))) {
|
|
444
|
+
return { provider, models: [], ok: false, source, error: "invalid models response", ...scope };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
let ids = parseModelsBody(provider, body, { openaiCompatible: provider === "openai" && !!baseUrl });
|
|
426
448
|
// Gemini paginates: follow nextPageToken (bounded) so the available list is
|
|
427
449
|
// COMPLETE — page 1 alone silently dropped the newest models (round-15).
|
|
428
450
|
if (provider === "gemini") {
|
|
@@ -436,26 +458,28 @@ export async function listProviderModels(
|
|
|
436
458
|
pageToken = pageBody.nextPageToken;
|
|
437
459
|
}
|
|
438
460
|
}
|
|
439
|
-
const
|
|
461
|
+
const allModels = [...new Set(ids)].sort();
|
|
462
|
+
const models = allModels.slice(0, limit);
|
|
440
463
|
// Record live ids for routing supplements. For OpenAI OAuth, keep the
|
|
441
464
|
// existing Codex allow-list widening separate: API-key/custom-base discovery
|
|
442
465
|
// must not make OAuth Codex calls accept unrelated API models.
|
|
443
|
-
recordLiveProviderModels(provider,
|
|
466
|
+
recordLiveProviderModels(provider, allModels, {
|
|
444
467
|
source,
|
|
445
|
-
baseUrl
|
|
468
|
+
baseUrl,
|
|
469
|
+
replace: provider === "openai",
|
|
446
470
|
...(provider === "openai" && source === "oauth" && oauthAccountId ? { accountId: oauthAccountId } : {}),
|
|
447
471
|
});
|
|
448
|
-
if (provider === "openai" && source === "oauth") recordLiveCodexModels(
|
|
472
|
+
if (provider === "openai" && source === "oauth" && !baseUrl) recordLiveCodexModels(allModels, oauthAccountId);
|
|
449
473
|
return {
|
|
450
474
|
provider,
|
|
451
475
|
models,
|
|
452
476
|
ok: true,
|
|
453
477
|
source,
|
|
454
|
-
...
|
|
478
|
+
...scope,
|
|
455
479
|
};
|
|
456
480
|
} catch (err) {
|
|
457
481
|
const msg = (err as Error)?.name === "TimeoutError" || (err as Error)?.name === "AbortError" ? "timeout" : "unreachable";
|
|
458
|
-
return { provider, models: [], ok: false, source, error: msg };
|
|
482
|
+
return { provider, models: [], ok: false, source, error: msg, ...scope };
|
|
459
483
|
}
|
|
460
484
|
}
|
|
461
485
|
|
|
@@ -495,13 +519,24 @@ export async function discoverModels(
|
|
|
495
519
|
const providers = opts.providers ?? [...PROVIDER_NAMES];
|
|
496
520
|
const useFallback = opts.catalogFallback !== false;
|
|
497
521
|
const results = await Promise.all(
|
|
498
|
-
providers.map(p =>
|
|
499
|
-
|
|
522
|
+
providers.map(async p => {
|
|
523
|
+
const discoveryOpts = {
|
|
500
524
|
...opts,
|
|
501
525
|
config: cfg,
|
|
502
526
|
baseUrl: p === "ollama" ? (cfg.ollamaBaseUrl ?? opts.baseUrl) : p === "lmstudio" ? (cfg.lmstudioBaseUrl ?? opts.baseUrl) : p === "openai" ? (cfg.openaiBaseUrl ?? opts.baseUrl) : opts.baseUrl,
|
|
503
|
-
}
|
|
504
|
-
|
|
527
|
+
};
|
|
528
|
+
const primary = await listProviderModels(p, discoveryOpts);
|
|
529
|
+
// Both credentials can coexist. API discovery alone omits subscription-only
|
|
530
|
+
// models and never teaches the Codex call gate what this account can serve.
|
|
531
|
+
if (p === "openai" && primary.source === "api_key" && !discoveryOpts.baseUrl && !process.env.OPENAI_BASE_URL) {
|
|
532
|
+
const credential = await resolveCredential("openai");
|
|
533
|
+
if (credential.kind === "oauth") {
|
|
534
|
+
return [primary, await listProviderModels(p, { ...discoveryOpts, preferOAuth: true })];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return [primary];
|
|
538
|
+
}),
|
|
505
539
|
);
|
|
506
|
-
|
|
540
|
+
const flat = results.flat();
|
|
541
|
+
return useFallback ? flat.map(catalogOr) : flat;
|
|
507
542
|
}
|
package/src/ai/model-picker.ts
CHANGED
|
@@ -16,9 +16,13 @@ export interface PickEntry {
|
|
|
16
16
|
/** Flatten successful discovery results into an ordered, 1-based pick list. */
|
|
17
17
|
export function flattenModels(results: ProviderModelsResult[]): PickEntry[] {
|
|
18
18
|
const out: PickEntry[] = [];
|
|
19
|
+
const seen = new Set<string>();
|
|
19
20
|
for (const r of results) {
|
|
20
21
|
if (!r.ok) continue;
|
|
21
22
|
for (const model of r.models) {
|
|
23
|
+
const key = `${r.provider}\u0000${model}`;
|
|
24
|
+
if (seen.has(key)) continue;
|
|
25
|
+
seen.add(key);
|
|
22
26
|
out.push({ index: out.length + 1, provider: r.provider, model });
|
|
23
27
|
}
|
|
24
28
|
}
|
package/src/commands/launch.ts
CHANGED
|
@@ -55,7 +55,7 @@ import { callLlm, type Message } from "../agent/loop";
|
|
|
55
55
|
import { friendlyProviderError } from "../util/provider-error";
|
|
56
56
|
import { readGlobalConfig, saveConfigPatch, resolveWikiRoot } from "../agent/state";
|
|
57
57
|
import { rememberModelPatch, recentModelsForDisplay } from "../agent/model-recency";
|
|
58
|
-
import { describeModel, describeAllProviders, describeProvider, resolveProvider, thinkingMaxTokens, resolveMaxOutputTokens, thinkingToReasoningEffort, discoverModels, flattenModels, resolveSelection, catalogMetadata, catalogByProvider, resolveRoleModel, CODEX_MODELS, qualifyModelId, modelServableWithConfig, isLocalProviderReachable } from "../ai";
|
|
58
|
+
import { describeModel, describeAllProviders, describeProvider, resolveProvider, thinkingMaxTokens, resolveMaxOutputTokens, thinkingToReasoningEffort, discoverModels, flattenModels, resolveSelection, catalogMetadata, catalogByProvider, liveProviderCatalogModels, resolveRoleModel, CODEX_MODELS, qualifyModelId, modelServableWithConfig, isLocalProviderReachable } from "../ai";
|
|
59
59
|
import { rehydrateLiveModels, writeModelCache } from "../ai/model-cache";
|
|
60
60
|
import type { ProviderModelsResult, PickEntry, ProviderName, ModelRole, ThinkLevel } from "../ai";
|
|
61
61
|
import { readGoalState, writeGoalState, clearGoalState, verifyGoal, applyEvidenceGate } from "../agent/goal-verifier";
|
|
@@ -335,8 +335,8 @@ function providerDefaultModel(p: ProviderName): string {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
/**
|
|
338
|
-
* Pick-list entries for ONE provider
|
|
339
|
-
*
|
|
338
|
+
* Pick-list entries for ONE provider. OpenAI availability comes only from live
|
|
339
|
+
* results; other providers retain their offline catalog fallbacks.
|
|
340
340
|
*
|
|
341
341
|
* Live discovery yields ids only for a logged-in, reachable provider, and
|
|
342
342
|
* `catalogOr` backfills the static catalog for OAuth sources and for API-key
|
|
@@ -348,6 +348,9 @@ function providerDefaultModel(p: ProviderName): string {
|
|
|
348
348
|
export function providerPickEntries(live: ProviderModelsResult[], want: ProviderName): PickEntry[] {
|
|
349
349
|
const fromLive = flattenModels(live.filter(r => r.provider === want));
|
|
350
350
|
if (fromLive.length) return fromLive;
|
|
351
|
+
// An OpenAI empty/error response is authoritative for this picker. Offering
|
|
352
|
+
// the static capability catalog here would fabricate account availability.
|
|
353
|
+
if (want === "openai") return [];
|
|
351
354
|
const catalog = catalogByProvider(want);
|
|
352
355
|
if (catalog.length) {
|
|
353
356
|
return catalog.map((m, i) => ({ index: i + 1, provider: want, model: qualifyModelId(m.providerModel, want) }));
|
|
@@ -2427,7 +2430,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2427
2430
|
// the OAuth Codex gate from the first keystroke, instead of only after the network
|
|
2428
2431
|
// discovery below happens to land.
|
|
2429
2432
|
const aliasNames = Object.keys(await listAliases());
|
|
2430
|
-
|
|
2433
|
+
await rehydrateLiveModels().catch(() => null);
|
|
2434
|
+
const completionConfig = await readGlobalConfig();
|
|
2435
|
+
const cachedCompletionModels = liveProviderCatalogModels({ openaiBaseUrl: completionConfig.openaiBaseUrl ?? process.env.OPENAI_BASE_URL });
|
|
2431
2436
|
void getLiveModels()
|
|
2432
2437
|
.then(r => {
|
|
2433
2438
|
liveModelsCache ??= r;
|
|
@@ -2442,14 +2447,16 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2442
2447
|
// access to were un-completable for the first seconds of every session.
|
|
2443
2448
|
const liveFromDiscovery = liveModelsCache ? flattenModels(liveModelsCache).map(e => e.model) : null;
|
|
2444
2449
|
const cachedFor = (p: string): string[] =>
|
|
2445
|
-
|
|
2450
|
+
cachedCompletionModels.filter(entry => entry.provider === p).map(entry => entry.canonical);
|
|
2446
2451
|
return {
|
|
2447
2452
|
...base,
|
|
2448
2453
|
slashCommands: [...base.slashCommands, ...skillSlashDetails.map(d => d.command)],
|
|
2449
|
-
liveModels: liveFromDiscovery ??
|
|
2454
|
+
liveModels: liveFromDiscovery ?? cachedCompletionModels.map(entry => entry.canonical),
|
|
2450
2455
|
aliases: aliasNames,
|
|
2451
2456
|
skillNames: resolvedSkillTokens,
|
|
2452
|
-
modelsForProvider: p => liveModelsCache
|
|
2457
|
+
modelsForProvider: p => liveModelsCache
|
|
2458
|
+
? flattenModels(liveModelsCache.filter(r => r.provider === p)).map(entry => entry.model)
|
|
2459
|
+
: cachedFor(p),
|
|
2453
2460
|
mentionPaths,
|
|
2454
2461
|
};
|
|
2455
2462
|
};
|