jeo-code 0.12.4 → 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 CHANGED
@@ -9,6 +9,27 @@ 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
+
20
+ ## [0.12.5] - 2026-10-17
21
+ _Antigravity and Kimi provider support in interactive setup, Kimi live model cache rehydration, immediate discovery caching on provider probing._
22
+
23
+ ### Added
24
+ - **Interactive Setup Provider Options** (`src/commands/setup.ts`, `src/commands/setup-helpers.ts`) — added `antigravity` (Google Cloud Code Assist OAuth) and `kimi` (Moonshot API key / Kimi Code OAuth) to interactive setup provider selection, enabling browser OAuth, bearer token paste, or API key configuration with default models (`antigravity/gemini-3-flash`, `kimi-k2.5`). Updated `buildEnabledProviders` to recognize configured `kimi` credentials.
25
+ - **Kimi Live Model Cache Rehydration** (`src/ai/model-cache.ts`) — cache hits for Kimi OAuth (`provider === 'kimi' && source === 'oauth'`) now rehydrate `recordLiveKimiCodeModels(entry.models)` alongside OpenAI Codex models.
26
+ - **Immediate Model Cache on Provider Probe** (`src/commands/provider.ts`) — successful `jeo provider test <id>` and `jeo provider add` probing now immediately writes discovered custom/compatible models to the disk cache (`writeModelCache([result])`).
27
+
28
+ ### Fixed
29
+ - **Model Cache Key Scoping** (`src/ai/model-cache.ts`) — fixed `keyOf` helper scoping and included `kimi` in source distinction so OAuth and API key catalogs do not clobber each other.
30
+ - **OAuth Token Preservation in Setup** (`src/commands/setup.ts`) — fixed `delete next.oauth` condition to check `Object.keys(next.oauth).length === 0` so non-Anthropic/OpenAI/Gemini OAuth credentials (such as `antigravity` or `kimi`) are not inadvertently dropped on save.
31
+ - **Auth Import Model Reporting** (`src/commands/auth.ts`) — added `antigravity` reporting to `reportLiveModels` following Gemini OAuth import.
32
+
12
33
  ## [0.12.4] - 2026-10-17
13
34
  _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`._
14
35
 
@@ -2566,7 +2587,8 @@ _Initial release._
2566
2587
  ### Added
2567
2588
  - Initial jeo-code agent and CLI.
2568
2589
 
2569
- [Unreleased]: https://github.com/akillness/jeo-code/compare/v0.11.4...HEAD
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
2570
2592
  [0.11.4]: https://github.com/akillness/jeo-code/releases/tag/v0.11.4
2571
2593
  [0.11.3]: https://github.com/akillness/jeo-code/releases/tag/v0.11.3
2572
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.
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.
419
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`.
420
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.
421
- - **[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.
422
- - **[0.12.1]** (2026-10-16) — Logged-in OpenAI subscription models were invisible on proxy-configured machines.
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.
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.
420
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`.
421
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.
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
- - **[0.12.1]** (2026-10-16) — Logged-in OpenAI subscription models were invisible on proxy-configured machines.
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.
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.
412
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`.
413
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.
414
- - **[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.
415
- - **[0.12.1]** (2026-10-16) — Logged-in OpenAI subscription models were invisible on proxy-configured machines.
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.
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.
417
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`.
418
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.
419
- - **[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.
420
- - **[0.12.1]** (2026-10-16) — Logged-in OpenAI subscription models were invisible on proxy-configured machines.
421
421
 
422
422
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
423
423
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.12.4",
3
+ "version": "0.12.6",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -19,7 +19,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
19
19
  import * as os from "node:os";
20
20
  import * as path from "node:path";
21
21
  import { jeoEnv } from "../util/env";
22
- import { recordLiveCodexModels, recordLiveProviderModels, setAntigravityAccountScope, setOpenAIOauthAccountScope } from "./model-catalog";
22
+ import { recordLiveCodexModels, recordLiveKimiCodeModels, recordLiveProviderModels, setAntigravityAccountScope, setOpenAIOauthAccountScope } from "./model-catalog";
23
23
  import type { Credential } from "../auth";
24
24
  import { getStoredOAuth, snapshotProvider } from "../auth/storage";
25
25
  import { getAntigravityUserAgent } from "./providers/antigravity";
@@ -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 {
@@ -158,7 +173,7 @@ export function mergeCacheEntries(
158
173
  const isScopedOAuth = (provider: ProviderName, source: ModelCacheSource): boolean =>
159
174
  (provider === "openai" || provider === "antigravity") && source === "oauth";
160
175
  const keyOf = (provider: ProviderName, baseUrl?: string, accountId?: string, source?: ModelCacheSource) =>
161
- `${provider}\u0000${baseUrl?.replace(/\/+$/, "") ?? ""}\u0000${isScopedOAuth(provider, source ?? "none") ? accountId ?? "" : ""}\u0000${provider === "openai" || provider === "antigravity" ? source ?? "none" : ""}`;
176
+ `${provider}\u0000${baseUrl?.replace(/\/+$/, "") ?? ""}\u0000${isScopedOAuth(provider, source ?? "none") ? accountId ?? "" : ""}\u0000${provider === "openai" || provider === "antigravity" || provider === "kimi" ? source ?? "none" : ""}`;
162
177
  for (const entry of previous) {
163
178
  const scoped = isScopedOAuth(entry.provider, entry.source);
164
179
  if (entry.provider === "agy") continue;
@@ -240,6 +255,9 @@ export function applyCachedModels(cache: ModelCacheFile | null, openAIOauthAccou
240
255
  if (isOpenAIOAuth && entry.accountId && !entry.baseUrl) {
241
256
  recordLiveCodexModels(entry.models, entry.accountId);
242
257
  }
258
+ if (entry.provider === "kimi" && entry.source === "oauth") {
259
+ recordLiveKimiCodeModels(entry.models);
260
+ }
243
261
  applied += entry.models.length;
244
262
  }
245
263
  return applied;
@@ -248,14 +266,7 @@ export function applyCachedModels(cache: ModelCacheFile | null, openAIOauthAccou
248
266
  /** Read + apply in one step. Returns the cache so callers can decide about refreshing. */
249
267
  export async function rehydrateLiveModels(): Promise<ModelCacheFile | null> {
250
268
  const cache = await readModelCache();
251
- let accountId: string | undefined;
252
- try {
253
- const stored = await getStoredOAuth("openai");
254
- const jwtAccountId = stored ? extractChatgptAccountId(stored.access) : undefined;
255
- accountId = jwtAccountId || stored?.accountId;
256
- } catch {
257
- accountId = undefined;
258
- }
269
+ const accountId = await openAIOauthAccountScope();
259
270
  const antigravityAccountId = await antigravityAccountScope();
260
271
  setOpenAIOauthAccountScope(accountId);
261
272
  setAntigravityAccountScope(antigravityAccountId);
@@ -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(activeOpenAIOauthScope);
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();
@@ -501,6 +506,30 @@ export const KIMI_CODE_MODELS: readonly string[] = [
501
506
  "kimi-k2",
502
507
  ];
503
508
 
509
+ const liveKimiCodeModels = new Set<string>();
510
+
511
+ /** Record ids confirmed by the Kimi Code OAuth `/v1/models` endpoint so brand-new
512
+ * subscription models work immediately without waiting for a static catalog release. */
513
+ export function recordLiveKimiCodeModels(ids: readonly string[]): void {
514
+ for (const id of ids) {
515
+ const trimmed = id.trim();
516
+ if (!trimmed) continue;
517
+ const wire = trimmed.startsWith("kimi/") ? trimmed.slice(5) : trimmed;
518
+ if (wire) liveKimiCodeModels.add(wire);
519
+ }
520
+ }
521
+
522
+ /** Whether `model` is served by the Kimi Code OAuth subscription endpoint. */
523
+ export function isKimiCodeModel(model: string): boolean {
524
+ const wire = model.startsWith("kimi/") ? model.slice(5) : model;
525
+ return KIMI_CODE_MODELS.includes(wire) || liveKimiCodeModels.has(wire);
526
+ }
527
+
528
+ /** Test-only reset for live Kimi Code observations. */
529
+ export function resetLiveKimiCodeModels(): void {
530
+ liveKimiCodeModels.clear();
531
+ }
532
+
504
533
  /** Recency compare for `releaseDate` ("YYYY-MM"): `>0` when `a` is newer; a
505
534
  * missing date sorts oldest, so an unconfirmed date never outranks a
506
535
  * verified-newer model. */
@@ -639,6 +668,20 @@ export function inferCatalogMetadata(modelId: string): CatalogModel | undefined
639
668
  company: "xAI",
640
669
  };
641
670
  }
671
+ // Kimi / Moonshot
672
+ const kimi = id.match(/^kimi-(k\d+|latest|for-coding)/);
673
+ if (kimi) {
674
+ return {
675
+ canonical: raw,
676
+ provider: "kimi",
677
+ providerModel: id,
678
+ contextTokens: 256_000,
679
+ maxOutputTokens: 16_384,
680
+ thinking: FULL,
681
+ images: false,
682
+ company: "Moonshot",
683
+ };
684
+ }
642
685
 
643
686
  return undefined;
644
687
  }
@@ -16,6 +16,7 @@ import {
16
16
  catalogByProvider,
17
17
  KIMI_CODE_MODELS,
18
18
  recordLiveCodexModels,
19
+ recordLiveKimiCodeModels,
19
20
  recordLiveProviderModels,
20
21
  setOpenAIOauthAccountScope,
21
22
  setAntigravityAccountScope,
@@ -23,10 +24,10 @@ import {
23
24
  import { extractChatgptAccountId } from "./providers/openai-responses";
24
25
  import { getStoredOAuth } from "../auth/storage";
25
26
  import { openaiCompatDef } from "./providers/openai-compatible-catalog";
26
- import { antigravityAccountScope } from "./model-cache";
27
+ import { antigravityAccountScope, openAIOauthAccountScope } from "./model-cache";
27
28
  import { getAntigravityUserAgent, resolveAntigravityProjectId } from "./providers/antigravity";
28
29
  import { discoverGoogleProjectId, ANTIGRAVITY_DISCOVERY_METADATA } from "../auth/flows/google-project";
29
-
30
+ import { getKimiCommonHeaders } from "../auth/flows/kimi";
30
31
  export interface ProviderModelsResult {
31
32
  provider: ProviderName;
32
33
  /** Discovered model ids (provider-qualified where the router expects it). */
@@ -185,6 +186,15 @@ export function discoveryRequest(
185
186
  return { url: "https://api.x.ai/v1/models", headers: token ? { Authorization: `Bearer ${token}` } : {} };
186
187
  }
187
188
  case "kimi": {
189
+ if (cred?.kind === "oauth") {
190
+ return {
191
+ url: "https://api.kimi.com/coding/v1/models",
192
+ headers: {
193
+ ...getKimiCommonHeaders(),
194
+ Authorization: `Bearer ${cred.token}`,
195
+ },
196
+ };
197
+ }
188
198
  const token = cred?.kind === "api_key" ? cred.token : "";
189
199
  return { url: "https://api.moonshot.ai/v1/models", headers: token ? { Authorization: `Bearer ${token}` } : {} };
190
200
  }
@@ -402,6 +412,7 @@ export async function listProviderModels(
402
412
 
403
413
  let cred: Credential | undefined;
404
414
  let oauthAccountId: string | undefined;
415
+ let oauthCatalogScope: string | undefined;
405
416
  let source: ProviderModelsResult["source"] = "keyless";
406
417
  if (provider === "xai") {
407
418
  // xAI (Grok) is API-key only and not an OAuth AuthProvider: resolve its key
@@ -456,8 +467,9 @@ export async function listProviderModels(
456
467
  if (provider === "openai" && source === "oauth" && cred.kind === "oauth") {
457
468
  const stored = await getStoredOAuth("openai").catch(() => undefined);
458
469
  const jwtAccountId = extractChatgptAccountId(cred.token);
459
- oauthAccountId = jwtAccountId || stored?.accountId;
460
- setOpenAIOauthAccountScope(oauthAccountId);
470
+ oauthAccountId = jwtAccountId || (stored?.access === cred.token ? stored.accountId : undefined);
471
+ oauthCatalogScope = await openAIOauthAccountScope(cred);
472
+ setOpenAIOauthAccountScope(oauthCatalogScope);
461
473
  }
462
474
  // Gemini models are exposed ONLY under API-key auth: a gemini OAuth token can
463
475
  // still list models from generativelanguage.googleapis.com, but it can no
@@ -474,7 +486,7 @@ export async function listProviderModels(
474
486
  }
475
487
  }
476
488
 
477
- const scope = { ...(oauthAccountId ? { accountId: oauthAccountId } : {}), ...(baseUrl ? { baseUrl } : {}) };
489
+ const scope = { ...(oauthCatalogScope ? { accountId: oauthCatalogScope } : {}), ...(baseUrl ? { baseUrl } : {}) };
478
490
  const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT;
479
491
  let signal = opts.signal ? AbortSignal.any([opts.signal, AbortSignal.timeout(timeout)]) : AbortSignal.timeout(timeout);
480
492
  let initialAntigravityScope: string | undefined;
@@ -568,6 +580,12 @@ export async function listProviderModels(
568
580
  }
569
581
  const allModels = [...new Set(ids)].sort();
570
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
+ }
571
589
  if (provider === "antigravity") {
572
590
  const currentScope = await antigravityAccountScope();
573
591
  if (currentScope !== scope.accountId && currentScope !== initialAntigravityScope) {
@@ -584,7 +602,8 @@ export async function listProviderModels(
584
602
  replace: provider === "openai" || provider === "antigravity",
585
603
  ...(source === "oauth" && scope.accountId ? { accountId: scope.accountId } : {}),
586
604
  });
587
- if (provider === "openai" && source === "oauth" && !baseUrl) recordLiveCodexModels(allModels, oauthAccountId);
605
+ if (provider === "openai" && source === "oauth" && !baseUrl) recordLiveCodexModels(allModels, oauthCatalogScope);
606
+ if (provider === "kimi" && source === "oauth") recordLiveKimiCodeModels(allModels);
588
607
  return {
589
608
  provider,
590
609
  models,
@@ -659,6 +678,12 @@ export async function discoverModels(
659
678
  return [primary, await listProviderModels(p, { ...discoveryOpts, baseUrl: undefined, preferOAuth: true, codexCatalog: true })];
660
679
  }
661
680
  }
681
+ if (p === "kimi" && primary.source !== "oauth") {
682
+ const credential = await resolveCredential("kimi");
683
+ if (credential.kind === "oauth") {
684
+ return [primary, await listProviderModels(p, { ...discoveryOpts, preferOAuth: true })];
685
+ }
686
+ }
662
687
  return [primary];
663
688
  }),
664
689
  );
@@ -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, 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
 
@@ -329,8 +332,7 @@ export function oauthServesModel(provider: AuthProvider, model: string): boolean
329
332
  return isCodexModel(model);
330
333
  }
331
334
  if (provider === "kimi") {
332
- const wire = model.startsWith("kimi/") ? model.slice(5) : model;
333
- return KIMI_CODE_MODELS.includes(wire);
335
+ return isKimiCodeModel(model);
334
336
  }
335
337
  if (isOAuthProvider(provider) && OAUTH_FLOW_REGISTRY[provider].verifiedEndToEnd === false) return false;
336
338
  return true;
@@ -405,6 +407,8 @@ interface Resolved {
405
407
  baseUrl: string | undefined;
406
408
  }
407
409
 
410
+ const codexCatalogInFlight = new Map<string, Promise<ProviderModelsResult>>();
411
+
408
412
  /**
409
413
  * The credential to actually use for a provider call. A configured local OpenAI-compatible base
410
414
  * URL must use the standard /chat/completions path, but the openai adapter dispatches on
@@ -437,7 +441,7 @@ export async function resolveCall(options: Partial<CallOptions>, kind: "request"
437
441
  }
438
442
  const adapter = providerRegistry.get(provider)!;
439
443
 
440
- const baseUrl =
444
+ let baseUrl =
441
445
  options.baseUrl ??
442
446
  (provider === "openai" ? config.openaiBaseUrl : undefined) ??
443
447
  (provider === "ollama" ? config.ollamaBaseUrl : undefined) ??
@@ -504,6 +508,47 @@ export async function resolveCall(options: Partial<CallOptions>, kind: "request"
504
508
  const credentialProvider = provider as AuthProvider;
505
509
  const credential = await resolveCredential(credentialProvider);
506
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
+ }
507
552
  const isLocalOpenAi = provider === "openai" && !!baseUrl;
508
553
  if (provider === "openai" && effective.kind === "oauth" && !isLocalOpenAi && !oauthServesModel("openai", model)) {
509
554
  throw new Error(
@@ -152,7 +152,7 @@ async function runAuthLogin(rest: string[]): Promise<void> {
152
152
  * already succeeded and is not rolled back by this check. Prints only model
153
153
  * ids/count — no tokens/secrets.
154
154
  */
155
- const LIVE_MODEL_REPORT_PROVIDERS = new Set<AuthProvider>(["openai", "anthropic", "antigravity"]);
155
+ const LIVE_MODEL_REPORT_PROVIDERS = new Set<AuthProvider>(["openai", "anthropic", "antigravity", "kimi"]);
156
156
 
157
157
  async function reportLiveModels(provider: AuthProvider): Promise<void> {
158
158
  try {
@@ -249,6 +249,7 @@ async function runAuthImport(rest: string[]): Promise<void> {
249
249
  }
250
250
  const expiryMsg = expires ? fmtExpiry(expires) : "";
251
251
  console.log(`Expiry status:${expiryMsg || " valid"}`);
252
+ await reportLiveModels("antigravity");
252
253
  } catch (err: any) {
253
254
  console.log(`[FAILED] ${err.message}`);
254
255
  process.exitCode = 1;
@@ -335,8 +336,16 @@ async function selectProvider(
335
336
  console.log(" 1) anthropic (real PKCE OAuth, verified end-to-end)");
336
337
  console.log(" 2) openai (real PKCE OAuth, Codex backend)");
337
338
  console.log(" 3) gemini (real Google OAuth, Cloud Code Assist)");
338
- const ans = (await rl.question("Choose [1-3]: ")).trim();
339
- const map: Record<string, AuthProvider> = { "1": "anthropic", "2": "openai", "3": "gemini" };
339
+ console.log(" 4) antigravity (Google Cloud Code Assist OAuth)");
340
+ console.log(" 5) kimi (Kimi Code OAuth device authorization)");
341
+ const ans = (await rl.question("Choose [1-5]: ")).trim();
342
+ const map: Record<string, AuthProvider> = {
343
+ "1": "anthropic",
344
+ "2": "openai",
345
+ "3": "gemini",
346
+ "4": "antigravity",
347
+ "5": "kimi",
348
+ };
340
349
  const p = map[ans];
341
350
  if (!p) {
342
351
  console.log("Invalid choice.");
@@ -31,6 +31,7 @@ import {
31
31
  normalizeProviderId,
32
32
  } from "../ai/providers/custom-providers";
33
33
  import { listProviderModels } from "../ai/model-discovery";
34
+ import { writeModelCache } from "../ai/model-cache";
34
35
  import { describeProvider } from "../ai/provider-status";
35
36
 
36
37
  const USAGE = [
@@ -186,6 +187,7 @@ async function probe(id: string, opts: { failOnError: boolean }): Promise<void>
186
187
  if (shown.length) console.log(` ${shown.join(", ")}${result.models.length > shown.length ? ", …" : ""}`);
187
188
  const first = result.models[0];
188
189
  console.log(` status : OK — use it with 'jeo launch --model ${first ? qualify(first) : `${id}/<model>`}'`);
190
+ await writeModelCache([result]);
189
191
  return;
190
192
  }
191
193
 
@@ -76,6 +76,7 @@ export function buildEnabledProviders(config: Config): string[] {
76
76
  if (cfg.providers?.anthropic || cfg.oauth?.anthropic) enabled.push("anthropic");
77
77
  if (cfg.providers?.openai || cfg.oauth?.openai) enabled.push("openai");
78
78
  if (cfg.providers?.gemini || cfg.oauth?.gemini || cfg.providers?.antigravity || cfg.oauth?.antigravity) enabled.push("google-oauth");
79
+ if (cfg.providers?.kimi || cfg.oauth?.kimi) enabled.push("kimi");
79
80
  if (cfg.ollamaBaseUrl) enabled.push(`ollama(${cfg.ollamaBaseUrl})`);
80
81
  if (cfg.openaiBaseUrl) enabled.push(`openai-compatible(${cfg.openaiBaseUrl})`);
81
82
  return enabled;
@@ -24,12 +24,14 @@ function reportModelChoice(r: { warning?: string; suggestions: string[] }): void
24
24
  if (r.suggestions.length) console.log(` Did you mean: ${r.suggestions.join(", ")}?`);
25
25
  }
26
26
 
27
- type ProviderChoice = "anthropic" | "openai" | "gemini" | "ollama" | "lmstudio" | "openai-compatible";
27
+ type ProviderChoice = "anthropic" | "openai" | "gemini" | "antigravity" | "kimi" | "ollama" | "lmstudio" | "openai-compatible";
28
28
 
29
29
  const DEFAULT_MODELS: Record<ProviderChoice, string> = {
30
30
  anthropic: "claude-sonnet-4-6",
31
31
  openai: "gpt-4o",
32
32
  gemini: "gemini-2.0-flash",
33
+ antigravity: "antigravity/gemini-3-flash",
34
+ kimi: "kimi-k2.5",
33
35
  ollama: "ollama/llama3.1:8b",
34
36
  lmstudio: "openai/local-model",
35
37
  "openai-compatible": "openai/local-model",
@@ -87,16 +89,19 @@ export async function runSetupCommand(): Promise<void> {
87
89
  console.log(" 1) anthropic — Claude (API key or OAuth bearer)");
88
90
  console.log(" 2) openai — GPT (API key or OAuth bearer)");
89
91
  console.log(" 3) gemini — Google AI Studio (API key or OAuth bearer)");
90
- console.log(" 4) ollama — local Ollama (no auth)");
91
- console.log(" 5) lmstudio — local LM Studio (OpenAI-compatible)");
92
- console.log(" 6) openai-compatible — custom OpenAI-compatible (vLLM, llama-cpp-server, ...)");
93
- console.log(" 7) skip — keep existing values, just change defaults\n");
94
-
95
- const sel = (await rl.question("Configure which provider? [1-7]: ")).trim();
92
+ console.log(" 4) antigravity — Google Cloud Code Assist (OAuth)");
93
+ console.log(" 5) kimi — Kimi / Moonshot (API key or Kimi Code OAuth)");
94
+ console.log(" 6) ollama — local Ollama (no auth)");
95
+ console.log(" 7) lmstudio — local LM Studio (OpenAI-compatible)");
96
+ console.log(" 8) openai-compatible — custom OpenAI-compatible (vLLM, llama-cpp-server, ...)");
97
+ console.log(" 9) skip — keep existing values, just change defaults\n");
98
+
99
+ const sel = (await rl.question("Configure which provider? [1-9]: ")).trim();
96
100
  const map: Record<string, ProviderChoice | "skip"> = {
97
101
  "1": "anthropic", "2": "openai", "3": "gemini",
98
- "4": "ollama", "5": "lmstudio", "6": "openai-compatible",
99
- "7": "skip",
102
+ "4": "antigravity", "5": "kimi",
103
+ "6": "ollama", "7": "lmstudio", "8": "openai-compatible",
104
+ "9": "skip",
100
105
  };
101
106
  const choice = map[sel] ?? "skip";
102
107
 
@@ -108,7 +113,7 @@ export async function runSetupCommand(): Promise<void> {
108
113
  next.providers = next.providers || {};
109
114
  next.oauth = next.oauth || {};
110
115
 
111
- if (choice === "anthropic" || choice === "openai" || choice === "gemini") {
116
+ if (choice === "anthropic" || choice === "openai" || choice === "gemini" || choice === "antigravity" || choice === "kimi") {
112
117
  const authMode = (
113
118
  await rl.question("Auth mode: (b)rowser OAuth login, (t)oken paste, or api (k)ey? [b]: ")
114
119
  ).trim().toLowerCase();
@@ -223,7 +228,7 @@ export async function runSetupCommand(): Promise<void> {
223
228
  rl.close();
224
229
 
225
230
  // Drop empty oauth/providers to keep config tidy.
226
- if (next.oauth && !next.oauth.anthropic && !next.oauth.openai && !next.oauth.gemini) delete next.oauth;
231
+ if (next.oauth && Object.keys(next.oauth).length === 0) delete next.oauth;
227
232
 
228
233
  await saveGlobalConfig(next);
229
234
  console.log("\n[SUCCESS] Configuration saved to ~/.jeo/config.json");
@@ -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 provider = knownErrorProvider(err) ?? (
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
- if (status === 401 || status === 403 || /\b40[13]\b/.test(msg)) {
121
- return `${provider} rejected the credential (HTTP ${status ?? "401/403"}). Run 'jeo auth status', re-login with /provider login <name>, and for Antigravity prefer '/provider login antigravity' (gemini login only works when the Cloud Code Assist backend authorizes that token).`;
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) …",