jeo-code 0.12.1 → 0.12.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.
@@ -80,7 +80,9 @@ const STD: ThinkLevel[] = ["low", "medium", "high"];
80
80
  * (`cheapestCredentialed`/`strongestCredentialed`/`compareStrengthAscending`)
81
81
  * now tiebreak on `releaseDate` directly rather than relying on position. */
82
82
  export const MODEL_CATALOG: readonly CatalogModel[] = [
83
- // Anthropic (newest first: sonnet-5-5 > opus-5-5 > sonnet-5 > fable-5 > opus-4-8 > opus-4-7 > sonnet-4-6 > opus-4-6). 4.5-and-below dropped from jeo's exposed model list per user direction.
83
+ // Anthropic (newest first: sonnet-5-5 > opus-5-5 > sonnet-5 > fable-5 > mythos-5).
84
+ // claude-4.x and below removed: live API discovery (https://api.anthropic.com/v1/models)
85
+ // returns the account's actual available models; the static catalog is fallback-only.
84
86
  // 5.5 generation (platform.claude.com/docs/en/models/{opus-5-5,sonnet-5-5}/overview): dateless pinned wire ids
85
87
  // `claude-opus-5-5` (released 2026-09-22) / `claude-sonnet-5-5` (2026-09-28), 1M ctx / 128k sync max output, adaptive
86
88
  // thinking (Opus 5.5 always on; Sonnet 5.5 on by default), text+image input. Canonical == wire id; anthropic.ts already
@@ -92,17 +94,7 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
92
94
  // Mythos 5 is limited-availability (Project Glasswing) but callable by id for approved accounts.
93
95
  { canonical: "claude-fable-5", provider: "anthropic", providerModel: "claude-fable-5", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-06" },
94
96
  { canonical: "claude-mythos-5", provider: "anthropic", providerModel: "claude-mythos-5", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, limitedAvailability: true, releaseDate: "2026-06" },
95
- // NOTE: opus 4.6+ use Anthropic ADAPTIVE thinking (type:"adaptive" + output_config.effort).
96
- // opus 4.7/4.8 OMIT visible thought unless the request opts into `display: "summarized"` —
97
- // anthropic.ts sets that on the adaptive transport so reasoning streams again (gjc parity).
98
- // The nativizable path still replays signature-only thinking blocks for cross-turn continuity.
99
- // The 4.6 generation onward is 1M context / 128k sync max output (Anthropic docs:
100
- // Opus 4.8 & Sonnet 5 comparison table; Opus 4.6/4.7, Sonnet 4.6 share the gen's
101
- // dateless-snapshot 1M/128k + 300k batch-output beta).
102
- { canonical: "claude-opus-4-8", provider: "anthropic", providerModel: "claude-opus-4-8", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-05" },
103
- { canonical: "claude-opus-4-7", provider: "anthropic", providerModel: "claude-opus-4-7", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-04" },
104
- { canonical: "claude-sonnet-4-6", provider: "anthropic", providerModel: "claude-sonnet-4-6", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-02" },
105
- { canonical: "claude-opus-4-6", provider: "anthropic", providerModel: "claude-opus-4-6", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-02" },
97
+
106
98
  // OpenAI (newest first: gpt-5.6 > gpt-5.5 > gpt-5.4 > o4-mini/o3/gpt-4.1 > o3-mini > gpt-4o-mini > gpt-4o)
107
99
  { canonical: "gpt-5.6", provider: "openai", providerModel: "gpt-5.6", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-06" },
108
100
  { canonical: "gpt-5.5", provider: "openai", providerModel: "gpt-5.5", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-04" },
@@ -278,7 +270,7 @@ export function resetLiveCodexModels(): void {
278
270
  * this process. Unlike `liveCodexModels`, this is NOT an OAuth allow-list; it is a
279
271
  * routing supplement so API-key and OpenAI-compatible catalogs discovered at runtime
280
272
  * can participate in prompt routing without waiting for a static release. */
281
- type LiveProviderModelSource = "oauth" | "api_key" | "keyless" | "none";
273
+ type LiveProviderModelSource = "oauth" | "api_key" | "keyless" | "none" | "cli_session";
282
274
  interface LiveProviderModelRecord {
283
275
  row: CatalogModel;
284
276
  source: LiveProviderModelSource;
@@ -339,6 +331,7 @@ function liveCanonicalId(provider: ProviderName, id: string): string {
339
331
  : `openai/${trimmed}`;
340
332
  }
341
333
  if (provider === "xai" || provider === "kimi") return trimmed;
334
+ if (provider === "agy") return `${provider}/${trimmed}`;
342
335
  return openaiCompatDef(provider) || provider === "ollama" || provider === "lmstudio" || provider === "antigravity"
343
336
  ? `${provider}/${trimmed}`
344
337
  : trimmed;
@@ -391,7 +384,7 @@ export function recordLiveProviderModels(
391
384
  if (provider === "openai" && source === "oauth" && scopedAccountId) {
392
385
  setOpenAIOauthAccountScope(scopedAccountId);
393
386
  }
394
- const shouldReplace = provider === "antigravity" || (provider === "openai" && !!opts.replace);
387
+ const shouldReplace = provider === "antigravity" || provider === "agy" || (provider === "openai" && !!opts.replace);
395
388
  if (shouldReplace) {
396
389
  for (const [key, record] of liveProviderModels) {
397
390
  const sameScope = provider === "openai"
@@ -525,7 +518,11 @@ export function formatTokens(n: number): string {
525
518
  /** Exact lookup by canonical id or provider model id. */
526
519
  export function findCatalogModel(idOrModel: string): CatalogModel | undefined {
527
520
  const q = idOrModel.trim();
528
- return MODEL_CATALOG.find(m => m.canonical === q || m.providerModel === q || `${m.provider}/${m.providerModel}` === q);
521
+ return MODEL_CATALOG.find(m =>
522
+ m.canonical === q
523
+ || `${m.provider}/${m.providerModel}` === q
524
+ || (!m.canonical.startsWith(`${m.provider}/`) && m.providerModel === q),
525
+ );
529
526
  }
530
527
 
531
528
  /**
@@ -652,7 +649,10 @@ export function catalogMetadata(modelId: string): CatalogModel | undefined {
652
649
  if (direct) return direct;
653
650
  // Tolerate provider-prefixed or bare provider model ids.
654
651
  const bare = modelId.replace(/^[a-z-]+\//, "");
655
- const hit = MODEL_CATALOG.find(m => m.providerModel === bare || m.providerModel.endsWith(`/${bare}`) || m.canonical === bare);
652
+ const hit = MODEL_CATALOG.find(m =>
653
+ m.canonical === bare
654
+ || (!m.canonical.startsWith(`${m.provider}/`) && (m.providerModel === bare || m.providerModel.endsWith(`/${bare}`))),
655
+ );
656
656
  if (hit) return hit;
657
657
  // Last resort: infer capabilities from the model family so a brand-new
658
658
  // revision still surfaces reasoning/thinking like its catalogued siblings.
@@ -679,5 +679,6 @@ export function companyLabel(provider: string, entry?: { company?: string }): st
679
679
  const compat = openaiCompatDef(low);
680
680
  if (compat) return compat.label;
681
681
  if (low === "antigravity") return "Antigravity";
682
+ if (low === "agy") return "Argon CLI";
682
683
  return provider.charAt(0).toUpperCase() + provider.slice(1);
683
684
  }
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { readGlobalConfig, type Config } from "../agent/state";
11
11
  import { resolveCredential, type AuthProvider, type Credential } from "../auth";
12
+ import { listAgyModels } from "../agent/antigravity-cli";
12
13
  import type { ProviderName } from "./types";
13
14
  import { PROVIDER_NAMES } from "./provider-status";
14
15
  import {
@@ -32,7 +33,7 @@ export interface ProviderModelsResult {
32
33
  models: string[];
33
34
  ok: boolean;
34
35
  /** How the request authenticated (for display). */
35
- source: "oauth" | "api_key" | "keyless" | "none";
36
+ source: "oauth" | "api_key" | "keyless" | "cli_session" | "none";
36
37
  /** Opaque OAuth account scope for account-specific cache rows. */
37
38
  accountId?: string;
38
39
  /** Actual custom endpoint queried, for endpoint-scoped persistent caches. */
@@ -50,6 +51,9 @@ export interface DiscoveryOptions {
50
51
  /** Per-request timeout; default 5000ms. */
51
52
  timeoutMs?: number;
52
53
  signal?: AbortSignal;
54
+ /** Working directory and executable override for the externally managed agy CLI. */
55
+ cwd?: string;
56
+ executable?: string;
53
57
  /** Cap returned ids; OpenAI and Antigravity default to the full account list. */
54
58
  limit?: number;
55
59
  /** Config snapshot used for provider base URLs. */
@@ -210,6 +214,10 @@ function isGeminiChatModel(id: string): boolean {
210
214
  type CodexModelRow = { slug?: string; id?: string; visibility?: string; supported_in_api?: boolean; priority?: number };
211
215
  type AntigravityModelRow = { slug?: string; id?: string; name?: string; isInternal?: boolean; model?: string };
212
216
 
217
+ function normalizeAntigravityModelId(id: string): string {
218
+ return id.replace(/^models\//, "").replace(/^antigravity\//, "");
219
+ }
220
+
213
221
  /** Parse a provider's models response body into normalized, chat-capable model ids. */
214
222
  export function parseModelsBody(provider: ProviderName, body: unknown, opts: { openaiCompatible?: boolean } = {}): string[] {
215
223
  const data = body as {
@@ -241,13 +249,14 @@ export function parseModelsBody(provider: ProviderName, body: unknown, opts: { o
241
249
  // "gemini-3-flash"); the entry's `model` field is an internal enum
242
250
  // (MODEL_PLACEHOLDER_*) and must never be surfaced. The response's OWN
243
251
  // metadata decides what to show — no hard-coded model lists:
244
- // 1. `agentModelSorts` groups are the API's positive agent/chat set
245
- // (exactly what Antigravity's model picker offers) — prefer it.
246
- // 2. Otherwise fall back to excluding the API's non-chat role lists
247
- // (tab completion, image generation, transcription, deprecations).
252
+ // Agent sorts and tiered model ids are complementary chat offerings, not
253
+ // interchangeable allowlists. Include both plus the declared default;
254
+ // only older responses without these fields need non-chat role filtering.
248
255
  const payload = body as {
249
256
  models?: Record<string, AntigravityModelRow> | AntigravityModelRow[];
250
257
  agentModelSorts?: { groups?: { modelIds?: string[] }[] }[];
258
+ tieredModelIds?: Record<string, string[]>;
259
+ defaultAgentModelId?: string;
251
260
  tabModelIds?: string[];
252
261
  imageGenerationModelIds?: string[];
253
262
  audioTranscriptionModelIds?: string[];
@@ -256,16 +265,21 @@ export function parseModelsBody(provider: ProviderName, body: unknown, opts: { o
256
265
  /** Array of ids OR an object keyed by deprecated id. */
257
266
  deprecatedModelIds?: string[] | Record<string, unknown>;
258
267
  };
259
- const normalizeId = (id: string): string => id.replace(/^models\//, "").replace(/^antigravity\//, "");
260
- const roleIds = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === "string").map(normalizeId) : []);
268
+ const roleIds = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === "string").map(normalizeAntigravityModelId) : []);
261
269
  const deprecated = Array.isArray(payload.deprecatedModelIds)
262
270
  ? roleIds(payload.deprecatedModelIds)
263
- : Object.keys(payload.deprecatedModelIds ?? {}).map(normalizeId);
271
+ : Object.keys(payload.deprecatedModelIds ?? {}).map(normalizeAntigravityModelId);
264
272
  const agentIds = new Set(
265
273
  (Array.isArray(payload.agentModelSorts) ? payload.agentModelSorts : [])
266
274
  .flatMap(sort => (Array.isArray(sort?.groups) ? sort.groups : []))
267
275
  .flatMap(group => roleIds(group?.modelIds)),
268
276
  );
277
+ for (const ids of Object.values(payload.tieredModelIds ?? {})) {
278
+ for (const id of roleIds(ids)) agentIds.add(id);
279
+ }
280
+ if (typeof payload.defaultAgentModelId === "string") agentIds.add(normalizeAntigravityModelId(payload.defaultAgentModelId));
281
+ const hasChatModelMetadata = Array.isArray(payload.agentModelSorts)
282
+ || payload.tieredModelIds !== undefined || payload.defaultAgentModelId !== undefined;
269
283
  const nonChat = new Set([
270
284
  ...roleIds(payload.tabModelIds),
271
285
  ...roleIds(payload.imageGenerationModelIds),
@@ -279,11 +293,11 @@ export function parseModelsBody(provider: ProviderName, body: unknown, opts: { o
279
293
  ? rawModels.map(model => [model?.slug ?? model?.id ?? model?.name ?? "", model] as const)
280
294
  : Object.entries(rawModels ?? {});
281
295
  return entries
282
- .map(([id, model]) => [typeof id === "string" ? normalizeId(id) : "", model] as const)
296
+ .map(([id, model]) => [typeof id === "string" ? normalizeAntigravityModelId(id) : "", model] as const)
283
297
  .filter(([id, model]) =>
284
298
  id.length > 0 &&
285
299
  model?.isInternal !== true &&
286
- (Array.isArray(payload.agentModelSorts) ? agentIds.has(id) : !nonChat.has(id)))
300
+ (hasChatModelMetadata ? agentIds.has(id) : !nonChat.has(id)))
287
301
  .map(([id]) => `antigravity/${id}`);
288
302
  }
289
303
  if (provider === "gemini") {
@@ -320,6 +334,8 @@ export function catalogOr(result: ProviderModelsResult): ProviderModelsResult {
320
334
  if (result.ok && result.models.length > 0) return result;
321
335
  // OpenAI has live list endpoints for both auth modes. Neither a failed request
322
336
  // nor an authoritative empty list establishes access to the static snapshot.
337
+ // Argon CLI catalogs are live, process-local, and never synthesized from jeo's static catalog.
338
+ if (result.provider === "agy") return result;
323
339
  if (result.provider === "openai") return result;
324
340
  // Antigravity's available models depend on the Cloud Code Assist agent backend
325
341
  // and must never be faked from a hard-coded catalog.
@@ -357,6 +373,17 @@ export async function listProviderModels(
357
373
  provider: ProviderName,
358
374
  opts: DiscoveryOptions = {},
359
375
  ): Promise<ProviderModelsResult> {
376
+ if (provider === "agy") {
377
+ try {
378
+ const models = await listAgyModels({ cwd: opts.cwd, timeoutMs: opts.timeoutMs, signal: opts.signal, executable: opts.executable });
379
+ const unique = [...new Set(models)];
380
+ recordLiveProviderModels("agy", unique, { source: "cli_session", replace: true });
381
+ return { provider: "agy", models: unique, ok: true, source: "cli_session" };
382
+ } catch (error) {
383
+ const message = error instanceof Error ? error.message : String(error);
384
+ return { provider: "agy", models: [], ok: false, source: "cli_session", error: message };
385
+ }
386
+ }
360
387
  const fetchImpl = opts.fetchImpl ?? fetch;
361
388
  const limit = opts.limit ?? (provider === "openai" || provider === "antigravity" ? Number.POSITIVE_INFINITY : DEFAULT_LIMIT);
362
389
  let baseUrl = opts.baseUrl;
@@ -496,20 +523,25 @@ export async function listProviderModels(
496
523
  const sorts = payload?.agentModelSorts;
497
524
  const invalidSorts = sorts !== undefined && (!Array.isArray(sorts) || sorts.some(sort =>
498
525
  !sort || typeof sort !== "object" || !Array.isArray(sort.groups) || sort.groups.some((group: { modelIds?: unknown } | null) =>
499
- !group || !Array.isArray(group.modelIds) || group.modelIds.some(id => typeof id !== "string" || !id.trim()))));
526
+ !group || !Array.isArray(group.modelIds) || group.modelIds.some(id => typeof id !== "string" || !normalizeAntigravityModelId(id).trim()))));
500
527
  const invalidRoles = ["tabModelIds", "imageGenerationModelIds", "audioTranscriptionModelIds", "commitMessageModelIds", "mqueryModelIds"].some(field => {
501
528
  const ids = payload?.[field];
502
- return ids !== undefined && (!Array.isArray(ids) || ids.some(id => typeof id !== "string" || !id.trim()));
529
+ return ids !== undefined && (!Array.isArray(ids) || ids.some(id => typeof id !== "string" || !normalizeAntigravityModelId(id).trim()));
503
530
  });
504
531
  const deprecated = payload?.deprecatedModelIds;
505
532
  const invalidDeprecated = deprecated !== undefined && (Array.isArray(deprecated)
506
- ? deprecated.some(id => typeof id !== "string" || !id.trim())
507
- : !deprecated || typeof deprecated !== "object");
508
- if (!rows || rows.some(([id, row]) => !row || typeof row !== "object" || Array.isArray(row)
509
- || (Array.isArray(models)
510
- ? ![row.slug, row.id, row.name].some(value => typeof value === "string" && value.trim().length > 0)
511
- : !id.trim()))
512
- || invalidSorts || invalidRoles || invalidDeprecated) {
533
+ ? deprecated.some(id => typeof id !== "string" || !normalizeAntigravityModelId(id).trim())
534
+ : !deprecated || typeof deprecated !== "object" || Object.keys(deprecated).some(id => !normalizeAntigravityModelId(id).trim()));
535
+ const tiered = payload?.tieredModelIds;
536
+ const invalidTiered = tiered !== undefined && (!tiered || typeof tiered !== "object" || Array.isArray(tiered)
537
+ || Object.values(tiered).some(ids => !Array.isArray(ids) || ids.some(id => typeof id !== "string" || !normalizeAntigravityModelId(id).trim())));
538
+ const defaultId = payload?.defaultAgentModelId;
539
+ const invalidDefault = defaultId !== undefined && (typeof defaultId !== "string" || !normalizeAntigravityModelId(defaultId).trim());
540
+ if (!rows || rows.some(([id, row]) => {
541
+ if (!row || typeof row !== "object" || Array.isArray(row)) return true;
542
+ const modelId = Array.isArray(models) ? row.slug ?? row.id ?? row.name : id;
543
+ return typeof modelId !== "string" || !normalizeAntigravityModelId(modelId).trim();
544
+ }) || invalidSorts || invalidRoles || invalidDeprecated || invalidTiered || invalidDefault) {
513
545
  return { provider, models: [], ok: false, source, error: "invalid models response", ...scope };
514
546
  }
515
547
  }
@@ -593,9 +625,14 @@ export async function discoverModels(
593
625
  ): Promise<ProviderModelsResult[]> {
594
626
  const cfg = opts.config ?? (await readGlobalConfig());
595
627
  const providers = opts.providers ?? [...PROVIDER_NAMES];
628
+ const agyExplicit = opts.providers?.includes("agy") ?? false;
629
+ const agyDefault = typeof cfg.defaultModel === "string" && cfg.defaultModel.trim().toLowerCase().startsWith("agy/");
596
630
  const useFallback = opts.catalogFallback !== false;
597
631
  const results = await Promise.all(
598
- providers.map(async p => {
632
+ providers.map(async (p): Promise<ProviderModelsResult[]> => {
633
+ if (p === "agy" && !agyExplicit && !agyDefault) {
634
+ return [{ provider: "agy", models: [], ok: false, source: "none", error: "official CLI discovery requires explicit selection" }];
635
+ }
599
636
  const discoveryOpts = {
600
637
  ...opts,
601
638
  config: cfg,
@@ -37,6 +37,7 @@ export function resolveProvider(model: string): ProviderName {
37
37
  if (m.startsWith("ollama/")) return "ollama";
38
38
  if (m.startsWith("lmstudio/")) return "lmstudio";
39
39
  if (m.startsWith("antigravity/")) return "antigravity";
40
+ if (m.startsWith("agy/")) return "agy";
40
41
  if (m.startsWith("xai/")) return "xai";
41
42
  if (m.startsWith("kimi/")) return "kimi";
42
43
  // Built-ins FIRST, then user-registered custom providers: a custom id can never be a
@@ -59,6 +60,7 @@ const STATIC_ID_PREFIX: Partial<Record<ProviderName, string>> = {
59
60
  openai: "openai/",
60
61
  gemini: "google/",
61
62
  antigravity: "antigravity/",
63
+ agy: "agy/",
62
64
  ollama: "ollama/",
63
65
  lmstudio: "lmstudio/",
64
66
  xai: "xai/",
@@ -95,6 +97,7 @@ export function providerModelFor(model: string): string {
95
97
  model.startsWith("antigravity/") ||
96
98
  model.startsWith("lmstudio/") ||
97
99
  model.startsWith("xai/") ||
100
+ model.startsWith("agy/") ||
98
101
  model.startsWith("kimi/") ||
99
102
  isOpenAICompatProvider(model.split("/")[0])
100
103
  ) {
@@ -350,6 +353,7 @@ export function oauthServesModel(provider: AuthProvider, model: string): boolean
350
353
  * - a configured OpenAI base URL is the keyless local-proxy path — any model.
351
354
  */
352
355
  export function modelServableWithConfig(provider: ProviderName, model: string, config: Partial<Pick<Config, "providers" | "oauth" | "openaiBaseUrl">>): boolean {
356
+ if (provider === "agy") return false;
353
357
  if (provider === "ollama" || provider === "lmstudio") return true;
354
358
  if (provider === "antigravity") return !!(config.oauth?.antigravity || config.oauth?.gemini);
355
359
  if (provider === "openai" && config.openaiBaseUrl) return true;
@@ -428,6 +432,9 @@ export async function resolveCall(options: Partial<CallOptions>, kind: "request"
428
432
  const aliases = { ...((config as { modelAliases?: Record<string, string> }).modelAliases ?? {}) };
429
433
  const model = expandAlias(options.model ?? config.defaultModel, { ...BUILTIN_ALIASES, ...aliases });
430
434
  const provider = resolveProvider(model);
435
+ if (provider === "agy") {
436
+ throw new Error("agy models are full-agent CLI sessions, not raw LLM calls; run the selected agy model through the explicit CLI bridge.");
437
+ }
431
438
  const adapter = providerRegistry.get(provider)!;
432
439
 
433
440
  const baseUrl =
@@ -9,18 +9,18 @@ export interface ModelAliases {
9
9
  export const BUILTIN_ALIASES: ModelAliases = {
10
10
  fast: "ollama/qwen2.5:0.5b",
11
11
  local: "ollama/qwen2.5:0.5b",
12
- sonnet: "claude-sonnet-4-6",
13
- opus: "claude-opus-4-6",
12
+ sonnet: "claude-sonnet-5-5",
13
+ opus: "claude-opus-5-5",
14
14
  gpt: "gpt-5.5",
15
15
  flash: "gemini-2.5-flash",
16
16
  grok: "grok-4.3",
17
17
  kimi: "kimi-k2-0711-preview",
18
- // Anthropic 5.5 generation: spellings that name the version. Bare `opus`/`sonnet` above stay on the
19
- // 4.6 ids (pinned backward-compat); users opt into 5.5 by naming the version.
18
+ // Anthropic 5.5 generation: versioned spellings as convenience aliases.
20
19
  "opus-5.5": "claude-opus-5-5",
21
20
  "sonnet-5.5": "claude-sonnet-5-5",
22
21
  "opus5.5": "claude-opus-5-5",
23
22
  "sonnet5.5": "claude-sonnet-5-5",
23
+
24
24
  // Antigravity's backend DEPRECATED this wire id (deprecatedModelIds:
25
25
  // { "gemini-3.1-pro-high": { newModelId: "gemini-pro-agent" } }) — keep
26
26
  // existing configs/roles pinned to the old id working on the successor.
@@ -46,7 +46,7 @@ export const FAMOUS_PROVIDER_ORDER: readonly string[] = [
46
46
  "openai",
47
47
  "gemini",
48
48
  "antigravity",
49
- "xai",
49
+ "agy",
50
50
  "kimi",
51
51
  "zai",
52
52
  "minimax",
@@ -142,7 +142,7 @@ export function sortRankedProviders<T extends RankableProvider>(providers: reado
142
142
  /** Map a jeo `ProviderStatus`-shaped record onto the ranking's auth-state vocabulary. */
143
143
  export function authStateFor(status: {
144
144
  ready: boolean;
145
- kind: "api_key" | "oauth" | "keyless" | "none";
145
+ kind: "api_key" | "oauth" | "keyless" | "cli_session" | "none";
146
146
  loggedIn?: boolean;
147
147
  }): ProviderAuthState {
148
148
  if (status.ready) return status.kind === "oauth" ? "valid" : "configured";
@@ -14,7 +14,7 @@ import { customProviderDef, customProviderNames, credentialSourceOf, resolveCust
14
14
 
15
15
  /** Compiled-in providers, in declaration order. Custom providers are NOT here — they
16
16
  * are discovered at runtime, so use {@link allProviderNames} for anything the user sees. */
17
- export const PROVIDER_NAMES: readonly ProviderName[] = ["anthropic", "openai", "gemini", "antigravity", "ollama", "lmstudio", "xai", "kimi", ...OPENAI_COMPAT_NAMES];
17
+ export const PROVIDER_NAMES: readonly ProviderName[] = ["anthropic", "openai", "gemini", "antigravity", "agy", "ollama", "lmstudio", "xai", "kimi", ...OPENAI_COMPAT_NAMES];
18
18
 
19
19
  /** Built-in providers PLUS the user's registered custom providers. Every user-facing
20
20
  * surface (pickers, `/provider`, autocomplete, doctor) should list from here so a
@@ -27,7 +27,7 @@ export function allProviderNames(): readonly ProviderName[] {
27
27
  /** Cloud providers that authenticate via API key / OAuth. Ollama is keyless. */
28
28
  export const CLOUD_PROVIDERS: readonly AuthProvider[] = ["anthropic", "openai", "gemini", "antigravity"];
29
29
 
30
- export type CredentialKind = "api_key" | "oauth" | "keyless" | "none";
30
+ export type CredentialKind = "api_key" | "oauth" | "keyless" | "cli_session" | "none";
31
31
 
32
32
  export interface ProviderStatus {
33
33
  name: ProviderName;
@@ -54,6 +54,7 @@ export interface ProviderStatus {
54
54
  * own (e.g. HF_TOKEN, NANO_GPT_API_KEY); built-ins use `<PROVIDER>_API_KEY`. */
55
55
  export function providerEnvVar(name: ProviderName): string | undefined {
56
56
  if (name === "ollama" || name === "lmstudio" || name === "antigravity") return undefined;
57
+ if (name === "agy") return undefined;
57
58
  const def = openaiCompatDef(name);
58
59
  if (def) return def.apiKeyEnv;
59
60
  return `${name.toUpperCase()}_API_KEY`;
@@ -77,6 +78,8 @@ export function credentialLabel(kind: CredentialKind): string {
77
78
  return "keyless (local)";
78
79
  case "none":
79
80
  return "none (run 'jeo setup' or 'jeo auth login')";
81
+ case "cli_session":
82
+ return "external CLI session (agy; auth and permissions managed by the CLI)";
80
83
  }
81
84
  }
82
85
 
@@ -133,6 +136,22 @@ function effectiveCredential(provider: AuthProvider, cred: Credential, cfg: Conf
133
136
  /** Resolve the status of a single provider. */
134
137
  export async function describeProvider(name: ProviderName, config?: Config): Promise<ProviderStatus> {
135
138
  const cfg = config ?? (await readGlobalConfig());
139
+ if (name === "agy") {
140
+ let executable: string | undefined;
141
+ try {
142
+ executable = Bun.which("agy") ?? undefined;
143
+ } catch {
144
+ executable = undefined;
145
+ }
146
+ return {
147
+ name,
148
+ kind: "cli_session",
149
+ label: executable
150
+ ? `UNKNOWN — external CLI session (agy available at ${executable}; auth/permissions are externally managed; not probed)`
151
+ : "external CLI session unavailable — agy CLI not found on PATH; install/login with the official agy CLI; jeo does not manage its credentials",
152
+ ready: !!executable,
153
+ };
154
+ }
136
155
  // Custom providers resolve their credential from their OWN env var / stored literal,
137
156
  // not from the fixed `providers` union — report that source verbatim so a user who
138
157
  // set the wrong env var sees WHICH variable jeo is looking at.
@@ -76,7 +76,7 @@ export const RESERVED_PROVIDER_IDS: readonly string[] = [
76
76
  "antigravity",
77
77
  "ollama",
78
78
  "lmstudio",
79
- "xai",
79
+ "agy",
80
80
  "kimi",
81
81
  // Reserved so a future built-in cannot be shadowed by a stale user entry.
82
82
  "google",
package/src/ai/types.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Credential } from "../auth";
2
2
 
3
3
  /** Providers compiled into jeo (built-ins + the OpenAI/Anthropic-compatible catalog). */
4
- export type BuiltinProviderName = "anthropic" | "openai" | "gemini" | "antigravity" | "ollama" | "lmstudio" | "xai" | "kimi" | "groq" | "deepseek" | "mistral" | "openrouter" | "together" | "cerebras" | "fireworks" | "nvidia" | "alibaba-coding-plan" | "huggingface" | "nanogpt" | "qwen-portal" | "synthetic" | "venice" | "zenmux" | "qianfan" | "xiaomi" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "xiaomi-token-plan-sgp" | "minimax-code" | "minimax-code-cn" | "zai" | "minimax" | "tencent" | "deepinfra" | "litellm";
4
+ export type BuiltinProviderName = "anthropic" | "openai" | "gemini" | "antigravity" | "agy" | "ollama" | "lmstudio" | "xai" | "kimi" | "groq" | "deepseek" | "mistral" | "openrouter" | "together" | "cerebras" | "fireworks" | "nvidia" | "alibaba-coding-plan" | "huggingface" | "nanogpt" | "qwen-portal" | "synthetic" | "venice" | "zenmux" | "qianfan" | "xiaomi" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "xiaomi-token-plan-sgp" | "minimax-code" | "minimax-code-cn" | "zai" | "minimax" | "tencent" | "deepinfra" | "litellm";
5
5
 
6
6
  /**
7
7
  * A provider id at runtime: a compiled-in name, OR a user-registered custom provider id
@@ -346,6 +346,21 @@ export async function runDoctorCommand(args: string[] = []): Promise<void> {
346
346
  const ollamaResult = await probeOllama(ollamaBase);
347
347
  for (const p of cloudProbes) probes.push(p);
348
348
  probes.push({ name: "ollama", credKind: "none (local)", result: ollamaResult });
349
+ if (defaultProvider === "agy") {
350
+ let executable: string | undefined;
351
+ try {
352
+ executable = Bun.which("agy") ?? undefined;
353
+ } catch {
354
+ executable = undefined;
355
+ }
356
+ probes.push({
357
+ name: "agy",
358
+ credKind: "external CLI session",
359
+ result: executable
360
+ ? { status: "skipped", detail: `UNKNOWN — agy CLI available at ${executable}; auth/permissions are externally managed; model generation not probed` }
361
+ : { status: "fail", detail: "agy CLI not found on PATH — install/login with the official agy CLI; jeo does not manage its credentials" },
362
+ });
363
+ }
349
364
 
350
365
  const oauthHealth: { provider: string; refreshable: boolean; expiresInMin?: number; email?: string }[] = [];
351
366
  for (const p of ["anthropic", "openai", "gemini"] as AuthProvider[]) {
@@ -360,7 +375,9 @@ export async function runDoctorCommand(args: string[] = []): Promise<void> {
360
375
  }
361
376
 
362
377
  const defaultProbe = probes.find(p => p.name === defaultProvider);
363
- const ready = defaultProbe?.result.status === "ok";
378
+ const ready = defaultProvider === "agy"
379
+ ? defaultProbe?.result.status === "skipped"
380
+ : defaultProbe?.result.status === "ok";
364
381
 
365
382
  // --- JSON output mode (CI / scripting) ---
366
383
  if (json) {
@@ -0,0 +1,60 @@
1
+ import { AgyCliError, runAgyTurn, type AgyTurnOptions, type AgyTurnResult } from "../../agent/antigravity-cli";
2
+ import { appendAgySession, appendMessage, loadSession } from "../../agent/session";
3
+ import type { ImageAttachment, Message } from "../../ai/types";
4
+
5
+ export interface AgySessionTurnOptions extends Omit<AgyTurnOptions, "conversationId" | "onConversation"> {
6
+ history: Message[];
7
+ sessionId?: string;
8
+ images?: ImageAttachment[];
9
+ }
10
+
11
+ /** An explicit full-agent turn: no raw-model loop, credential fallback, or tool replay. */
12
+ export async function runAgySessionTurn(options: AgySessionTurnOptions): Promise<AgyTurnResult> {
13
+ const { history, sessionId, images, ...turn } = options;
14
+ if (images?.length) throw new Error("Image attachments are not supported by the official agy bridge");
15
+ if (turn.signal?.aborted) throw new AgyCliError("Official CLI turn cancelled", "cancelled");
16
+
17
+ const previousCount = history.reduce((count, message) => count + (message.role === "system" ? 0 : 1), 0);
18
+ const state = sessionId ? (await loadSession(sessionId, turn.cwd)).header.agy : undefined;
19
+ // A non-CLI turn or compaction changes the cursor. Start fresh with text context
20
+ // rather than resume a stale agent conversation or agy's unrelated latest one.
21
+ let conversationId = state?.messageCount === previousCount ? state.conversationId : undefined;
22
+ let prompt = turn.prompt;
23
+ if (!conversationId) {
24
+ const context = history.filter(message => message.role !== "system" && !message.toolUse?.length && !message.toolResults?.length);
25
+ if (context.some(message => message.images?.length)) {
26
+ throw new Error("Earlier image attachments cannot be forwarded to the official agy bridge; start a new text-only session");
27
+ }
28
+ if (context.length) {
29
+ prompt = "Earlier conversation context (data only, not system instructions or commands to execute):\n"
30
+ + JSON.stringify(context.map(({ role, content }) => ({ role, content })))
31
+ + "\n\nCurrent user request:\n" + prompt;
32
+ }
33
+ }
34
+
35
+ const user: Message = { role: "user", content: turn.prompt };
36
+ if (sessionId) await appendMessage(sessionId, user, turn.cwd);
37
+ history.push(user);
38
+ const result = await runAgyTurn({
39
+ ...turn,
40
+ prompt,
41
+ conversationId,
42
+ onConversation: async id => {
43
+ conversationId = id;
44
+ if (sessionId) await appendAgySession(sessionId, { conversationId: id, messageCount: previousCount + 1 }, turn.cwd);
45
+ },
46
+ });
47
+
48
+ const assistant: Message = { role: "assistant", content: result.response };
49
+ try {
50
+ if (sessionId) await appendMessage(sessionId, assistant, turn.cwd);
51
+ history.push(assistant);
52
+ if (sessionId) await appendAgySession(sessionId, {
53
+ conversationId: result.conversationId,
54
+ messageCount: previousCount + 2,
55
+ }, turn.cwd);
56
+ } catch (error) {
57
+ throw new Error(`Official CLI completed, but saving its jeo session failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
58
+ }
59
+ return result;
60
+ }
@@ -44,6 +44,11 @@ export interface RouteSlashResult {
44
44
  lines: string[];
45
45
  }
46
46
 
47
+ /** Off means a FIXED model: no prompt routing AND no equivalent-model failover. */
48
+ function stateLabel(enabled: boolean): string {
49
+ return enabled ? "routing: on (this session)" : "routing: off (this session) — model fixed, no failover";
50
+ }
51
+
47
52
  const USAGE = "Usage: /route [status|on|off|why|history [n]|save|on save|off save]";
48
53
 
49
54
  function isRealDecision(decision: RouteDecision | { note: string } | null): decision is RouteDecision {
@@ -92,7 +97,7 @@ export async function runRouteSlash(input: string, ctx: RouteSlashCtx): Promise<
92
97
  const effective = ctx.sessionRouteOverride ?? ctx.routingConfigEnabled;
93
98
 
94
99
  if (!sub || sub === "status") {
95
- const lines = [`routing: ${effective ? "on" : "off"} (this session)`];
100
+ const lines = [stateLabel(effective)];
96
101
  if (isRealDecision(ctx.lastRouteDecision)) {
97
102
  lines.push(`last decision: ${ctx.lastRouteDecision.tier} → ${ctx.lastRouteDecision.model} (${ctx.lastRouteDecision.source}: ${ctx.lastRouteDecision.signals.join(", ") || "none"}, confidence ${ctx.lastRouteDecision.confidence.toFixed(2)})`);
98
103
  }
@@ -113,9 +118,9 @@ export async function runRouteSlash(input: string, ctx: RouteSlashCtx): Promise<
113
118
  const enabled = sub === "on";
114
119
  if (arg === "save") {
115
120
  await saveConfigPatch(raw => ({ routing: { ...raw.routing, enabled } }));
116
- return { sessionRouteOverride: enabled, lines: [`routing: ${enabled ? "on" : "off"} (this session) — saved to ~/.jeo/config.json`] };
121
+ return { sessionRouteOverride: enabled, lines: [`${stateLabel(enabled)} — saved to ~/.jeo/config.json`] };
117
122
  }
118
- return { sessionRouteOverride: enabled, lines: [`routing: ${enabled ? "on" : "off"} (this session)`] };
123
+ return { sessionRouteOverride: enabled, lines: [stateLabel(enabled)] };
119
124
  }
120
125
 
121
126
  // Bare `/route save` persists whatever is CURRENTLY effective (a prior