jeo-code 0.11.1 → 0.11.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/CHANGELOG.md +46 -1
- package/README.ja.md +60 -28
- package/README.ko.md +63 -30
- package/README.md +59 -27
- package/README.zh.md +62 -30
- package/package.json +1 -1
- package/src/agent/config-schema.ts +20 -7
- package/src/agent/debug-session.ts +7 -3
- package/src/agent/engine.ts +46 -102
- package/src/agent/loop-guards.ts +11 -13
- package/src/agent/notify/daemon-control.ts +109 -24
- package/src/agent/notify/discord-api.ts +148 -0
- package/src/agent/notify/discord-daemon.ts +443 -0
- package/src/agent/notify/paths.ts +1 -1
- package/src/agent/notify/session-discovery.ts +16 -0
- package/src/agent/notify/session-endpoint.ts +43 -25
- package/src/agent/notify/slack-api.ts +177 -0
- package/src/agent/notify/slack-daemon.ts +361 -0
- package/src/agent/notify/telegram-daemon.ts +215 -83
- package/src/agent/state.ts +13 -1
- package/src/ai/model-cache.ts +22 -15
- package/src/ai/model-catalog.ts +20 -11
- package/src/ai/model-discovery.ts +81 -54
- package/src/ai/model-picker.ts +4 -0
- package/src/autopilot.ts +14 -5
- package/src/cli/runner.ts +4 -4
- package/src/commands/approve.ts +21 -16
- package/src/commands/daemon.ts +5 -3
- package/src/commands/launch/agents-slash.ts +8 -3
- package/src/commands/launch.ts +45 -46
- package/src/commands/notify.ts +215 -83
- package/src/commands/ralplan.ts +2 -1
- package/src/commands/team.ts +17 -12
- package/src/commands/ultragoal.ts +2 -1
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
|
}
|
|
@@ -69,13 +70,6 @@ const DEFAULT_LIMIT = 100;
|
|
|
69
70
|
const CODEX_CLIENT_VERSION = "2.0.0";
|
|
70
71
|
export const CODEX_MODELS_URL = `https://chatgpt.com/backend-api/codex/models?client_version=${CODEX_CLIENT_VERSION}`;
|
|
71
72
|
const ANTIGRAVITY_MODELS_URL = "https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels";
|
|
72
|
-
const ANTIGRAVITY_MODEL_DENYLIST = new Set([
|
|
73
|
-
"chat_20706",
|
|
74
|
-
"chat_23310",
|
|
75
|
-
"gemini-2.5-flash-thinking",
|
|
76
|
-
"gemini-3-pro-low",
|
|
77
|
-
"gemini-2.5-pro",
|
|
78
|
-
]);
|
|
79
73
|
|
|
80
74
|
function anthropicHeaders(cred: Credential): Record<string, string> {
|
|
81
75
|
if (cred.kind === "oauth") {
|
|
@@ -107,6 +101,7 @@ export function discoveryRequest(
|
|
|
107
101
|
provider: ProviderName,
|
|
108
102
|
cred: Credential | undefined,
|
|
109
103
|
baseUrl?: string,
|
|
104
|
+
oauthAccountId?: string,
|
|
110
105
|
): { url: string; headers: Record<string, string>; method?: "GET" | "POST"; body?: string } {
|
|
111
106
|
// Catalog-driven compat providers: OpenAI `${base}/models` (Bearer) or Anthropic
|
|
112
107
|
// `${base}/v1/models` (x-api-key). Both return { data: [{ id }] }.
|
|
@@ -125,7 +120,7 @@ export function discoveryRequest(
|
|
|
125
120
|
case "openai": {
|
|
126
121
|
const token = cred?.kind === "oauth" || cred?.kind === "api_key" ? cred.token : "";
|
|
127
122
|
if (cred?.kind === "oauth" && !baseUrl && !process.env.OPENAI_BASE_URL) {
|
|
128
|
-
const accountId = extractChatgptAccountId(token);
|
|
123
|
+
const accountId = extractChatgptAccountId(token) || oauthAccountId;
|
|
129
124
|
const headers: Record<string, string> = {
|
|
130
125
|
Authorization: `Bearer ${token}`,
|
|
131
126
|
"OpenAI-Beta": "responses=experimental",
|
|
@@ -180,11 +175,12 @@ export function discoveryRequest(
|
|
|
180
175
|
|
|
181
176
|
/**
|
|
182
177
|
* OpenAI `/v1/models` lists every model family — embeddings, audio/tts, image, moderation,
|
|
183
|
-
* realtime —
|
|
178
|
+
* realtime — jeo uses chat/completions and Responses. Drop the families that cannot serve a
|
|
184
179
|
* chat turn so pickers never offer a model that fails at call time.
|
|
185
180
|
*/
|
|
186
181
|
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
|
|
182
|
+
return !/(^|[-/])(text-embedding|embedding|tts|whisper|dall-e|moderation|omni-moderation|davinci|babbage|computer-use|realtime|audio|image|sora|transcribe|instruct)([-/]|$)/i.test(id)
|
|
183
|
+
&& !/^codex([-/]|$)/i.test(id);
|
|
188
184
|
}
|
|
189
185
|
|
|
190
186
|
/**
|
|
@@ -195,11 +191,11 @@ function isGeminiChatModel(id: string): boolean {
|
|
|
195
191
|
return !/(^|[-/])(embedding|aqa|tts|image|imagen|veo|lyria|nano-banana|deep-research|computer-use|antigravity)([-/]|$)/i.test(id);
|
|
196
192
|
}
|
|
197
193
|
|
|
198
|
-
type CodexModelRow = { slug?: string; id?: string; supported_in_api?: boolean; priority?: number };
|
|
194
|
+
type CodexModelRow = { slug?: string; id?: string; visibility?: string; supported_in_api?: boolean; priority?: number };
|
|
199
195
|
type AntigravityModelRow = { slug?: string; id?: string; name?: string; isInternal?: boolean; model?: string };
|
|
200
196
|
|
|
201
197
|
/** Parse a provider's models response body into normalized, chat-capable model ids. */
|
|
202
|
-
export function parseModelsBody(provider: ProviderName, body: unknown): string[] {
|
|
198
|
+
export function parseModelsBody(provider: ProviderName, body: unknown, opts: { openaiCompatible?: boolean } = {}): string[] {
|
|
203
199
|
const data = body as {
|
|
204
200
|
data?: { id?: string }[];
|
|
205
201
|
models?: ({ name?: string; supportedGenerationMethods?: string[] } & CodexModelRow)[];
|
|
@@ -244,10 +240,11 @@ export function parseModelsBody(provider: ProviderName, body: unknown): string[]
|
|
|
244
240
|
/** Array of ids OR an object keyed by deprecated id. */
|
|
245
241
|
deprecatedModelIds?: string[] | Record<string, unknown>;
|
|
246
242
|
};
|
|
247
|
-
const
|
|
243
|
+
const normalizeId = (id: string): string => id.replace(/^models\//, "").replace(/^antigravity\//, "");
|
|
244
|
+
const roleIds = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === "string").map(normalizeId) : []);
|
|
248
245
|
const deprecated = Array.isArray(payload.deprecatedModelIds)
|
|
249
246
|
? roleIds(payload.deprecatedModelIds)
|
|
250
|
-
: Object.keys(payload.deprecatedModelIds ?? {});
|
|
247
|
+
: Object.keys(payload.deprecatedModelIds ?? {}).map(normalizeId);
|
|
251
248
|
const agentIds = new Set(
|
|
252
249
|
(Array.isArray(payload.agentModelSorts) ? payload.agentModelSorts : [])
|
|
253
250
|
.flatMap(sort => (Array.isArray(sort?.groups) ? sort.groups : []))
|
|
@@ -262,18 +259,16 @@ export function parseModelsBody(provider: ProviderName, body: unknown): string[]
|
|
|
262
259
|
...deprecated,
|
|
263
260
|
]);
|
|
264
261
|
const rawModels = payload.models;
|
|
265
|
-
const
|
|
266
|
-
? rawModels.map(
|
|
267
|
-
: Object.entries(rawModels ?? {})
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
.map(id => id
|
|
275
|
-
.filter(Boolean)
|
|
276
|
-
.map(id => id.startsWith("antigravity/") ? id : `antigravity/${id}`);
|
|
262
|
+
const entries = Array.isArray(rawModels)
|
|
263
|
+
? rawModels.map(model => [model?.slug ?? model?.id ?? model?.name ?? "", model] as const)
|
|
264
|
+
: Object.entries(rawModels ?? {});
|
|
265
|
+
return entries
|
|
266
|
+
.map(([id, model]) => [typeof id === "string" ? normalizeId(id) : "", model] as const)
|
|
267
|
+
.filter(([id, model]) =>
|
|
268
|
+
id.length > 0 &&
|
|
269
|
+
model?.isInternal !== true &&
|
|
270
|
+
(agentIds.size > 0 ? agentIds.has(id) : !nonChat.has(id)))
|
|
271
|
+
.map(([id]) => `antigravity/${id}`);
|
|
277
272
|
}
|
|
278
273
|
if (provider === "gemini") {
|
|
279
274
|
// Keep only models the generateContent endpoint can serve (skip embeddings/tts/aqa/etc).
|
|
@@ -285,14 +280,16 @@ export function parseModelsBody(provider: ProviderName, body: unknown): string[]
|
|
|
285
280
|
}
|
|
286
281
|
if (provider === "openai" && data.models?.some(m => m.slug || m.id)) {
|
|
287
282
|
return data.models
|
|
288
|
-
|
|
283
|
+
// supported_in_api describes API-key availability, not ChatGPT entitlement.
|
|
284
|
+
// Codex's ChatGPT picker uses visibility, including subscription-only rows.
|
|
285
|
+
.filter(m => m.visibility === undefined || m.visibility === "list")
|
|
289
286
|
.map(m => m.slug ?? m.id ?? "")
|
|
290
287
|
// Review-only entries (e.g. codex-auto-review) are not chat-turn models.
|
|
291
288
|
.filter(id => id && !/(^|[-/])auto-review([-/]|$)/i.test(id));
|
|
292
289
|
}
|
|
293
290
|
// anthropic / openai: { data: [{ id }] }
|
|
294
291
|
const ids = (data.data ?? []).map(m => m.id ?? "").filter(Boolean);
|
|
295
|
-
return provider === "openai" ? ids.filter(isOpenAiChatModel) : ids;
|
|
292
|
+
return provider === "openai" && !opts.openaiCompatible ? ids.filter(isOpenAiChatModel) : ids;
|
|
296
293
|
}
|
|
297
294
|
|
|
298
295
|
/**
|
|
@@ -305,23 +302,23 @@ export function parseModelsBody(provider: ProviderName, body: unknown): string[]
|
|
|
305
302
|
*/
|
|
306
303
|
export function catalogOr(result: ProviderModelsResult): ProviderModelsResult {
|
|
307
304
|
if (result.ok && result.models.length > 0) return result;
|
|
305
|
+
// OpenAI has live list endpoints for both auth modes. Neither a failed request
|
|
306
|
+
// nor an authoritative empty list establishes access to the static snapshot.
|
|
307
|
+
if (result.provider === "openai") return result;
|
|
308
308
|
// Antigravity's available models depend on the Cloud Code Assist agent backend
|
|
309
309
|
// and must never be faked from a hard-coded catalog.
|
|
310
310
|
if (result.provider === "antigravity") return result;
|
|
311
311
|
// No catalog-fallback for oauth-sourced gemini failures: OAuth can LIST but
|
|
312
312
|
// not SERVE gemini models (see listProviderModels' gemini gate).
|
|
313
313
|
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
|
|
314
|
+
// Other OAuth providers fall back to their static catalog. API-key providers
|
|
315
|
+
// only fall back when the provider has NO
|
|
317
316
|
// models-list endpoint at all (HTTP 404 / not found) — never for an auth
|
|
318
317
|
// rejection, so a wrong key stays a failure instead of fabricating catalog rows.
|
|
319
318
|
const eligible = result.source === "oauth"
|
|
320
319
|
|| (result.source === "api_key" && isMissingModelsEndpoint(result.error));
|
|
321
320
|
if (!eligible) return result;
|
|
322
|
-
const ids = result.provider === "
|
|
323
|
-
? [...CODEX_MODELS]
|
|
324
|
-
: result.provider === "kimi" && result.source === "oauth"
|
|
321
|
+
const ids = result.provider === "kimi" && result.source === "oauth"
|
|
325
322
|
// Kimi OAuth (Kimi Code subscription) serves ONLY the Kimi Code catalog —
|
|
326
323
|
// moonshot API-platform ids would 404 against api.kimi.com/coding.
|
|
327
324
|
? KIMI_CODE_MODELS.map(id => `kimi/${id}`)
|
|
@@ -345,7 +342,8 @@ export async function listProviderModels(
|
|
|
345
342
|
opts: DiscoveryOptions = {},
|
|
346
343
|
): Promise<ProviderModelsResult> {
|
|
347
344
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
348
|
-
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
345
|
+
const limit = opts.limit ?? (provider === "openai" ? Number.POSITIVE_INFINITY : DEFAULT_LIMIT);
|
|
346
|
+
let baseUrl = opts.baseUrl;
|
|
349
347
|
|
|
350
348
|
let cred: Credential | undefined;
|
|
351
349
|
let oauthAccountId: string | undefined;
|
|
@@ -363,6 +361,7 @@ export async function listProviderModels(
|
|
|
363
361
|
cred = raw;
|
|
364
362
|
source = cred.kind === "oauth" ? "oauth" : cred.kind === "api_key" ? "api_key" : "none";
|
|
365
363
|
const config = opts.config ?? (await readGlobalConfig());
|
|
364
|
+
if (provider === "openai") baseUrl = (baseUrl ?? config.openaiBaseUrl ?? process.env.OPENAI_BASE_URL)?.replace(/\/+$/, "");
|
|
366
365
|
|
|
367
366
|
if (provider === "antigravity") {
|
|
368
367
|
// Antigravity lists models from the LIVE Cloud Code Assist endpoint
|
|
@@ -391,6 +390,12 @@ export async function listProviderModels(
|
|
|
391
390
|
cred = { kind: "api_key", provider: prov, token: config.providers[prov]! };
|
|
392
391
|
source = "api_key";
|
|
393
392
|
}
|
|
393
|
+
if (provider === "openai" && baseUrl && cred.kind === "oauth") {
|
|
394
|
+
// Subscription credentials belong to ChatGPT, not a configured proxy.
|
|
395
|
+
const key = config.providers?.openai;
|
|
396
|
+
cred = key ? { kind: "api_key", provider: "openai", token: key } : { kind: "none", provider: "openai" };
|
|
397
|
+
source = key ? "api_key" : "keyless";
|
|
398
|
+
}
|
|
394
399
|
if (provider === "openai" && source === "oauth" && cred.kind === "oauth") {
|
|
395
400
|
const stored = await getStoredOAuth("openai").catch(() => undefined);
|
|
396
401
|
const jwtAccountId = extractChatgptAccountId(cred.token);
|
|
@@ -406,23 +411,32 @@ export async function listProviderModels(
|
|
|
406
411
|
if (provider === "gemini" && cred.kind === "oauth") {
|
|
407
412
|
return { provider, models: [], ok: false, source, error: "OAuth cannot serve gemini models — set GEMINI_API_KEY, or use antigravity/* (Cloud Code Assist)" };
|
|
408
413
|
}
|
|
409
|
-
const isLocalOpenAi = provider === "openai" && !!
|
|
414
|
+
const isLocalOpenAi = provider === "openai" && !!baseUrl;
|
|
410
415
|
if (source === "none" && !isLocalOpenAi) {
|
|
411
416
|
return { provider, models: [], ok: false, source, error: "not logged in" };
|
|
412
417
|
}
|
|
413
418
|
}
|
|
414
419
|
|
|
415
|
-
const { url, headers, method, body: requestBody } = discoveryRequest(provider, cred,
|
|
420
|
+
const { url, headers, method, body: requestBody } = discoveryRequest(provider, cred, baseUrl, oauthAccountId);
|
|
421
|
+
const scope = { ...(oauthAccountId ? { accountId: oauthAccountId } : {}), ...(baseUrl ? { baseUrl } : {}) };
|
|
416
422
|
const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
417
423
|
const signal = opts.signal ?? AbortSignal.timeout(timeout);
|
|
418
424
|
try {
|
|
419
425
|
const res = await fetchImpl(url, { method: method ?? "GET", headers, body: requestBody, signal });
|
|
420
426
|
if (!res.ok) {
|
|
421
427
|
const reason = res.status === 401 || res.status === 403 ? "auth rejected" : `HTTP ${res.status}`;
|
|
422
|
-
return { provider, models: [], ok: false, source, error: reason };
|
|
428
|
+
return { provider, models: [], ok: false, source, error: reason, ...scope };
|
|
423
429
|
}
|
|
424
430
|
const body = await res.json();
|
|
425
|
-
|
|
431
|
+
if (provider === "openai") {
|
|
432
|
+
const payload = body as { models?: unknown; data?: unknown } | null;
|
|
433
|
+
const rows = Array.isArray(payload?.models) ? payload.models : payload?.data;
|
|
434
|
+
if (!Array.isArray(rows) || rows.some(row => !row || typeof row !== "object"
|
|
435
|
+
|| ![row.slug, row.id].some(id => typeof id === "string" && id.trim().length > 0))) {
|
|
436
|
+
return { provider, models: [], ok: false, source, error: "invalid models response", ...scope };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
let ids = parseModelsBody(provider, body, { openaiCompatible: provider === "openai" && !!baseUrl });
|
|
426
440
|
// Gemini paginates: follow nextPageToken (bounded) so the available list is
|
|
427
441
|
// COMPLETE — page 1 alone silently dropped the newest models (round-15).
|
|
428
442
|
if (provider === "gemini") {
|
|
@@ -436,26 +450,28 @@ export async function listProviderModels(
|
|
|
436
450
|
pageToken = pageBody.nextPageToken;
|
|
437
451
|
}
|
|
438
452
|
}
|
|
439
|
-
const
|
|
453
|
+
const allModels = [...new Set(ids)].sort();
|
|
454
|
+
const models = allModels.slice(0, limit);
|
|
440
455
|
// Record live ids for routing supplements. For OpenAI OAuth, keep the
|
|
441
456
|
// existing Codex allow-list widening separate: API-key/custom-base discovery
|
|
442
457
|
// must not make OAuth Codex calls accept unrelated API models.
|
|
443
|
-
recordLiveProviderModels(provider,
|
|
458
|
+
recordLiveProviderModels(provider, allModels, {
|
|
444
459
|
source,
|
|
445
|
-
baseUrl
|
|
460
|
+
baseUrl,
|
|
461
|
+
replace: provider === "openai",
|
|
446
462
|
...(provider === "openai" && source === "oauth" && oauthAccountId ? { accountId: oauthAccountId } : {}),
|
|
447
463
|
});
|
|
448
|
-
if (provider === "openai" && source === "oauth") recordLiveCodexModels(
|
|
464
|
+
if (provider === "openai" && source === "oauth" && !baseUrl) recordLiveCodexModels(allModels, oauthAccountId);
|
|
449
465
|
return {
|
|
450
466
|
provider,
|
|
451
467
|
models,
|
|
452
468
|
ok: true,
|
|
453
469
|
source,
|
|
454
|
-
...
|
|
470
|
+
...scope,
|
|
455
471
|
};
|
|
456
472
|
} catch (err) {
|
|
457
473
|
const msg = (err as Error)?.name === "TimeoutError" || (err as Error)?.name === "AbortError" ? "timeout" : "unreachable";
|
|
458
|
-
return { provider, models: [], ok: false, source, error: msg };
|
|
474
|
+
return { provider, models: [], ok: false, source, error: msg, ...scope };
|
|
459
475
|
}
|
|
460
476
|
}
|
|
461
477
|
|
|
@@ -495,13 +511,24 @@ export async function discoverModels(
|
|
|
495
511
|
const providers = opts.providers ?? [...PROVIDER_NAMES];
|
|
496
512
|
const useFallback = opts.catalogFallback !== false;
|
|
497
513
|
const results = await Promise.all(
|
|
498
|
-
providers.map(p =>
|
|
499
|
-
|
|
514
|
+
providers.map(async p => {
|
|
515
|
+
const discoveryOpts = {
|
|
500
516
|
...opts,
|
|
501
517
|
config: cfg,
|
|
502
518
|
baseUrl: p === "ollama" ? (cfg.ollamaBaseUrl ?? opts.baseUrl) : p === "lmstudio" ? (cfg.lmstudioBaseUrl ?? opts.baseUrl) : p === "openai" ? (cfg.openaiBaseUrl ?? opts.baseUrl) : opts.baseUrl,
|
|
503
|
-
}
|
|
504
|
-
|
|
519
|
+
};
|
|
520
|
+
const primary = await listProviderModels(p, discoveryOpts);
|
|
521
|
+
// Both credentials can coexist. API discovery alone omits subscription-only
|
|
522
|
+
// models and never teaches the Codex call gate what this account can serve.
|
|
523
|
+
if (p === "openai" && primary.source === "api_key" && !discoveryOpts.baseUrl && !process.env.OPENAI_BASE_URL) {
|
|
524
|
+
const credential = await resolveCredential("openai");
|
|
525
|
+
if (credential.kind === "oauth") {
|
|
526
|
+
return [primary, await listProviderModels(p, { ...discoveryOpts, preferOAuth: true })];
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return [primary];
|
|
530
|
+
}),
|
|
505
531
|
);
|
|
506
|
-
|
|
532
|
+
const flat = results.flat();
|
|
533
|
+
return useFallback ? flat.map(catalogOr) : flat;
|
|
507
534
|
}
|
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/autopilot.ts
CHANGED
|
@@ -180,6 +180,7 @@ export function foldBest(goal: Goal, best: number | undefined, score: number): n
|
|
|
180
180
|
/**
|
|
181
181
|
* Single source of truth for the ratchet keep/revert decision. Shared by step,
|
|
182
182
|
* loop, and status so they can never diverge.
|
|
183
|
+
* - every goal: a failed eval always reverts, regardless of its printed score.
|
|
183
184
|
* - gate goal: keep iff the eval passed (score is irrelevant).
|
|
184
185
|
* - min/max goal: a non-measurable (NaN) score can never prove improvement, so
|
|
185
186
|
* it is always reverted; otherwise keep iff it improves on the best so far.
|
|
@@ -190,7 +191,8 @@ export function decideStep(
|
|
|
190
191
|
passed: boolean,
|
|
191
192
|
best: number | undefined,
|
|
192
193
|
): "keep" | "revert" {
|
|
193
|
-
if (
|
|
194
|
+
if (!passed) return "revert";
|
|
195
|
+
if (goal === "gate") return "keep";
|
|
194
196
|
if (Number.isNaN(score)) return "revert";
|
|
195
197
|
return isImprovement(goal, score, best) ? "keep" : "revert";
|
|
196
198
|
}
|
|
@@ -255,8 +257,11 @@ function cmdStep(flags: Record<string, string>): void {
|
|
|
255
257
|
if (decision === "revert" && flags["on-revert"]) {
|
|
256
258
|
try {
|
|
257
259
|
execSync(flags["on-revert"], { stdio: "inherit", shell: getShell() });
|
|
258
|
-
} catch {
|
|
259
|
-
|
|
260
|
+
} catch (error) {
|
|
261
|
+
appendLog({ type: "stop", reason: "rollback_failed", change, score, passed, prevBest: best ?? null, output, error: String(error) });
|
|
262
|
+
console.error(`jeo autopilot: stop — --on-revert hook failed: ${String(error)}`);
|
|
263
|
+
process.exitCode = 1;
|
|
264
|
+
return;
|
|
260
265
|
}
|
|
261
266
|
}
|
|
262
267
|
|
|
@@ -295,6 +300,7 @@ function cmdLoop(flags: Record<string, string>): void {
|
|
|
295
300
|
if (!runnerOk) {
|
|
296
301
|
appendLog({ type: "stop", reason: "runner_failed", iteration: i });
|
|
297
302
|
console.log(`jeo autopilot: stop — runner failed at iteration ${i}`);
|
|
303
|
+
process.exitCode = 1;
|
|
298
304
|
return;
|
|
299
305
|
}
|
|
300
306
|
|
|
@@ -304,8 +310,11 @@ function cmdLoop(flags: Record<string, string>): void {
|
|
|
304
310
|
if (decision === "revert" && flags["on-revert"]) {
|
|
305
311
|
try {
|
|
306
312
|
execSync(flags["on-revert"], { stdio: "inherit", shell: getShell() });
|
|
307
|
-
} catch {
|
|
308
|
-
|
|
313
|
+
} catch (error) {
|
|
314
|
+
appendLog({ type: "stop", reason: "rollback_failed", iteration: i, change: `loop#${i}`, score, passed, prevBest: best ?? null, output, error: String(error) });
|
|
315
|
+
console.error(`jeo autopilot: stop — --on-revert hook failed at iteration ${i}: ${String(error)}`);
|
|
316
|
+
process.exitCode = 1;
|
|
317
|
+
return;
|
|
309
318
|
}
|
|
310
319
|
}
|
|
311
320
|
appendLog({ type: "step", iteration: i, change: `loop#${i}`, score, passed, decision, prevBest: best ?? null, output });
|
package/src/cli/runner.ts
CHANGED
|
@@ -112,8 +112,8 @@ export const COMMANDS: readonly CommandSpec[] = [
|
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
114
|
name: "notify",
|
|
115
|
-
summary: "Configure/inspect
|
|
116
|
-
usage: "notify [setup [--token
|
|
115
|
+
summary: "Configure/inspect Telegram, Discord, and Slack notification channels.",
|
|
116
|
+
usage: "notify [setup|status|health|test] [--provider telegram|discord|slack] [--token-env NAME] [--app-token-env NAME] [--chat-id ID|--channel-id ID] [--allowed-user-ids IDS]",
|
|
117
117
|
loader: async () => {
|
|
118
118
|
const m = await import("../commands/notify");
|
|
119
119
|
return args => m.runNotifyCommand(args);
|
|
@@ -121,7 +121,7 @@ export const COMMANDS: readonly CommandSpec[] = [
|
|
|
121
121
|
},
|
|
122
122
|
{
|
|
123
123
|
name: "daemon",
|
|
124
|
-
summary: "Manage the
|
|
124
|
+
summary: "Manage the shared Telegram/Discord/Slack notification daemon.",
|
|
125
125
|
usage: "daemon [status|start|stop|reload]",
|
|
126
126
|
loader: async () => {
|
|
127
127
|
const m = await import("../commands/daemon");
|
|
@@ -197,7 +197,7 @@ export const COMMANDS: readonly CommandSpec[] = [
|
|
|
197
197
|
},
|
|
198
198
|
{
|
|
199
199
|
name: "notify-daemon-run",
|
|
200
|
-
summary: "(internal) Foreground
|
|
200
|
+
summary: "(internal) Foreground notification worker, spawned by 'jeo daemon start'.",
|
|
201
201
|
usage: "notify-daemon-run",
|
|
202
202
|
loader: async () => {
|
|
203
203
|
const m = await import("../agent/notify/telegram-daemon");
|