jeo-code 0.12.5 → 0.12.6
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 +10 -1
- 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 +16 -8
- package/src/ai/model-catalog.ts +7 -2
- package/src/ai/model-discovery.ts +13 -5
- package/src/ai/model-manager.ts +48 -2
- package/src/util/provider-error.ts +34 -3
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,14 @@ The README mirrors the latest 5 entries — regenerate with `bun run changelog:s
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.12.6] - 2026-10-17
|
|
13
|
+
_Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls._
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **OpenAI subscription startup behind a configured proxy** — account-confirmed Codex models keep their OAuth transport instead of sending a keyless request to the OpenAI-compatible endpoint. Cold calls confirm the subscription catalog first; explicit per-call URLs, configured proxy keys, and endpoint-discovered models retain their existing routes. Subscription requests use the direct Codex backend, not the configured proxy.
|
|
17
|
+
- **Provider-specific authentication recovery** — HTTP 401/403 errors name the appropriate login or API-key configuration, omit unrelated Antigravity advice for OpenAI, preserve exact status codes, and no longer treat a structured non-authentication failure as a credential rejection merely because its body mentions 401.
|
|
18
|
+
- **Account-bound subscription catalogs** — distinguish opaque bearer identities without exposing tokens, reject discovery results that finish after an account switch, share concurrent same-account catalog requests, and keep cancellation of one caller from interrupting other callers.
|
|
19
|
+
|
|
12
20
|
## [0.12.5] - 2026-10-17
|
|
13
21
|
_Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing._
|
|
14
22
|
|
|
@@ -2579,7 +2587,8 @@ _Initial release._
|
|
|
2579
2587
|
### Added
|
|
2580
2588
|
- Initial jeo-code agent and CLI.
|
|
2581
2589
|
|
|
2582
|
-
[Unreleased]: https://github.com/akillness/jeo-code/compare/v0.
|
|
2590
|
+
[Unreleased]: https://github.com/akillness/jeo-code/compare/v0.12.6...HEAD
|
|
2591
|
+
[0.12.6]: https://github.com/akillness/jeo-code/releases/tag/v0.12.6
|
|
2583
2592
|
[0.11.4]: https://github.com/akillness/jeo-code/releases/tag/v0.11.4
|
|
2584
2593
|
[0.11.3]: https://github.com/akillness/jeo-code/releases/tag/v0.11.3
|
|
2585
2594
|
[0.11.2]: https://github.com/akillness/jeo-code/releases/tag/v0.11.2
|
package/README.ja.md
CHANGED
|
@@ -416,10 +416,10 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
416
416
|
|
|
417
417
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
418
418
|
- **[Unreleased]**
|
|
419
|
+
- **[0.12.6]** (2026-10-17) — Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls.
|
|
419
420
|
- **[0.12.5]** (2026-10-17) — Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing.
|
|
420
421
|
- **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
|
|
421
422
|
- **[0.12.3]** (2026-10-17) — Anthropic static model catalog trimmed to claude-5.x with live API discovery, and `/route off` strictly fixes the active model with no failover.
|
|
422
|
-
- **[0.12.2]** (2026-10-16) — Select Argon through the official agy full agent, with its own live catalog and session, separately from jeo's Cloud Code Assist backend.
|
|
423
423
|
|
|
424
424
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
425
425
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -417,10 +417,10 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
417
417
|
|
|
418
418
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
419
419
|
- **[Unreleased]**
|
|
420
|
+
- **[0.12.6]** (2026-10-17) — Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls.
|
|
420
421
|
- **[0.12.5]** (2026-10-17) — Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing.
|
|
421
422
|
- **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
|
|
422
423
|
- **[0.12.3]** (2026-10-17) — Anthropic static model catalog trimmed to claude-5.x with live API discovery, and `/route off` strictly fixes the active model with no failover.
|
|
423
|
-
- **[0.12.2]** (2026-10-16) — Select Argon through the official agy full agent, with its own live catalog and session, separately from jeo's Cloud Code Assist backend.
|
|
424
424
|
|
|
425
425
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
426
426
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -409,10 +409,10 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
409
409
|
|
|
410
410
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
411
411
|
- **[Unreleased]**
|
|
412
|
+
- **[0.12.6]** (2026-10-17) — Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls.
|
|
412
413
|
- **[0.12.5]** (2026-10-17) — Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing.
|
|
413
414
|
- **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
|
|
414
415
|
- **[0.12.3]** (2026-10-17) — Anthropic static model catalog trimmed to claude-5.x with live API discovery, and `/route off` strictly fixes the active model with no failover.
|
|
415
|
-
- **[0.12.2]** (2026-10-16) — Select Argon through the official agy full agent, with its own live catalog and session, separately from jeo's Cloud Code Assist backend.
|
|
416
416
|
|
|
417
417
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
418
418
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -414,10 +414,10 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
414
414
|
|
|
415
415
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
416
416
|
- **[Unreleased]**
|
|
417
|
+
- **[0.12.6]** (2026-10-17) — Preserve OpenAI subscription authentication behind configured proxies, isolate account catalogs, and give provider-specific recovery instructions while retaining Antigravity OAuth calls.
|
|
417
418
|
- **[0.12.5]** (2026-10-17) — Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing.
|
|
418
419
|
- **[0.12.4]** (2026-10-17) — Refresh Antigravity provider (`antigravity/*`) account catalogs with `antigravity/hub/2.23.1` so entitled Gemini 4 Argon and Gemini 3.8 Flash tiers appear directly in `/model provider antigravity`.
|
|
419
420
|
- **[0.12.3]** (2026-10-17) — Anthropic static model catalog trimmed to claude-5.x with live API discovery, and `/route off` strictly fixes the active model with no failover.
|
|
420
|
-
- **[0.12.2]** (2026-10-16) — Select Argon through the official agy full agent, with its own live catalog and session, separately from jeo's Cloud Code Assist backend.
|
|
421
421
|
|
|
422
422
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
423
423
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/ai/model-cache.ts
CHANGED
|
@@ -60,6 +60,21 @@ function cachePath(): string {
|
|
|
60
60
|
return path.join(cacheDir(), "model-catalog-cache.json");
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/** Account-bound catalog identity; opaque manual bearers must not share a scope. */
|
|
64
|
+
export async function openAIOauthAccountScope(credential?: Credential): Promise<string | undefined> {
|
|
65
|
+
let selected = credential;
|
|
66
|
+
if (!selected) {
|
|
67
|
+
const snapshot = await snapshotProvider("openai");
|
|
68
|
+
if (!snapshot.oauth) return undefined;
|
|
69
|
+
selected = { kind: "oauth", provider: "openai", token: snapshot.oauth };
|
|
70
|
+
}
|
|
71
|
+
if (selected.kind !== "oauth" || selected.provider !== "openai") return undefined;
|
|
72
|
+
const stored = await getStoredOAuth("openai");
|
|
73
|
+
const accountId = extractChatgptAccountId(selected.token)
|
|
74
|
+
|| (stored?.access === selected.token ? stored.accountId?.trim() : undefined);
|
|
75
|
+
return accountId || `token:${createHash("sha256").update(selected.token).digest("hex")}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
/** Peek at the selected OAuth account without importing or refreshing credentials. */
|
|
64
79
|
export async function antigravityAccountScope(credential?: Credential): Promise<string | undefined> {
|
|
65
80
|
try {
|
|
@@ -251,14 +266,7 @@ export function applyCachedModels(cache: ModelCacheFile | null, openAIOauthAccou
|
|
|
251
266
|
/** Read + apply in one step. Returns the cache so callers can decide about refreshing. */
|
|
252
267
|
export async function rehydrateLiveModels(): Promise<ModelCacheFile | null> {
|
|
253
268
|
const cache = await readModelCache();
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
const stored = await getStoredOAuth("openai");
|
|
257
|
-
const jwtAccountId = stored ? extractChatgptAccountId(stored.access) : undefined;
|
|
258
|
-
accountId = jwtAccountId || stored?.accountId;
|
|
259
|
-
} catch {
|
|
260
|
-
accountId = undefined;
|
|
261
|
-
}
|
|
269
|
+
const accountId = await openAIOauthAccountScope();
|
|
262
270
|
const antigravityAccountId = await antigravityAccountScope();
|
|
263
271
|
setOpenAIOauthAccountScope(accountId);
|
|
264
272
|
setAntigravityAccountScope(antigravityAccountId);
|
package/src/ai/model-catalog.ts
CHANGED
|
@@ -255,12 +255,17 @@ export function recordLiveCodexModels(ids: readonly string[], accountId?: string
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
/** A confirmed account catalog is authoritative; use the snapshot before discovery. */
|
|
258
|
-
export function isCodexModel(model: string): boolean {
|
|
258
|
+
export function isCodexModel(model: string, accountId = activeOpenAIOauthScope): boolean {
|
|
259
259
|
const wire = model.startsWith("openai/") ? model.slice(7) : model;
|
|
260
|
-
const confirmed = liveCodexModels.get(
|
|
260
|
+
const confirmed = liveCodexModels.get(oauthScope(accountId));
|
|
261
261
|
return confirmed ? confirmed.has(wire) : CODEX_MODELS.includes(wire);
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
/** Whether discovery (including a restored account cache) established a Codex catalog. */
|
|
265
|
+
export function hasConfirmedCodexCatalog(accountId = activeOpenAIOauthScope): boolean {
|
|
266
|
+
return liveCodexModels.has(oauthScope(accountId));
|
|
267
|
+
}
|
|
268
|
+
|
|
264
269
|
/** Test-only reset for live Codex observations and the active account scope. */
|
|
265
270
|
export function resetLiveCodexModels(): void {
|
|
266
271
|
liveCodexModels.clear();
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import { extractChatgptAccountId } from "./providers/openai-responses";
|
|
25
25
|
import { getStoredOAuth } from "../auth/storage";
|
|
26
26
|
import { openaiCompatDef } from "./providers/openai-compatible-catalog";
|
|
27
|
-
import { antigravityAccountScope } from "./model-cache";
|
|
27
|
+
import { antigravityAccountScope, openAIOauthAccountScope } from "./model-cache";
|
|
28
28
|
import { getAntigravityUserAgent, resolveAntigravityProjectId } from "./providers/antigravity";
|
|
29
29
|
import { discoverGoogleProjectId, ANTIGRAVITY_DISCOVERY_METADATA } from "../auth/flows/google-project";
|
|
30
30
|
import { getKimiCommonHeaders } from "../auth/flows/kimi";
|
|
@@ -412,6 +412,7 @@ export async function listProviderModels(
|
|
|
412
412
|
|
|
413
413
|
let cred: Credential | undefined;
|
|
414
414
|
let oauthAccountId: string | undefined;
|
|
415
|
+
let oauthCatalogScope: string | undefined;
|
|
415
416
|
let source: ProviderModelsResult["source"] = "keyless";
|
|
416
417
|
if (provider === "xai") {
|
|
417
418
|
// xAI (Grok) is API-key only and not an OAuth AuthProvider: resolve its key
|
|
@@ -466,8 +467,9 @@ export async function listProviderModels(
|
|
|
466
467
|
if (provider === "openai" && source === "oauth" && cred.kind === "oauth") {
|
|
467
468
|
const stored = await getStoredOAuth("openai").catch(() => undefined);
|
|
468
469
|
const jwtAccountId = extractChatgptAccountId(cred.token);
|
|
469
|
-
oauthAccountId = jwtAccountId || stored?.accountId;
|
|
470
|
-
|
|
470
|
+
oauthAccountId = jwtAccountId || (stored?.access === cred.token ? stored.accountId : undefined);
|
|
471
|
+
oauthCatalogScope = await openAIOauthAccountScope(cred);
|
|
472
|
+
setOpenAIOauthAccountScope(oauthCatalogScope);
|
|
471
473
|
}
|
|
472
474
|
// Gemini models are exposed ONLY under API-key auth: a gemini OAuth token can
|
|
473
475
|
// still list models from generativelanguage.googleapis.com, but it can no
|
|
@@ -484,7 +486,7 @@ export async function listProviderModels(
|
|
|
484
486
|
}
|
|
485
487
|
}
|
|
486
488
|
|
|
487
|
-
const scope = { ...(
|
|
489
|
+
const scope = { ...(oauthCatalogScope ? { accountId: oauthCatalogScope } : {}), ...(baseUrl ? { baseUrl } : {}) };
|
|
488
490
|
const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
489
491
|
let signal = opts.signal ? AbortSignal.any([opts.signal, AbortSignal.timeout(timeout)]) : AbortSignal.timeout(timeout);
|
|
490
492
|
let initialAntigravityScope: string | undefined;
|
|
@@ -578,6 +580,12 @@ export async function listProviderModels(
|
|
|
578
580
|
}
|
|
579
581
|
const allModels = [...new Set(ids)].sort();
|
|
580
582
|
const models = allModels.slice(0, limit);
|
|
583
|
+
if (provider === "openai" && source === "oauth" && !baseUrl) {
|
|
584
|
+
if (await openAIOauthAccountScope() !== oauthCatalogScope) {
|
|
585
|
+
return { provider, models: [], ok: false, source, error: "account changed during model discovery", ...scope };
|
|
586
|
+
}
|
|
587
|
+
setOpenAIOauthAccountScope(oauthCatalogScope);
|
|
588
|
+
}
|
|
581
589
|
if (provider === "antigravity") {
|
|
582
590
|
const currentScope = await antigravityAccountScope();
|
|
583
591
|
if (currentScope !== scope.accountId && currentScope !== initialAntigravityScope) {
|
|
@@ -594,7 +602,7 @@ export async function listProviderModels(
|
|
|
594
602
|
replace: provider === "openai" || provider === "antigravity",
|
|
595
603
|
...(source === "oauth" && scope.accountId ? { accountId: scope.accountId } : {}),
|
|
596
604
|
});
|
|
597
|
-
if (provider === "openai" && source === "oauth" && !baseUrl) recordLiveCodexModels(allModels,
|
|
605
|
+
if (provider === "openai" && source === "oauth" && !baseUrl) recordLiveCodexModels(allModels, oauthCatalogScope);
|
|
598
606
|
if (provider === "kimi" && source === "oauth") recordLiveKimiCodeModels(allModels);
|
|
599
607
|
return {
|
|
600
608
|
provider,
|
package/src/ai/model-manager.ts
CHANGED
|
@@ -6,12 +6,15 @@ import "./register-providers"; // side-effect: registers built-in adapters into
|
|
|
6
6
|
import type { CallOptions, Message, ProviderAdapter, ProviderName } from "./types";
|
|
7
7
|
import { expandAlias, resolveModelId, effectiveAliasesFor, BUILTIN_ALIASES } from "./model-registry";
|
|
8
8
|
import { findCatalogEntry, type ModelCatalogEntry } from "./model-catalog-compat";
|
|
9
|
-
import { toProviderModel, CODEX_MODELS, KIMI_CODE_MODELS, findCatalogModel, isCodexModel, isKimiCodeModel, findLiveCatalogModel } from "./model-catalog";
|
|
9
|
+
import { toProviderModel, CODEX_MODELS, KIMI_CODE_MODELS, findCatalogModel, isCodexModel, isKimiCodeModel, findLiveCatalogModel, hasConfirmedCodexCatalog, isLiveProviderModel } from "./model-catalog";
|
|
10
10
|
import { xaiCredential } from "./providers/xai";
|
|
11
11
|
import { allCompatNames, isOpenAICompatProvider } from "./providers/openai-compatible-catalog";
|
|
12
12
|
import { withRetry, defaultRetryable, withConnectionContext, isConnectionError, ConnectionContextError, type RetryOptions } from "../util/retry";
|
|
13
13
|
import { jeoEnv } from "../util/env";
|
|
14
14
|
import type { Config } from "../agent/state";
|
|
15
|
+
import { listProviderModels } from "./model-discovery";
|
|
16
|
+
import type { ProviderModelsResult } from "./model-discovery";
|
|
17
|
+
import { openAIOauthAccountScope } from "./model-cache";
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
|
|
@@ -404,6 +407,8 @@ interface Resolved {
|
|
|
404
407
|
baseUrl: string | undefined;
|
|
405
408
|
}
|
|
406
409
|
|
|
410
|
+
const codexCatalogInFlight = new Map<string, Promise<ProviderModelsResult>>();
|
|
411
|
+
|
|
407
412
|
/**
|
|
408
413
|
* The credential to actually use for a provider call. A configured local OpenAI-compatible base
|
|
409
414
|
* URL must use the standard /chat/completions path, but the openai adapter dispatches on
|
|
@@ -436,7 +441,7 @@ export async function resolveCall(options: Partial<CallOptions>, kind: "request"
|
|
|
436
441
|
}
|
|
437
442
|
const adapter = providerRegistry.get(provider)!;
|
|
438
443
|
|
|
439
|
-
|
|
444
|
+
let baseUrl =
|
|
440
445
|
options.baseUrl ??
|
|
441
446
|
(provider === "openai" ? config.openaiBaseUrl : undefined) ??
|
|
442
447
|
(provider === "ollama" ? config.ollamaBaseUrl : undefined) ??
|
|
@@ -503,6 +508,47 @@ export async function resolveCall(options: Partial<CallOptions>, kind: "request"
|
|
|
503
508
|
const credentialProvider = provider as AuthProvider;
|
|
504
509
|
const credential = await resolveCredential(credentialProvider);
|
|
505
510
|
const effective = effectiveCredentialForProvider(credentialProvider, credential, config, model);
|
|
511
|
+
// A subscription catalog is independent of a configured OpenAI-compatible
|
|
512
|
+
// endpoint. Keep its OAuth transport rather than replacing it with "no-key".
|
|
513
|
+
// Explicit endpoint overrides, proxy keys and endpoint-discovered models win.
|
|
514
|
+
if (provider === "openai" && baseUrl && options.baseUrl === undefined
|
|
515
|
+
&& effective.kind === "oauth" && !config.providers.openai
|
|
516
|
+
&& !isLiveProviderModel("openai", model, { openaiBaseUrl: baseUrl })) {
|
|
517
|
+
options.signal?.throwIfAborted();
|
|
518
|
+
const accountScope = await openAIOauthAccountScope(effective);
|
|
519
|
+
if (accountScope && !hasConfirmedCodexCatalog(accountScope)) {
|
|
520
|
+
let discovery = codexCatalogInFlight.get(accountScope);
|
|
521
|
+
if (!discovery) {
|
|
522
|
+
// One caller's cancellation must not cancel another agent's catalog read.
|
|
523
|
+
discovery = listProviderModels("openai", { config, codexCatalog: true });
|
|
524
|
+
codexCatalogInFlight.set(accountScope, discovery);
|
|
525
|
+
void discovery.finally(() => codexCatalogInFlight.delete(accountScope)).catch(() => {});
|
|
526
|
+
}
|
|
527
|
+
const signal = options.signal;
|
|
528
|
+
if (signal) {
|
|
529
|
+
const aborted = Promise.withResolvers<never>();
|
|
530
|
+
const onAbort = () => aborted.reject(signal.reason);
|
|
531
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
532
|
+
try {
|
|
533
|
+
signal.throwIfAborted();
|
|
534
|
+
await Promise.race([discovery, aborted.promise]);
|
|
535
|
+
} finally {
|
|
536
|
+
signal.removeEventListener("abort", onAbort);
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
await discovery;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
options.signal?.throwIfAborted();
|
|
543
|
+
if (await openAIOauthAccountScope() !== accountScope) {
|
|
544
|
+
throw new Error("OpenAI credential changed during model discovery; retry the request.");
|
|
545
|
+
}
|
|
546
|
+
// Never infer cloud eligibility from the bundled snapshot when discovery fails.
|
|
547
|
+
if (accountScope && hasConfirmedCodexCatalog(accountScope) && isCodexModel(model, accountScope)) {
|
|
548
|
+
baseUrl = undefined;
|
|
549
|
+
callOptions.baseUrl = undefined;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
506
552
|
const isLocalOpenAi = provider === "openai" && !!baseUrl;
|
|
507
553
|
if (provider === "openai" && effective.kind === "oauth" && !isLocalOpenAi && !oauthServesModel("openai", model)) {
|
|
508
554
|
throw new Error(
|
|
@@ -9,6 +9,8 @@ import { companyLabel } from "../ai/model-catalog";
|
|
|
9
9
|
import { ProviderHttpError, ProviderStreamError } from "../ai/providers/errors";
|
|
10
10
|
import { AgyCliError } from "../agent/antigravity-cli";
|
|
11
11
|
import { sanitizeForTerminal } from "../tui/components/code-view";
|
|
12
|
+
import { OAUTH_PROVIDERS, API_KEY_ONLY_PROVIDERS } from "../auth/storage";
|
|
13
|
+
import { openaiCompatDef, allCompatNames } from "../ai/providers/openai-compatible-catalog";
|
|
12
14
|
// Re-export: engine.ts and callers import the refusal predicate from here; the
|
|
13
15
|
// definition lives in retry.ts so defaultRetryable can fail fast without a cycle.
|
|
14
16
|
export { isRefusalError } from "./retry";
|
|
@@ -72,6 +74,12 @@ function retryAfterMsOf(err: unknown): number | undefined {
|
|
|
72
74
|
return err instanceof ProviderHttpError ? err.retryAfterMs : undefined;
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
function authProviderForLabel(label: string): string | undefined {
|
|
78
|
+
const normalized = label.toLowerCase() === "openai codex" ? "openai" : label.toLowerCase();
|
|
79
|
+
const matches = (id: string) => id === normalized || companyLabel(id).toLowerCase() === normalized;
|
|
80
|
+
return OAUTH_PROVIDERS.find(matches) ?? API_KEY_ONLY_PROVIDERS.find(matches) ?? allCompatNames().find(matches);
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
export function friendlyProviderError(err: unknown): string {
|
|
76
84
|
if (err instanceof AgyCliError) return sanitizeForTerminal(err.message);
|
|
77
85
|
if (err instanceof ConnectionContextError) {
|
|
@@ -92,7 +100,10 @@ export function friendlyProviderError(err: unknown): string {
|
|
|
92
100
|
// Tencent/groq that speak Anthropic's/OpenAI's wire protocol — see
|
|
93
101
|
// `relabelProviderError`); only guess from message text for errors that never
|
|
94
102
|
// carried a `.provider` field (bare Error, a re-wrapped doneReason string, …).
|
|
95
|
-
const
|
|
103
|
+
const reportedProvider = knownErrorProvider(err) ?? /^(?:Error:\s*)?([^\r\n]+?) (?:request failed|stream failed|rejected the credential)\b/i.exec(msg)?.[1] ?? /^(?:Error:\s*)?(OpenAI(?: Codex)?|Antigravity|Anthropic|Gemini|Google)\b/i.exec(msg)?.[1];
|
|
104
|
+
// A raw prefix may contain a secret, not a provider name. Only registry labels
|
|
105
|
+
// may survive redaction or the round trip through an engine's doneReason.
|
|
106
|
+
const provider = reportedProvider && authProviderForLabel(reportedProvider) ? reportedProvider : (
|
|
96
107
|
/antigravity/i.test(msg)
|
|
97
108
|
? "Antigravity"
|
|
98
109
|
: /anthropic/i.test(msg)
|
|
@@ -117,8 +128,28 @@ export function friendlyProviderError(err: unknown): string {
|
|
|
117
128
|
: "";
|
|
118
129
|
return `Rate limited by ${provider} (HTTP 429).${retry} Auto-retry cannot clear this window right now — slow your request rate, wait for the reset, or switch model with /model (a local ollama model never rate-limits).`;
|
|
119
130
|
}
|
|
120
|
-
|
|
121
|
-
|
|
131
|
+
const credentialStatus = status ?? Number(/\b(401|403)\b/.exec(msg)?.[1]);
|
|
132
|
+
if (credentialStatus === 401 || credentialStatus === 403) {
|
|
133
|
+
const authProvider = authProviderForLabel(provider);
|
|
134
|
+
let hint = "Check the configured credentials with 'jeo setup' or /provider.";
|
|
135
|
+
if (authProvider === "openai") {
|
|
136
|
+
hint = "For ChatGPT/Codex, run 'jeo auth login openai' or /provider login openai. For API/custom endpoints, check OPENAI_API_KEY and OPENAI_BASE_URL (config: providers.openai / openaiBaseUrl).";
|
|
137
|
+
} else if (authProvider === "gemini") {
|
|
138
|
+
hint = "Check GEMINI_API_KEY. For Google OAuth, select an antigravity/* model and run 'jeo auth login antigravity' or /provider login antigravity.";
|
|
139
|
+
} else if (authProvider === "antigravity") {
|
|
140
|
+
hint = "Run 'jeo auth login antigravity' or /provider login antigravity; a Gemini login only works when Cloud Code Assist authorizes that token.";
|
|
141
|
+
} else if (authProvider === "kimi") {
|
|
142
|
+
hint = "For Kimi Code, run 'jeo auth login kimi'. For the Moonshot API, check KIMI_API_KEY or set it with /provider key kimi <key>.";
|
|
143
|
+
} else if (authProvider && !OAUTH_PROVIDERS.some(id => id === authProvider)) {
|
|
144
|
+
const apiKeyEnv = openaiCompatDef(authProvider)?.apiKeyEnv ?? `${authProvider.toUpperCase()}_API_KEY`;
|
|
145
|
+
const command = API_KEY_ONLY_PROVIDERS.some(id => id === authProvider)
|
|
146
|
+
? ` Set the key with /provider key ${authProvider} <key> or 'jeo auth login ${authProvider} --token <key>'.`
|
|
147
|
+
: " Update the custom provider's configured credentials.";
|
|
148
|
+
hint = `Check ${apiKeyEnv} or providers.${authProvider}.${command}`;
|
|
149
|
+
} else if (authProvider) {
|
|
150
|
+
hint = `Run 'jeo auth login ${authProvider}' or /provider login ${authProvider}; for API keys, check providers.${authProvider}.`;
|
|
151
|
+
}
|
|
152
|
+
return `${provider} rejected the credential (HTTP ${credentialStatus}). Run 'jeo auth status'. ${hint}`;
|
|
122
153
|
}
|
|
123
154
|
// A persistent 5xx (server-side fault on the provider's backend) — `ProviderHttpError`
|
|
124
155
|
// already embeds the numeric status directly in its message ("… failed (HTTP 500) …",
|