jeo-code 0.1.0 → 0.4.4

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.
Files changed (177) hide show
  1. package/README.ja.md +160 -0
  2. package/README.ko.md +160 -0
  3. package/README.md +115 -297
  4. package/README.zh.md +160 -0
  5. package/package.json +11 -6
  6. package/scripts/install.sh +28 -28
  7. package/scripts/uninstall.sh +17 -15
  8. package/src/AGENTS.md +50 -0
  9. package/src/agent/AGENTS.md +49 -0
  10. package/src/agent/bash-fixups.ts +103 -0
  11. package/src/agent/compaction.ts +410 -19
  12. package/src/agent/config-schema.ts +119 -5
  13. package/src/agent/context-files.ts +314 -17
  14. package/src/agent/dev/AGENTS.md +36 -0
  15. package/src/agent/dev/advanced-analyzer.ts +12 -0
  16. package/src/agent/dev/evolution-bridge.ts +82 -0
  17. package/src/agent/dev/evolution-logger.ts +41 -0
  18. package/src/agent/dev/self-analysis.ts +64 -0
  19. package/src/agent/dev/self-improve.ts +24 -0
  20. package/src/agent/dev/spec-automation.ts +49 -0
  21. package/src/agent/engine.ts +804 -54
  22. package/src/agent/hooks.ts +273 -0
  23. package/src/agent/loop.ts +21 -1
  24. package/src/agent/memory.ts +201 -0
  25. package/src/agent/model-recency.ts +32 -0
  26. package/src/agent/output-minimizer.ts +108 -0
  27. package/src/agent/output-util.ts +64 -0
  28. package/src/agent/plan.ts +187 -0
  29. package/src/agent/seed.ts +52 -0
  30. package/src/agent/session.ts +235 -21
  31. package/src/agent/state.ts +286 -39
  32. package/src/agent/step-budget.ts +232 -0
  33. package/src/agent/subagents.ts +223 -26
  34. package/src/agent/task-tool.ts +272 -0
  35. package/src/agent/todo-tool.ts +87 -0
  36. package/src/agent/tokenizer.ts +117 -0
  37. package/src/agent/tool-registry.ts +54 -0
  38. package/src/agent/tools.ts +562 -103
  39. package/src/agent/web-search.ts +538 -0
  40. package/src/ai/AGENTS.md +44 -0
  41. package/src/ai/index.ts +1 -0
  42. package/src/ai/model-catalog-compat.ts +3 -1
  43. package/src/ai/model-catalog.ts +74 -9
  44. package/src/ai/model-discovery.ts +215 -17
  45. package/src/ai/model-manager.ts +346 -32
  46. package/src/ai/model-picker.ts +1 -1
  47. package/src/ai/model-registry.ts +4 -2
  48. package/src/ai/pricing.ts +84 -0
  49. package/src/ai/provider-registry.ts +23 -0
  50. package/src/ai/provider-status.ts +60 -16
  51. package/src/ai/providers/AGENTS.md +42 -0
  52. package/src/ai/providers/anthropic.ts +250 -31
  53. package/src/ai/providers/antigravity.ts +219 -0
  54. package/src/ai/providers/errors.ts +15 -1
  55. package/src/ai/providers/gemini.ts +196 -13
  56. package/src/ai/providers/ollama.ts +37 -7
  57. package/src/ai/providers/openai-responses.ts +173 -0
  58. package/src/ai/providers/openai.ts +64 -12
  59. package/src/ai/sse.ts +4 -1
  60. package/src/ai/types.ts +18 -1
  61. package/src/auth/AGENTS.md +41 -0
  62. package/src/auth/callback-server.ts +6 -1
  63. package/src/auth/flows/AGENTS.md +32 -0
  64. package/src/auth/flows/antigravity.ts +151 -0
  65. package/src/auth/flows/google-project.ts +190 -0
  66. package/src/auth/flows/google.ts +39 -18
  67. package/src/auth/flows/index.ts +15 -5
  68. package/src/auth/flows/openai.ts +2 -2
  69. package/src/auth/oauth.ts +8 -0
  70. package/src/auth/refresh.ts +44 -27
  71. package/src/auth/storage.ts +149 -26
  72. package/src/auth/types.ts +1 -1
  73. package/src/autopilot.ts +362 -0
  74. package/src/bun-imports.d.ts +4 -0
  75. package/src/cli/AGENTS.md +39 -0
  76. package/src/cli/runner.ts +148 -14
  77. package/src/cli.ts +13 -4
  78. package/src/commands/AGENTS.md +40 -0
  79. package/src/commands/approve.ts +62 -3
  80. package/src/commands/auth.ts +167 -25
  81. package/src/commands/chat.ts +37 -8
  82. package/src/commands/deep-interview.ts +633 -175
  83. package/src/commands/doctor.ts +84 -37
  84. package/src/commands/evolve-core.ts +18 -0
  85. package/src/commands/evolve.ts +2 -1
  86. package/src/commands/export.ts +176 -0
  87. package/src/commands/gjc.ts +52 -0
  88. package/src/commands/launch.ts +3549 -240
  89. package/src/commands/mcp.ts +3 -3
  90. package/src/commands/ooo-seed.ts +19 -0
  91. package/src/commands/ralplan.ts +253 -35
  92. package/src/commands/resume.ts +1 -1
  93. package/src/commands/session.ts +183 -0
  94. package/src/commands/setup-helpers.ts +10 -3
  95. package/src/commands/setup.ts +57 -16
  96. package/src/commands/skills.ts +78 -18
  97. package/src/commands/state.ts +198 -0
  98. package/src/commands/status.ts +84 -0
  99. package/src/commands/team.ts +340 -212
  100. package/src/commands/ultragoal.ts +122 -61
  101. package/src/commands/update.ts +244 -0
  102. package/src/ledger.ts +270 -0
  103. package/src/mcp/AGENTS.md +38 -0
  104. package/src/mcp/server.ts +115 -14
  105. package/src/mcp/tools.ts +42 -22
  106. package/src/md-modules.d.ts +4 -0
  107. package/src/prompts/AGENTS.md +41 -0
  108. package/src/prompts/agents/AGENTS.md +35 -0
  109. package/src/prompts/agents/architect.md +35 -0
  110. package/src/prompts/agents/critic.md +37 -0
  111. package/src/prompts/agents/executor.md +36 -0
  112. package/src/prompts/agents/planner.md +37 -0
  113. package/src/prompts/skills/AGENTS.md +36 -0
  114. package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
  115. package/src/prompts/skills/deep-dive/SKILL.md +13 -0
  116. package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
  117. package/src/prompts/skills/deep-interview/SKILL.md +12 -0
  118. package/src/prompts/skills/gjc/AGENTS.md +31 -0
  119. package/src/prompts/skills/gjc/SKILL.md +15 -0
  120. package/src/prompts/skills/ralplan/AGENTS.md +31 -0
  121. package/src/prompts/skills/ralplan/SKILL.md +11 -0
  122. package/src/prompts/skills/team/AGENTS.md +31 -0
  123. package/src/prompts/skills/team/SKILL.md +11 -0
  124. package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
  125. package/src/prompts/skills/ultragoal/SKILL.md +11 -0
  126. package/src/skills/AGENTS.md +38 -0
  127. package/src/skills/catalog.ts +565 -31
  128. package/src/tui/AGENTS.md +43 -0
  129. package/src/tui/app.ts +1181 -92
  130. package/src/tui/components/AGENTS.md +42 -0
  131. package/src/tui/components/ascii-art.ts +257 -15
  132. package/src/tui/components/autocomplete.ts +98 -16
  133. package/src/tui/components/autopilot-status.ts +65 -0
  134. package/src/tui/components/category-index.ts +49 -0
  135. package/src/tui/components/code-view.ts +54 -11
  136. package/src/tui/components/color.ts +171 -2
  137. package/src/tui/components/config-panel.ts +82 -15
  138. package/src/tui/components/duration.ts +38 -0
  139. package/src/tui/components/evolution.ts +3 -3
  140. package/src/tui/components/footer.ts +91 -42
  141. package/src/tui/components/forge.ts +426 -31
  142. package/src/tui/components/hints.ts +54 -0
  143. package/src/tui/components/hud.ts +73 -0
  144. package/src/tui/components/index.ts +4 -0
  145. package/src/tui/components/input-box.ts +150 -0
  146. package/src/tui/components/layout.ts +11 -3
  147. package/src/tui/components/live-model-picker.ts +108 -0
  148. package/src/tui/components/markdown-table.ts +140 -0
  149. package/src/tui/components/markdown-text.ts +97 -0
  150. package/src/tui/components/meter.ts +4 -1
  151. package/src/tui/components/model-picker.ts +3 -2
  152. package/src/tui/components/provider-picker.ts +3 -2
  153. package/src/tui/components/section.ts +70 -0
  154. package/src/tui/components/select-list.ts +40 -10
  155. package/src/tui/components/skill-picker.ts +25 -0
  156. package/src/tui/components/slash.ts +244 -21
  157. package/src/tui/components/status.ts +272 -11
  158. package/src/tui/components/step-timeline.ts +218 -0
  159. package/src/tui/components/stream.ts +26 -9
  160. package/src/tui/components/themes.ts +212 -6
  161. package/src/tui/components/todo-card.ts +47 -0
  162. package/src/tui/components/tool-list.ts +58 -12
  163. package/src/tui/components/transcript.ts +120 -0
  164. package/src/tui/components/update-box.ts +31 -0
  165. package/src/tui/components/welcome.ts +162 -0
  166. package/src/tui/components/width.ts +163 -0
  167. package/src/tui/monitoring/AGENTS.md +31 -0
  168. package/src/tui/monitoring/hud-view.ts +55 -0
  169. package/src/tui/renderer.ts +112 -3
  170. package/src/tui/terminal.ts +40 -33
  171. package/src/util/AGENTS.md +39 -0
  172. package/src/util/clipboard-image.ts +118 -0
  173. package/src/util/env.ts +12 -0
  174. package/src/util/provider-error.ts +78 -0
  175. package/src/util/retry.ts +91 -6
  176. package/src/util/update-check.ts +64 -0
  177. package/src/commands/models.ts +0 -104
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Static model catalog — capability metadata for well-known public models, so
3
3
  * the TUI can show context window, max output, supported thinking levels, and
4
- * image support next to a model (gjc `--list-models` design parity, reimplemented
5
- * in joc's own structure). This is factual capability data about public models,
6
- * not a copy of any vendor's catalog source. Live discovery
4
+ * image support next to a model. This is factual capability data about public
5
+ * models, not a copy of any vendor's catalog source. Live discovery
7
6
  * (`model-discovery.ts`) remains the source of truth for *availability*; this
8
7
  * catalog annotates known ids with capabilities.
9
8
  */
@@ -14,7 +13,7 @@ export type ThinkLevel = "minimal" | "low" | "medium" | "high" | "xhigh";
14
13
  export const THINK_LEVELS: readonly ThinkLevel[] = ["minimal", "low", "medium", "high", "xhigh"];
15
14
 
16
15
  export interface CatalogModel {
17
- /** joc-facing canonical id (what a user types). */
16
+ /** jeo-facing canonical id (what a user types). */
18
17
  canonical: string;
19
18
  provider: ProviderName;
20
19
  /** Exact provider model id used on the wire. */
@@ -27,19 +26,39 @@ export interface CatalogModel {
27
26
  thinking: ThinkLevel[];
28
27
  /** Whether the model accepts image input. */
29
28
  images: boolean;
29
+ /** Optional company override. */
30
+ company?: string;
30
31
  }
31
32
 
32
33
  const FULL: ThinkLevel[] = ["minimal", "low", "medium", "high", "xhigh"];
33
34
  const STD: ThinkLevel[] = ["minimal", "low", "medium", "high"];
34
35
 
36
+ export const ANTIGRAVITY_MODELS = [
37
+ "claude-opus-4-5-thinking",
38
+ "claude-opus-4-6-thinking",
39
+ "claude-sonnet-4-5",
40
+ "claude-sonnet-4-5-thinking",
41
+ "claude-sonnet-4-6",
42
+ "claude-sonnet-4-6-thinking",
43
+ "gemini-2.5-flash",
44
+ "gemini-2.5-flash-thinking",
45
+ "gemini-2.5-pro",
46
+ "gemini-3-flash",
47
+ "gemini-3-pro-high",
48
+ "gemini-3-pro-low",
49
+ "gemini-3.1-pro-high",
50
+ "gemini-3.1-pro-low",
51
+ "gpt-oss-120b-medium",
52
+ ] as const;
53
+
35
54
  /** A curated set of common public models with their documented capabilities. */
36
55
  export const MODEL_CATALOG: readonly CatalogModel[] = [
37
56
  // Anthropic
38
- { canonical: "claude-3-5-haiku", provider: "anthropic", providerModel: "claude-3-5-haiku-latest", contextTokens: 200_000, maxOutputTokens: 8_192, thinking: [], images: true },
39
- { canonical: "claude-3-5-sonnet", provider: "anthropic", providerModel: "claude-3-5-sonnet-20241022", contextTokens: 200_000, maxOutputTokens: 8_192, thinking: [], images: true },
40
- { canonical: "claude-3-7-sonnet", provider: "anthropic", providerModel: "claude-3-7-sonnet-20250219", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
41
- { canonical: "claude-sonnet-4", provider: "anthropic", providerModel: "claude-sonnet-4-20250514", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
42
- { canonical: "claude-opus-4", provider: "anthropic", providerModel: "claude-opus-4-20250514", contextTokens: 200_000, maxOutputTokens: 32_000, thinking: FULL, images: true },
57
+ { canonical: "claude-3-5-sonnet", provider: "anthropic", providerModel: "claude-3-5-sonnet-20241022", contextTokens: 200_000, maxOutputTokens: 8192, thinking: [], images: true },
58
+ { canonical: "claude-haiku-4-5", provider: "anthropic", providerModel: "claude-haiku-4-5-20251001", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
59
+ { canonical: "claude-sonnet-4-5", provider: "anthropic", providerModel: "claude-sonnet-4-5-20250929", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
60
+ { canonical: "claude-opus-4-1", provider: "anthropic", providerModel: "claude-opus-4-1-20250805", contextTokens: 200_000, maxOutputTokens: 32_000, thinking: FULL, images: true },
61
+ { canonical: "claude-opus-4-5", provider: "anthropic", providerModel: "claude-opus-4-5-20251101", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
43
62
  // OpenAI
44
63
  { canonical: "gpt-4o", provider: "openai", providerModel: "gpt-4o", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: true },
45
64
  { canonical: "gpt-4o-mini", provider: "openai", providerModel: "gpt-4o-mini", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: true },
@@ -47,15 +66,37 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
47
66
  { canonical: "o3", provider: "openai", providerModel: "o3", contextTokens: 200_000, maxOutputTokens: 100_000, thinking: STD, images: true },
48
67
  { canonical: "o3-mini", provider: "openai", providerModel: "o3-mini", contextTokens: 200_000, maxOutputTokens: 100_000, thinking: STD, images: false },
49
68
  { canonical: "o4-mini", provider: "openai", providerModel: "o4-mini", contextTokens: 200_000, maxOutputTokens: 100_000, thinking: STD, images: true },
69
+ { canonical: "gpt-5.5", provider: "openai", providerModel: "gpt-5.5", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true },
70
+ { canonical: "gpt-5.4", provider: "openai", providerModel: "gpt-5.4", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true },
50
71
  // Google
51
72
  { canonical: "gemini-1.5-pro", provider: "gemini", providerModel: "gemini-1.5-pro", contextTokens: 1_000_000, maxOutputTokens: 8_192, thinking: [], images: true },
52
73
  { canonical: "gemini-2.0-flash", provider: "gemini", providerModel: "gemini-2.0-flash", contextTokens: 1_000_000, maxOutputTokens: 8_192, thinking: [], images: true },
53
74
  { canonical: "gemini-2.5-flash", provider: "gemini", providerModel: "gemini-2.5-flash", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: STD, images: true },
54
75
  { canonical: "gemini-2.5-pro", provider: "gemini", providerModel: "gemini-2.5-pro", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: STD, images: true },
76
+ // Google Antigravity / Gemini CLI (Cloud Code Assist) — provider-qualified to avoid
77
+ // collisions with public Gemini, Anthropic, and OpenAI/Codex ids.
78
+ ...ANTIGRAVITY_MODELS.map((id): CatalogModel => ({
79
+ canonical: `antigravity/${id}`,
80
+ provider: "antigravity",
81
+ providerModel: id,
82
+ contextTokens: id.includes("claude") ? 200_000 : id.includes("gemini-3") ? 1_000_000 : 1_000_000,
83
+ maxOutputTokens: id.includes("claude") ? 64_000 : 65_536,
84
+ thinking: id.includes("thinking") || id.includes("-high") || id.includes("-low") || id.includes("gemini-3") ? FULL : STD,
85
+ images: !id.includes("gpt-oss"),
86
+ company: id.includes("claude") ? "Anthropic via Antigravity" : id.includes("gpt") ? "OpenAI via Antigravity" : "Google Antigravity",
87
+ })),
55
88
  // Ollama (local)
56
89
  { canonical: "qwen2.5", provider: "ollama", providerModel: "ollama/qwen2.5:0.5b", contextTokens: 32_768, maxOutputTokens: 8_192, thinking: [], images: false },
57
90
  ];
58
91
 
92
+ /**
93
+ * OpenAI models the ChatGPT/Codex subscription backend (`codex/responses`) actually
94
+ * serves. The Codex backend rejects standard API ids (gpt-4o, o3, …) and exposes no
95
+ * usable list endpoint, so an OAuth-only OpenAI login surfaces exactly these instead
96
+ * of the full chat-completions catalog. Verified live against a ChatGPT account.
97
+ */
98
+ export const CODEX_MODELS: readonly string[] = ["gpt-5.5", "gpt-5.4"];
99
+
59
100
  /** Format a token count compactly (1000 → 1K, 1_000_000 → 1M). */
60
101
  export function formatTokens(n: number): string {
61
102
  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`;
@@ -69,6 +110,18 @@ export function findCatalogModel(idOrModel: string): CatalogModel | undefined {
69
110
  return MODEL_CATALOG.find(m => m.canonical === q || m.providerModel === q || `${m.provider}/${m.providerModel}` === q);
70
111
  }
71
112
 
113
+ /**
114
+ * Map a user-facing canonical id to the exact provider model id used on the wire
115
+ * (e.g. `claude-3-5-sonnet` → `claude-3-5-sonnet-20241022`). Ids that are not a
116
+ * known canonical (already a provider id, a live-discovered id, an alias target)
117
+ * are returned unchanged. Scope to `provider` when known so a canonical never
118
+ * leaks across providers.
119
+ */
120
+ export function toProviderModel(id: string, provider?: ProviderName): string {
121
+ const m = MODEL_CATALOG.find(c => c.canonical === id && (!provider || c.provider === provider));
122
+ return m ? m.providerModel : id;
123
+ }
124
+
72
125
  /** Case-insensitive substring match over canonical + provider model id. */
73
126
  export function fuzzyMatchCatalog(query: string): CatalogModel[] {
74
127
  const q = query.trim().toLowerCase();
@@ -95,3 +148,15 @@ export function supportsThinking(modelId: string, level: ThinkLevel): boolean {
95
148
  const meta = catalogMetadata(modelId);
96
149
  return meta ? meta.thinking.includes(level) : false;
97
150
  }
151
+ export function companyLabel(provider: string, entry?: { company?: string }): string {
152
+ if (entry?.company) {
153
+ return entry.company;
154
+ }
155
+ const low = provider.toLowerCase();
156
+ if (low === "anthropic") return "Anthropic";
157
+ if (low === "openai") return "OpenAI";
158
+ if (low === "gemini") return "Google";
159
+ if (low === "ollama") return "Ollama";
160
+ if (low === "antigravity") return "Antigravity";
161
+ return provider.charAt(0).toUpperCase() + provider.slice(1);
162
+ }
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Live model discovery — query a provider's `models` endpoint with the resolved
3
3
  * credential (OAuth bearer or API key) and return the model ids the account can
4
- * actually use. This powers the TUI `/models` / `/model` / `/provider` flows and
5
- * `joc models`, so users pick from the real, logged-in catalog instead of a
6
- * static alias guess.
4
+ * actually use. This powers the TUI `/model` and `/provider` flows, so users
5
+ * pick from the real, logged-in catalog instead of a static alias guess.
7
6
  *
8
7
  * Network access is injectable (`fetchImpl`) and every call is timeout-bounded so
9
8
  * the TUI never hangs; failures degrade to a tagged result, never a throw.
@@ -12,6 +11,8 @@ import { readGlobalConfig, type Config } from "../agent/state";
12
11
  import { resolveCredential, type AuthProvider, type Credential } from "../auth";
13
12
  import type { ProviderName } from "./types";
14
13
  import { PROVIDER_NAMES } from "./provider-status";
14
+ import { catalogByProvider, CODEX_MODELS } from "./model-catalog";
15
+ import { extractChatgptAccountId } from "./providers/openai-responses";
15
16
 
16
17
  export interface ProviderModelsResult {
17
18
  provider: ProviderName;
@@ -22,6 +23,8 @@ export interface ProviderModelsResult {
22
23
  source: "oauth" | "api_key" | "keyless" | "none";
23
24
  /** Present on failure: a short, human-readable reason. */
24
25
  error?: string;
26
+ /** True when the live endpoint was unusable and ids came from the static catalog. */
27
+ fallback?: boolean;
25
28
  }
26
29
 
27
30
  export interface DiscoveryOptions {
@@ -33,10 +36,26 @@ export interface DiscoveryOptions {
33
36
  signal?: AbortSignal;
34
37
  /** Cap the number of returned ids per provider; default 100. */
35
38
  limit?: number;
39
+ /** Config snapshot used for provider base URLs. */
40
+ config?: Config;
36
41
  }
37
42
 
38
43
  const DEFAULT_TIMEOUT = 5000;
39
44
  const DEFAULT_LIMIT = 100;
45
+ // The Codex models endpoint REQUIRES `client_version` (HTTP 400 without it) and
46
+ // GATES the list by it — old versions get `{"models":[]}`. Keep this high enough
47
+ // to receive the full current list (verified live 2026-06-12: 0.46→[], 0.99→gpt-5.4,
48
+ // 1.0/2.0→full gpt-5.5 set). On drift the catalog fallback keeps Codex usable.
49
+ const CODEX_CLIENT_VERSION = "2.0.0";
50
+ const CODEX_MODELS_URL = `https://chatgpt.com/backend-api/codex/models?client_version=${CODEX_CLIENT_VERSION}`;
51
+ const ANTIGRAVITY_MODELS_URL = "https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels";
52
+ const ANTIGRAVITY_MODEL_DENYLIST = new Set([
53
+ "chat_20706",
54
+ "chat_23310",
55
+ "gemini-2.5-flash-thinking",
56
+ "gemini-3-pro-low",
57
+ "gemini-2.5-pro",
58
+ ]);
40
59
 
41
60
  function anthropicHeaders(cred: Credential): Record<string, string> {
42
61
  if (cred.kind === "oauth") {
@@ -48,28 +67,55 @@ function anthropicHeaders(cred: Credential): Record<string, string> {
48
67
  return {};
49
68
  }
50
69
 
70
+ function authProviderFor(provider: ProviderName): AuthProvider | undefined {
71
+ if (provider === "ollama") return undefined;
72
+ return provider;
73
+ }
74
+
51
75
  /** Build the discovery request (url + headers) for a provider/credential. */
52
76
  export function discoveryRequest(
53
77
  provider: ProviderName,
54
78
  cred: Credential | undefined,
55
79
  baseUrl?: string,
56
- ): { url: string; headers: Record<string, string> } {
80
+ ): { url: string; headers: Record<string, string>; method?: "GET" | "POST"; body?: string } {
57
81
  switch (provider) {
58
82
  case "anthropic":
59
83
  return { url: "https://api.anthropic.com/v1/models", headers: anthropicHeaders(cred!) };
60
84
  case "openai": {
61
- const base = (baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
62
85
  const token = cred?.kind === "oauth" || cred?.kind === "api_key" ? cred.token : "";
86
+ if (cred?.kind === "oauth" && !baseUrl && !process.env.OPENAI_BASE_URL) {
87
+ const accountId = extractChatgptAccountId(token);
88
+ const headers: Record<string, string> = {
89
+ Authorization: `Bearer ${token}`,
90
+ "OpenAI-Beta": "responses=experimental",
91
+ originator: "codex_cli_rs",
92
+ };
93
+ if (accountId) headers["chatgpt-account-id"] = accountId;
94
+ return { url: CODEX_MODELS_URL, headers };
95
+ }
96
+ const base = (baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
63
97
  return { url: `${base}/models`, headers: token ? { Authorization: `Bearer ${token}` } : {} };
64
98
  }
65
99
  case "gemini": {
66
100
  const oauth = cred?.kind === "oauth" ? cred.token : undefined;
67
101
  const apiKey = cred?.kind === "api_key" ? cred.token : undefined;
102
+ // pageSize=1000: the DEFAULT page is 50 models WITH a nextPageToken — the
103
+ // single-shot fetch silently dropped everything past page 1 (verified live:
104
+ // 50+token vs 55 total). listProviderModels also follows nextPageToken.
68
105
  const url = oauth
69
- ? "https://generativelanguage.googleapis.com/v1beta/models"
70
- : `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey ?? ""}`;
106
+ ? "https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000"
107
+ : `https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000&key=${apiKey ?? ""}`;
71
108
  return { url, headers: oauth ? { authorization: `Bearer ${oauth}` } : {} };
72
109
  }
110
+ case "antigravity": {
111
+ const token = cred?.kind === "oauth" ? cred.token : "";
112
+ return {
113
+ url: ANTIGRAVITY_MODELS_URL,
114
+ headers: token ? { authorization: `Bearer ${token}`, "content-type": "application/json", "User-Agent": "antigravity/1.104.0" } : {},
115
+ method: "POST",
116
+ body: "{}",
117
+ };
118
+ }
73
119
  case "ollama": {
74
120
  const base = (baseUrl ?? "http://localhost:11434").replace(/\/$/, "");
75
121
  return { url: `${base}/api/tags`, headers: {} };
@@ -77,17 +123,123 @@ export function discoveryRequest(
77
123
  }
78
124
  }
79
125
 
80
- /** Parse a provider's models response body into normalized model ids. */
126
+ /**
127
+ * OpenAI `/v1/models` lists every model family — embeddings, audio/tts, image, moderation,
128
+ * realtime — but jeo only calls chat/completions. Drop the families that can never serve a
129
+ * chat turn so pickers never offer a model that fails at call time.
130
+ */
131
+ function isOpenAiChatModel(id: string): boolean {
132
+ return !/(^|[-/])(text-embedding|embedding|tts|whisper|dall-e|moderation|omni-moderation|davinci|babbage|computer-use|realtime|audio|image|sora|transcribe|instruct|codex)([-/]|$)/i.test(id);
133
+ }
134
+
135
+ /**
136
+ * Gemini exposes generateContent for image/tts/embedding variants too, but those emit
137
+ * audio/image/vectors — not a usable text turn for a coding chat. Drop them by family.
138
+ */
139
+ function isGeminiChatModel(id: string): boolean {
140
+ return !/(^|[-/])(embedding|aqa|tts|image|imagen|veo|lyria|nano-banana|deep-research|computer-use|antigravity)([-/]|$)/i.test(id);
141
+ }
142
+
143
+ type CodexModelRow = { slug?: string; id?: string; supported_in_api?: boolean; priority?: number };
144
+ type AntigravityModelRow = { slug?: string; id?: string; name?: string; isInternal?: boolean; model?: string };
145
+
146
+ /** Parse a provider's models response body into normalized, chat-capable model ids. */
81
147
  export function parseModelsBody(provider: ProviderName, body: unknown): string[] {
82
- const data = body as { data?: { id?: string }[]; models?: { name?: string }[] };
148
+ const data = body as {
149
+ data?: { id?: string }[];
150
+ models?: ({ name?: string; supportedGenerationMethods?: string[] } & CodexModelRow)[];
151
+ };
83
152
  if (provider === "ollama") {
84
153
  return (data.models ?? []).map(m => `ollama/${m.name ?? ""}`).filter(s => s !== "ollama/");
85
154
  }
155
+ if (provider === "antigravity") {
156
+ // fetchAvailableModels keys the map by the CALLABLE model id (e.g.
157
+ // "gemini-3-flash"); the entry's `model` field is an internal enum
158
+ // (MODEL_PLACEHOLDER_*) and must never be surfaced. The response's OWN
159
+ // metadata decides what to show — no hard-coded model lists:
160
+ // 1. `agentModelSorts` groups are the API's positive agent/chat set
161
+ // (exactly what Antigravity's model picker offers) — prefer it.
162
+ // 2. Otherwise fall back to excluding the API's non-chat role lists
163
+ // (tab completion, image generation, transcription, deprecations).
164
+ const payload = body as {
165
+ models?: Record<string, AntigravityModelRow> | AntigravityModelRow[];
166
+ agentModelSorts?: { groups?: { modelIds?: string[] }[] }[];
167
+ tabModelIds?: string[];
168
+ imageGenerationModelIds?: string[];
169
+ audioTranscriptionModelIds?: string[];
170
+ commitMessageModelIds?: string[];
171
+ mqueryModelIds?: string[];
172
+ /** Array of ids OR an object keyed by deprecated id. */
173
+ deprecatedModelIds?: string[] | Record<string, unknown>;
174
+ };
175
+ const roleIds = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : []);
176
+ const deprecated = Array.isArray(payload.deprecatedModelIds)
177
+ ? roleIds(payload.deprecatedModelIds)
178
+ : Object.keys(payload.deprecatedModelIds ?? {});
179
+ const agentIds = new Set(
180
+ (Array.isArray(payload.agentModelSorts) ? payload.agentModelSorts : [])
181
+ .flatMap(sort => (Array.isArray(sort?.groups) ? sort.groups : []))
182
+ .flatMap(group => roleIds(group?.modelIds)),
183
+ );
184
+ const nonChat = new Set([
185
+ ...roleIds(payload.tabModelIds),
186
+ ...roleIds(payload.imageGenerationModelIds),
187
+ ...roleIds(payload.audioTranscriptionModelIds),
188
+ ...roleIds(payload.commitMessageModelIds),
189
+ ...roleIds(payload.mqueryModelIds),
190
+ ...deprecated,
191
+ ]);
192
+ const rawModels = payload.models;
193
+ const ids = Array.isArray(rawModels)
194
+ ? rawModels.map(m => m.slug ?? m.id ?? m.name ?? "").filter(Boolean)
195
+ : Object.entries(rawModels ?? {})
196
+ .filter(([id, model]) =>
197
+ !ANTIGRAVITY_MODEL_DENYLIST.has(id) &&
198
+ model?.isInternal !== true &&
199
+ (agentIds.size > 0 ? agentIds.has(id) : !nonChat.has(id)))
200
+ .map(([id]) => id);
201
+ return ids
202
+ .map(id => id.replace(/^models\//, ""))
203
+ .filter(Boolean)
204
+ .map(id => id.startsWith("antigravity/") ? id : `antigravity/${id}`);
205
+ }
86
206
  if (provider === "gemini") {
87
- return (data.models ?? []).map(m => (m.name ?? "").replace(/^models\//, "")).filter(Boolean);
207
+ // Keep only models the generateContent endpoint can serve (skip embeddings/tts/aqa/etc).
208
+ // When the list omits supportedGenerationMethods, keep the id (be permissive).
209
+ return (data.models ?? [])
210
+ .filter(m => !m.supportedGenerationMethods || m.supportedGenerationMethods.includes("generateContent"))
211
+ .map(m => (m.name ?? "").replace(/^models\//, ""))
212
+ .filter(id => id && isGeminiChatModel(id));
213
+ }
214
+ if (provider === "openai" && data.models?.some(m => m.slug || m.id)) {
215
+ return data.models
216
+ .filter(m => m.supported_in_api !== false)
217
+ .map(m => m.slug ?? m.id ?? "")
218
+ // Review-only entries (e.g. codex-auto-review) are not chat-turn models.
219
+ .filter(id => id && !/(^|[-/])auto-review([-/]|$)/i.test(id));
88
220
  }
89
221
  // anthropic / openai: { data: [{ id }] }
90
- return (data.data ?? []).map(m => m.id ?? "").filter(Boolean);
222
+ const ids = (data.data ?? []).map(m => m.id ?? "").filter(Boolean);
223
+ return provider === "openai" ? ids.filter(isOpenAiChatModel) : ids;
224
+ }
225
+
226
+ /**
227
+ * When a provider is authenticated (oauth/api_key) but the live `models` endpoint
228
+ * is unusable — e.g. ChatGPT/Codex OAuth tokens are rejected by `api.openai.com/v1/models`
229
+ * — surface the static catalog ids so the provider's models still appear in pickers.
230
+ * Keyless/not-logged-in results are returned unchanged.
231
+ */
232
+ export function catalogOr(result: ProviderModelsResult): ProviderModelsResult {
233
+ if (result.ok && result.models.length > 0) return result;
234
+ // OpenAI/Codex OAuth legitimately rejects the standard /models endpoint while
235
+ // the fixed Codex ids still work for calls. Other OAuth providers may fall
236
+ // back to their static catalog too — EXCEPT Antigravity, whose available
237
+ // models depend on the Cloud Code Assist agent backend and must not be faked.
238
+ if (result.source !== "oauth") return result;
239
+ if (result.provider === "antigravity") return result;
240
+ const ids = result.provider === "openai" ? [...CODEX_MODELS] : catalogByProvider(result.provider).map(m => m.providerModel);
241
+ if (ids.length === 0) return result;
242
+ return { ...result, models: ids, ok: true, fallback: true };
91
243
  }
92
244
 
93
245
  /** Discover the live model list for one provider. Never throws. */
@@ -101,25 +253,68 @@ export async function listProviderModels(
101
253
  let cred: Credential | undefined;
102
254
  let source: ProviderModelsResult["source"] = "keyless";
103
255
  if (provider !== "ollama") {
104
- cred = await resolveCredential(provider as AuthProvider);
256
+ const authProvider = authProviderFor(provider);
257
+ const raw = await resolveCredential(authProvider!);
258
+ cred = raw;
105
259
  source = cred.kind === "oauth" ? "oauth" : cred.kind === "api_key" ? "api_key" : "none";
260
+ const config = opts.config ?? (await readGlobalConfig());
261
+
262
+ if (provider === "antigravity") {
263
+ // Antigravity lists models from the LIVE Cloud Code Assist endpoint
264
+ // (v1internal:fetchAvailableModels) — never from a hard-coded catalog.
265
+ // A gemini-cli OAuth token is tried as a fallback credential for the
266
+ // list call; if the backend rejects it the failure is surfaced honestly.
267
+ if (cred.kind !== "oauth") {
268
+ const gemini = await resolveCredential("gemini");
269
+ if (gemini.kind === "oauth") {
270
+ cred = gemini;
271
+ source = "oauth";
272
+ }
273
+ }
274
+ if (cred.kind !== "oauth") {
275
+ return { provider, models: [], ok: false, source, error: "not logged in with Antigravity OAuth" };
276
+ }
277
+ }
278
+
279
+ const prov = authProvider!;
280
+ // Antigravity's list endpoint accepts ONLY OAuth (the request builder sends
281
+ // no api-key header), so never swap its credential to an api_key.
282
+ if (provider !== "antigravity" && cred.kind === "oauth" && config.providers?.[prov]) {
283
+ // An API key is the broader, documented path — prefer it for live discovery.
284
+ cred = { kind: "api_key", provider: prov, token: config.providers[prov]! };
285
+ source = "api_key";
286
+ }
106
287
  const isLocalOpenAi = provider === "openai" && !!(opts.baseUrl ?? process.env.OPENAI_BASE_URL);
107
288
  if (source === "none" && !isLocalOpenAi) {
108
289
  return { provider, models: [], ok: false, source, error: "not logged in" };
109
290
  }
110
291
  }
111
292
 
112
- const { url, headers } = discoveryRequest(provider, cred, opts.baseUrl);
293
+ const { url, headers, method, body: requestBody } = discoveryRequest(provider, cred, opts.baseUrl);
113
294
  const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT;
114
295
  const signal = opts.signal ?? AbortSignal.timeout(timeout);
115
296
  try {
116
- const res = await fetchImpl(url, { headers, signal });
297
+ const res = await fetchImpl(url, { method: method ?? "GET", headers, body: requestBody, signal });
117
298
  if (!res.ok) {
118
299
  const reason = res.status === 401 || res.status === 403 ? "auth rejected" : `HTTP ${res.status}`;
119
300
  return { provider, models: [], ok: false, source, error: reason };
120
301
  }
121
302
  const body = await res.json();
122
- const models = parseModelsBody(provider, body).sort().slice(0, limit);
303
+ let ids = parseModelsBody(provider, body);
304
+ // Gemini paginates: follow nextPageToken (bounded) so the available list is
305
+ // COMPLETE — page 1 alone silently dropped the newest models (round-15).
306
+ if (provider === "gemini") {
307
+ let pageToken = (body as { nextPageToken?: string })?.nextPageToken;
308
+ for (let page = 0; pageToken && page < 4; page++) {
309
+ const pagedUrl = `${url}&pageToken=${encodeURIComponent(pageToken)}`;
310
+ const pageRes = await fetchImpl(pagedUrl, { method: "GET", headers, signal });
311
+ if (!pageRes.ok) break; // partial list beats a hard failure
312
+ const pageBody = await pageRes.json() as { nextPageToken?: string };
313
+ ids = ids.concat(parseModelsBody(provider, pageBody));
314
+ pageToken = pageBody.nextPageToken;
315
+ }
316
+ }
317
+ const models = [...new Set(ids)].sort().slice(0, limit);
123
318
  return { provider, models, ok: true, source };
124
319
  } catch (err) {
125
320
  const msg = (err as Error)?.name === "TimeoutError" || (err as Error)?.name === "AbortError" ? "timeout" : "unreachable";
@@ -133,16 +328,19 @@ export async function listProviderModels(
133
328
  * probed. Runs in parallel.
134
329
  */
135
330
  export async function discoverModels(
136
- opts: DiscoveryOptions & { providers?: ProviderName[]; config?: Config } = {},
331
+ opts: DiscoveryOptions & { providers?: ProviderName[]; config?: Config; catalogFallback?: boolean } = {},
137
332
  ): Promise<ProviderModelsResult[]> {
138
333
  const cfg = opts.config ?? (await readGlobalConfig());
139
334
  const providers = opts.providers ?? [...PROVIDER_NAMES];
140
- return Promise.all(
335
+ const useFallback = opts.catalogFallback !== false;
336
+ const results = await Promise.all(
141
337
  providers.map(p =>
142
338
  listProviderModels(p, {
143
339
  ...opts,
340
+ config: cfg,
144
341
  baseUrl: p === "ollama" ? (cfg.ollamaBaseUrl ?? opts.baseUrl) : p === "openai" ? (cfg.openaiBaseUrl ?? opts.baseUrl) : opts.baseUrl,
145
342
  }),
146
343
  ),
147
344
  );
345
+ return useFallback ? results.map(catalogOr) : results;
148
346
  }