jeo-code 0.6.22 → 0.6.23

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 (45) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.ja.md +5 -1
  3. package/README.ko.md +5 -1
  4. package/README.md +5 -1
  5. package/README.zh.md +5 -1
  6. package/package.json +1 -1
  7. package/src/agent/config-schema.ts +12 -0
  8. package/src/agent/session.ts +10 -3
  9. package/src/agent/state.ts +19 -14
  10. package/src/ai/index.ts +1 -0
  11. package/src/ai/model-catalog.ts +121 -1
  12. package/src/ai/model-discovery.ts +55 -3
  13. package/src/ai/model-manager.ts +43 -11
  14. package/src/ai/model-registry.ts +2 -0
  15. package/src/ai/provider-status.ts +26 -7
  16. package/src/ai/providers/anthropic-compatible.ts +27 -0
  17. package/src/ai/providers/anthropic.ts +3 -1
  18. package/src/ai/providers/antigravity.ts +31 -6
  19. package/src/ai/providers/gemini.ts +45 -4
  20. package/src/ai/providers/kimi.ts +18 -0
  21. package/src/ai/providers/lmstudio.ts +8 -0
  22. package/src/ai/providers/ollama.ts +17 -5
  23. package/src/ai/providers/openai-compatible-catalog.ts +72 -0
  24. package/src/ai/providers/openai-compatible.ts +31 -0
  25. package/src/ai/providers/openai.ts +23 -7
  26. package/src/ai/providers/xai.ts +18 -0
  27. package/src/ai/register-providers.ts +18 -0
  28. package/src/ai/think-tags.ts +84 -0
  29. package/src/ai/types.ts +6 -1
  30. package/src/auth/flows/index.ts +3 -3
  31. package/src/auth/index.ts +4 -1
  32. package/src/auth/oauth.ts +3 -3
  33. package/src/auth/refresh.ts +5 -0
  34. package/src/auth/storage.ts +12 -1
  35. package/src/commands/auth.ts +19 -2
  36. package/src/commands/launch/flags.ts +5 -1
  37. package/src/commands/launch/input.ts +13 -0
  38. package/src/commands/launch.ts +78 -12
  39. package/src/commands/setup.ts +3 -2
  40. package/src/tui/app.ts +51 -31
  41. package/src/tui/components/ascii-art.ts +11 -7
  42. package/src/tui/components/autocomplete.ts +16 -0
  43. package/src/tui/components/forge.ts +1 -1
  44. package/src/tui/components/transcript.ts +7 -0
  45. package/src/tui/components/width.ts +21 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
+ ## [0.6.23] - 2026-06-19
10
+ _Live reasoning/thinking streams in the TUI across every provider, three new OpenAI-compatible backends (LM Studio, xAI, Kimi) join the auth/discovery/catalog surface, and Gemini gains native function-calling._
11
+
12
+ ### Added
13
+ - **Multi-provider reasoning/thinking streaming in the TUI.** Native reasoning is surfaced live (dimmed) and committed to scrollback for Anthropic (`thinking` deltas), OpenAI Codex/Responses (`reasoning*` deltas), OpenAI-compatible chat (`reasoning_content`/`reasoning`), Gemini & Antigravity (`thought` parts), and Ollama (`message.thinking`). A provider-agnostic `<think>…</think>` splitter routes inline chain-of-thought (DeepSeek-R1/Qwen-style local models) to the reasoning channel so it never pollutes the answer or the tool-call parse.
14
+ - **Three new OpenAI-compatible providers — LM Studio (keyless local), xAI/Grok (`XAI_API_KEY`), and Kimi/Moonshot (`KIMI_API_KEY`).** All route through a shared `makeOpenAICompatibleAdapter` factory and are wired into `/provider`, `jeo auth status/login`, model discovery, and the capability catalog.
15
+ - **Native Gemini function-calling (gjc parity).** Gemini now declares `functionDeclarations` and parses `functionCall` parts instead of the JSON-in-prose protocol — capable models stop fighting the `done` format, cutting wasted steps and stray "apology" prose from replies (verified live: a trivial reply dropped from 3 steps/14s to 1 step/2s).
16
+ - **Mid-turn `/command` and `$skill` dispatch** with a live command/skill preview while typing.
17
+
18
+ ### Changed
19
+ - **API-key providers are first-class in the auth core.** `AuthProvider` now splits into the OAuth-capable subset (`OAuthProvider`) plus API-key-only providers (xai/kimi); these resolve through the standard `resolveCredential` path (`config.providers` / `<NAME>_API_KEY`) and model discovery now sends their key (a prior gap left discovery unauthenticated). `jeo auth login <xai|kimi> --token <key>` stores the API key.
20
+
21
+ ### Fixed
22
+ - **Config-schema dropped a stored `xai` key on validation** (the providers schema was missing `xai`/`kimi`); both are now persisted.
23
+
9
24
  ## [0.6.22] - 2026-06-18
10
25
  _Extended-thinking activation is now consistent across providers: a `low` session thinking level enables reasoning everywhere._
11
26
 
package/README.ja.md CHANGED
@@ -2,6 +2,10 @@
2
2
  <img src="assets/hero.png" alt="jeo-code 自律コーディングエージェントのヒーローイラスト" width="100%" />
3
3
  </p>
4
4
 
5
+ <p align="center">
6
+ <img src="assets/icon.png" alt="jeo-code icon" width="96" />
7
+ </p>
8
+
5
9
  <h1 align="center">jeo-code (jeo)</h1>
6
10
 
7
11
  <p align="center">
@@ -200,11 +204,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
200
204
  ## 変更履歴 (Changelog)
201
205
 
202
206
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
207
+ - **[0.6.23]** (2026-06-19) — Live reasoning/thinking streams in the TUI across every provider, three new OpenAI-compatible backends (LM Studio, xAI, Kimi) join the auth/discovery/catalog surface, and Gemini gains native function-calling.
203
208
  - **[0.6.22]** (2026-06-18) — Extended-thinking activation is now consistent across providers: a `low` session thinking level enables reasoning everywhere.
204
209
  - **[0.6.21]** (2026-06-18) — Session thinking level now reaches the provider's actual reasoning depth, not just the token ceiling.
205
210
  - **[0.6.20]** (2026-06-18) — Launch REPL internals decomposed into testable modules: `@mention` path completion, slash-command view renderers, and slash-command handlers extracted from the monolithic `launch.ts` into dedicated files with full unit-test coverage.
206
211
  - **[0.6.19]** (2026-06-18) — Post-turn hooks run once per batch (not per edit), local hook reads are mtime-cached, tool-result formatting is parallelized, and wrapped colored text keeps its tint.
207
- - **[0.6.18]** (2026-06-17) — Memory data-flow diagram and a README "Memory flow" section documenting the actual runtime behavior.
208
212
 
209
213
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
214
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -2,6 +2,10 @@
2
2
  <img src="assets/hero.png" alt="jeo-code 자율 코딩 에이전트 히어로 일러스트" width="100%" />
3
3
  </p>
4
4
 
5
+ <p align="center">
6
+ <img src="assets/icon.png" alt="jeo-code icon" width="96" />
7
+ </p>
8
+
5
9
  <h1 align="center">jeo-code (jeo)</h1>
6
10
 
7
11
  <p align="center">
@@ -200,11 +204,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
200
204
  ## 변경 이력 (Changelog)
201
205
 
202
206
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
207
+ - **[0.6.23]** (2026-06-19) — Live reasoning/thinking streams in the TUI across every provider, three new OpenAI-compatible backends (LM Studio, xAI, Kimi) join the auth/discovery/catalog surface, and Gemini gains native function-calling.
203
208
  - **[0.6.22]** (2026-06-18) — Extended-thinking activation is now consistent across providers: a `low` session thinking level enables reasoning everywhere.
204
209
  - **[0.6.21]** (2026-06-18) — Session thinking level now reaches the provider's actual reasoning depth, not just the token ceiling.
205
210
  - **[0.6.20]** (2026-06-18) — Launch REPL internals decomposed into testable modules: `@mention` path completion, slash-command view renderers, and slash-command handlers extracted from the monolithic `launch.ts` into dedicated files with full unit-test coverage.
206
211
  - **[0.6.19]** (2026-06-18) — Post-turn hooks run once per batch (not per edit), local hook reads are mtime-cached, tool-result formatting is parallelized, and wrapped colored text keeps its tint.
207
- - **[0.6.18]** (2026-06-17) — Memory data-flow diagram and a README "Memory flow" section documenting the actual runtime behavior.
208
212
 
209
213
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
214
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
  <img src="assets/hero.png" alt="jeo-code autonomous coding-agent hero illustration" width="100%" />
3
3
  </p>
4
4
 
5
+ <p align="center">
6
+ <img src="assets/icon.png" alt="jeo-code icon" width="96" />
7
+ </p>
8
+
5
9
  <h1 align="center">jeo-code (jeo)</h1>
6
10
 
7
11
  <p align="center">
@@ -200,11 +204,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
200
204
  ## Changelog
201
205
 
202
206
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
207
+ - **[0.6.23]** (2026-06-19) — Live reasoning/thinking streams in the TUI across every provider, three new OpenAI-compatible backends (LM Studio, xAI, Kimi) join the auth/discovery/catalog surface, and Gemini gains native function-calling.
203
208
  - **[0.6.22]** (2026-06-18) — Extended-thinking activation is now consistent across providers: a `low` session thinking level enables reasoning everywhere.
204
209
  - **[0.6.21]** (2026-06-18) — Session thinking level now reaches the provider's actual reasoning depth, not just the token ceiling.
205
210
  - **[0.6.20]** (2026-06-18) — Launch REPL internals decomposed into testable modules: `@mention` path completion, slash-command view renderers, and slash-command handlers extracted from the monolithic `launch.ts` into dedicated files with full unit-test coverage.
206
211
  - **[0.6.19]** (2026-06-18) — Post-turn hooks run once per batch (not per edit), local hook reads are mtime-cached, tool-result formatting is parallelized, and wrapped colored text keeps its tint.
207
- - **[0.6.18]** (2026-06-17) — Memory data-flow diagram and a README "Memory flow" section documenting the actual runtime behavior.
208
212
 
209
213
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
214
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -2,6 +2,10 @@
2
2
  <img src="assets/hero.png" alt="jeo-code 自主编码代理主视觉插图" width="100%" />
3
3
  </p>
4
4
 
5
+ <p align="center">
6
+ <img src="assets/icon.png" alt="jeo-code icon" width="96" />
7
+ </p>
8
+
5
9
  <h1 align="center">jeo-code (jeo)</h1>
6
10
 
7
11
  <p align="center">
@@ -200,11 +204,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
200
204
  ## 更新日志 (Changelog)
201
205
 
202
206
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
207
+ - **[0.6.23]** (2026-06-19) — Live reasoning/thinking streams in the TUI across every provider, three new OpenAI-compatible backends (LM Studio, xAI, Kimi) join the auth/discovery/catalog surface, and Gemini gains native function-calling.
203
208
  - **[0.6.22]** (2026-06-18) — Extended-thinking activation is now consistent across providers: a `low` session thinking level enables reasoning everywhere.
204
209
  - **[0.6.21]** (2026-06-18) — Session thinking level now reaches the provider's actual reasoning depth, not just the token ceiling.
205
210
  - **[0.6.20]** (2026-06-18) — Launch REPL internals decomposed into testable modules: `@mention` path completion, slash-command view renderers, and slash-command handlers extracted from the monolithic `launch.ts` into dedicated files with full unit-test coverage.
206
211
  - **[0.6.19]** (2026-06-18) — Post-turn hooks run once per batch (not per edit), local hook reads are mtime-cached, tool-result formatting is parallelized, and wrapped colored text keeps its tint.
207
- - **[0.6.18]** (2026-06-17) — Memory data-flow diagram and a README "Memory flow" section documenting the actual runtime behavior.
208
212
 
209
213
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
210
214
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.6.22",
3
+ "version": "0.6.23",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -1,4 +1,5 @@
1
1
  import { findCatalogEntry } from "../ai/model-catalog-compat";
2
+ import { OPENAI_COMPAT_PROVIDERS } from "../ai/providers/openai-compatible-catalog";
2
3
  import { CODEX_MODELS } from "../ai/model-catalog";
3
4
  import { z } from "zod";
4
5
 
@@ -18,6 +19,11 @@ const StoredOAuthSchema = z.object({
18
19
  });
19
20
 
20
21
  const OAuthEntry = z.union([z.string(), StoredOAuthSchema]);
22
+ // Catalog-driven OpenAI-compatible providers contribute their own apiKey + oauth-slot
23
+ // schema keys (incl. hyphenated names like `alibaba-coding-plan`), so config-file keys
24
+ // are validated/kept rather than stripped. Adding a provider = one catalog row.
25
+ const compatKeySchema = Object.fromEntries(OPENAI_COMPAT_PROVIDERS.map(p => [p.name, z.string().optional()]));
26
+ const compatOAuthSchema = Object.fromEntries(OPENAI_COMPAT_PROVIDERS.map(p => [p.name, OAuthEntry.optional()]));
21
27
  const HookConfigSchema = z.object({
22
28
  enabled: z.boolean().optional(),
23
29
  hooks: z
@@ -45,6 +51,9 @@ export const ConfigSchema = z
45
51
  openai: z.string().optional(),
46
52
  gemini: z.string().optional(),
47
53
  antigravity: z.string().optional(),
54
+ xai: z.string().optional(),
55
+ kimi: z.string().optional(),
56
+ ...compatKeySchema,
48
57
  })
49
58
  .default({}),
50
59
  oauth: z
@@ -53,6 +62,9 @@ export const ConfigSchema = z
53
62
  openai: OAuthEntry.optional(),
54
63
  gemini: OAuthEntry.optional(),
55
64
  antigravity: OAuthEntry.optional(),
65
+ xai: OAuthEntry.optional(),
66
+ kimi: OAuthEntry.optional(),
67
+ ...compatOAuthSchema,
56
68
  })
57
69
  .optional(),
58
70
  ollamaBaseUrl: z.string().optional(),
@@ -404,9 +404,16 @@ export async function exportSession(
404
404
  const role = m.role.charAt(0).toUpperCase() + m.role.slice(1);
405
405
  // Fence longer than the longest backtick run in the body (CommonMark) so message
406
406
  // content containing ``` doesn't prematurely close the code fence.
407
- const longest = (m.content.match(/`+/g) ?? []).reduce((mx, r) => Math.max(mx, r.length), 0);
408
- const fence = "`".repeat(Math.max(3, longest + 1));
409
- lines.push(`## ${role}`, "", fence, m.content, fence, "");
407
+ const fenceFor = (s: string) => "`".repeat(Math.max(3, (s.match(/`+/g) ?? []).reduce((mx, r) => Math.max(mx, r.length), 0) + 1));
408
+ lines.push(`## ${role}`, "");
409
+ // Persisted thinking (gjc "think → answer"): include it in the durable export so the
410
+ // markdown record matches the in-app transcript and the JSON export.
411
+ if (m.role === "assistant" && m.reasoning?.trim()) {
412
+ const tf = fenceFor(m.reasoning);
413
+ lines.push("### Thinking", "", tf, m.reasoning, tf, "");
414
+ }
415
+ const fence = fenceFor(m.content);
416
+ lines.push(fence, m.content, fence, "");
410
417
  }
411
418
  return lines.join("\n");
412
419
  }
@@ -2,6 +2,8 @@ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import * as os from "node:os";
4
4
  import { parseConfig } from "./config-schema";
5
+ import type { AuthProvider } from "../auth/storage";
6
+ import { OPENAI_COMPAT_PROVIDERS } from "../ai/providers/openai-compatible-catalog";
5
7
  import { jeoEnv } from "../util/env";
6
8
 
7
9
  /** Persisted OAuth credential set (access + refresh + expiry) for a provider. */
@@ -26,27 +28,21 @@ export interface HookConfig {
26
28
  }
27
29
 
28
30
  export interface Config {
29
- providers: {
30
- anthropic?: string;
31
- openai?: string;
32
- gemini?: string;
33
- antigravity?: string;
34
- };
31
+ /** Per-provider API keys, keyed by AuthProvider (cloud keys + catalog OpenAI-compatible). */
32
+ providers: Partial<Record<AuthProvider, string>>;
35
33
  /**
36
34
  * OAuth credentials. `resolveCredential()` returns these before API keys so refresh
37
35
  * metadata is not lost, but provider execution/status applies the GJC parity rule:
38
- * an API key is broader and wins whenever both key + OAuth exist.
36
+ * an API key is broader and wins whenever both key + OAuth exist. API-key-only
37
+ * providers never populate OAuth; the key exists for index-compatibility.
39
38
  */
40
- oauth?: {
41
- anthropic?: string | StoredOAuth;
42
- openai?: string | StoredOAuth;
43
- gemini?: string | StoredOAuth;
44
- antigravity?: string | StoredOAuth;
45
- };
39
+ oauth?: Partial<Record<AuthProvider, string | StoredOAuth>>;
46
40
  /** Base URL for the local Ollama server (keyless). */
47
41
  ollamaBaseUrl?: string;
48
- /** Base URL override for OpenAI-compatible providers (LM Studio, vLLM, llama-cpp-server, ...). */
42
+ /** Base URL override for OpenAI-compatible providers (vLLM, llama-cpp-server, ...). */
49
43
  openaiBaseUrl?: string;
44
+ /** Base URL for the local LM Studio server (keyless, OpenAI-compatible). */
45
+ lmstudioBaseUrl?: string;
50
46
  defaultModel: string;
51
47
  theme?: string;
52
48
  thinkingLevel?: "minimal" | "low" | "medium" | "high" | "xhigh";
@@ -193,6 +189,13 @@ function withEnvOverlay(cfg: Config): Config {
193
189
  if (!providers.anthropic && process.env.ANTHROPIC_API_KEY) providers.anthropic = process.env.ANTHROPIC_API_KEY;
194
190
  if (!providers.openai && process.env.OPENAI_API_KEY) providers.openai = process.env.OPENAI_API_KEY;
195
191
  if (!providers.gemini && process.env.GEMINI_API_KEY) providers.gemini = process.env.GEMINI_API_KEY;
192
+ if (!providers.xai && process.env.XAI_API_KEY) providers.xai = process.env.XAI_API_KEY;
193
+ // Catalog-driven OpenAI-compatible providers: each provider's own `apiKeyEnv`
194
+ // (e.g. GROQ_API_KEY, HF_TOKEN, NANO_GPT_API_KEY) fills config.providers[name].
195
+ for (const def of OPENAI_COMPAT_PROVIDERS) {
196
+ const key = def.name as AuthProvider; // every catalog name is an AuthProvider
197
+ if (!providers[key] && process.env[def.apiKeyEnv]) providers[key] = process.env[def.apiKeyEnv];
198
+ }
196
199
  return {
197
200
  ...cfg,
198
201
  providers,
@@ -200,6 +203,7 @@ function withEnvOverlay(cfg: Config): Config {
200
203
  defaultModel: jeoEnv("DEFAULT_MODEL") || cfg.defaultModel,
201
204
  ollamaBaseUrl: cfg.ollamaBaseUrl || process.env.OLLAMA_HOST || "http://localhost:11434",
202
205
  openaiBaseUrl: cfg.openaiBaseUrl || process.env.OPENAI_BASE_URL,
206
+ lmstudioBaseUrl: cfg.lmstudioBaseUrl || process.env.LMSTUDIO_BASE_URL || "http://localhost:1234/v1",
203
207
  roles: {
204
208
  smol: cfg.roles?.smol || jeoEnv("SMOL_MODEL"),
205
209
  slow: cfg.roles?.slow || jeoEnv("SLOW_MODEL"),
@@ -214,6 +218,7 @@ function envDefaultConfig(): Config {
214
218
  anthropic: process.env.ANTHROPIC_API_KEY,
215
219
  openai: process.env.OPENAI_API_KEY,
216
220
  gemini: process.env.GEMINI_API_KEY,
221
+ xai: process.env.XAI_API_KEY,
217
222
  },
218
223
  defaultModel: jeoEnv("DEFAULT_MODEL") || DEFAULT_MODEL,
219
224
  thinkingLevel: "medium",
package/src/ai/index.ts CHANGED
@@ -10,3 +10,4 @@ export { openaiAdapter } from "./providers/openai";
10
10
  export { geminiAdapter } from "./providers/gemini";
11
11
  export { antigravityAdapter } from "./providers/antigravity";
12
12
  export { ollamaAdapter } from "./providers/ollama";
13
+ export { lmstudioAdapter } from "./providers/lmstudio";
@@ -7,6 +7,7 @@
7
7
  * catalog annotates known ids with capabilities.
8
8
  */
9
9
  import type { ProviderName } from "./types";
10
+ import { openaiCompatDef } from "./providers/openai-compatible-catalog";
10
11
 
11
12
  export type ThinkLevel = "minimal" | "low" | "medium" | "high" | "xhigh";
12
13
 
@@ -36,6 +37,8 @@ const STD: ThinkLevel[] = ["minimal", "low", "medium", "high"];
36
37
  export const ANTIGRAVITY_MODELS = [
37
38
  "claude-opus-4-5-thinking",
38
39
  "claude-opus-4-6-thinking",
40
+ "claude-opus-4-8",
41
+ "claude-opus-4-8-thinking",
39
42
  "claude-sonnet-4-5",
40
43
  "claude-sonnet-4-5-thinking",
41
44
  "claude-sonnet-4-6",
@@ -59,6 +62,10 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
59
62
  { canonical: "claude-sonnet-4-5", provider: "anthropic", providerModel: "claude-sonnet-4-5-20250929", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
60
63
  { canonical: "claude-opus-4-1", provider: "anthropic", providerModel: "claude-opus-4-1-20250805", contextTokens: 200_000, maxOutputTokens: 32_000, thinking: FULL, images: true },
61
64
  { canonical: "claude-opus-4-5", provider: "anthropic", providerModel: "claude-opus-4-5-20251101", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
65
+ // NOTE: confirm exact dated provider ids when these ship publicly; the family
66
+ // heuristic in `catalogMetadata` keeps reasoning working even before that.
67
+ { canonical: "claude-opus-4-6", provider: "anthropic", providerModel: "claude-opus-4-6", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
68
+ { canonical: "claude-opus-4-8", provider: "anthropic", providerModel: "claude-opus-4-8", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
62
69
  // OpenAI
63
70
  { canonical: "gpt-4o", provider: "openai", providerModel: "gpt-4o", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: true },
64
71
  { canonical: "gpt-4o-mini", provider: "openai", providerModel: "gpt-4o-mini", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: true },
@@ -68,6 +75,16 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
68
75
  { canonical: "o4-mini", provider: "openai", providerModel: "o4-mini", contextTokens: 200_000, maxOutputTokens: 100_000, thinking: STD, images: true },
69
76
  { canonical: "gpt-5.5", provider: "openai", providerModel: "gpt-5.5", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true },
70
77
  { canonical: "gpt-5.4", provider: "openai", providerModel: "gpt-5.4", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true },
78
+ // xAI (Grok) — OpenAI-compatible at https://api.x.ai/v1 (XAI_API_KEY)
79
+ { canonical: "grok-4.3", provider: "xai", providerModel: "grok-4.3", contextTokens: 256_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
80
+ { canonical: "grok-4-fast-reasoning", provider: "xai", providerModel: "grok-4-fast-reasoning", contextTokens: 2_000_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
81
+ { canonical: "grok-4-fast-non-reasoning", provider: "xai", providerModel: "grok-4-fast-non-reasoning", contextTokens: 2_000_000, maxOutputTokens: 64_000, thinking: [], images: true },
82
+ { canonical: "grok-code-fast-1", provider: "xai", providerModel: "grok-code-fast-1", contextTokens: 256_000, maxOutputTokens: 64_000, thinking: FULL, images: false },
83
+ // Kimi (Moonshot) — OpenAI-compatible at https://api.moonshot.ai/v1 (KIMI_API_KEY)
84
+ { canonical: "kimi-k2-0711-preview", provider: "kimi", providerModel: "kimi-k2-0711-preview", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: false },
85
+ { canonical: "kimi-thinking-preview", provider: "kimi", providerModel: "kimi-thinking-preview", contextTokens: 128_000, maxOutputTokens: 32_000, thinking: FULL, images: true },
86
+ { canonical: "kimi-latest", provider: "kimi", providerModel: "kimi-latest", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: true },
87
+ { canonical: "moonshot-v1-128k", provider: "kimi", providerModel: "moonshot-v1-128k", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: false },
71
88
  // Google
72
89
  { canonical: "gemini-1.5-pro", provider: "gemini", providerModel: "gemini-1.5-pro", contextTokens: 1_000_000, maxOutputTokens: 8_192, thinking: [], images: true },
73
90
  { canonical: "gemini-2.0-flash", provider: "gemini", providerModel: "gemini-2.0-flash", contextTokens: 1_000_000, maxOutputTokens: 8_192, thinking: [], images: true },
@@ -134,13 +151,111 @@ export function catalogByProvider(provider: ProviderName): CatalogModel[] {
134
151
  return MODEL_CATALOG.filter(m => m.provider === provider);
135
152
  }
136
153
 
154
+
155
+ /**
156
+ * Heuristic capability inference for ids the static catalog does not list yet.
157
+ *
158
+ * New model revisions ship faster than this file is edited (e.g. a fresh
159
+ * `claude-opus-4-8` before its entry is added). Rather than treating every
160
+ * uncatalogued id as "no reasoning" — which silently hides the thinking TUI —
161
+ * we map the id to its model family and version and synthesize metadata so a
162
+ * brand-new revision behaves like its catalogued siblings (e.g. opus-4-6).
163
+ *
164
+ * Conservative by design: returns `undefined` for ids that do not match a known
165
+ * reasoning-capable family, so random/unknown ids stay "unknown caps".
166
+ */
167
+ export function inferCatalogMetadata(modelId: string): CatalogModel | undefined {
168
+ const raw = modelId.trim();
169
+ if (!raw) return undefined;
170
+ const antigravity = /^antigravity\//i.test(raw);
171
+ const id = raw.replace(/^antigravity\//i, "").toLowerCase();
172
+
173
+ // Anthropic Claude: opus/sonnet/haiku. Major version >= 4 ships extended
174
+ // thinking (mirrors every catalogued claude-4-x entry); claude-3-x does not.
175
+ const claude = id.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:[-.](\d+))?/);
176
+ if (claude) {
177
+ const major = Number(claude[2]);
178
+ const thinking = major >= 4 ? FULL : [];
179
+ return {
180
+ canonical: raw,
181
+ provider: antigravity ? "antigravity" : "anthropic",
182
+ providerModel: id,
183
+ contextTokens: 200_000,
184
+ maxOutputTokens: claude[1] === "haiku" ? 64_000 : 64_000,
185
+ thinking,
186
+ images: true,
187
+ company: antigravity ? "Anthropic via Antigravity" : "Anthropic",
188
+ };
189
+ }
190
+
191
+ // OpenAI reasoning families: the o-series (o1, o3, … any major incl. o10+) and
192
+ // gpt-5+ (digit-count agnostic so gpt-6/o10 never silently lose reasoning the way
193
+ // opus-4-8 did). gpt-4 and earlier are non-reasoning. Mirrors the openai.ts gate.
194
+ const gptMajor = id.match(/^gpt-(\d+)/);
195
+ const openaiReasoner = /^o\d+(-|$)/.test(id) || (gptMajor ? Number(gptMajor[1]) >= 5 : false);
196
+ if (openaiReasoner) {
197
+ const wide = gptMajor ? Number(gptMajor[1]) >= 5 : false;
198
+ return {
199
+ canonical: raw,
200
+ provider: antigravity ? "antigravity" : "openai",
201
+ providerModel: id,
202
+ contextTokens: wide ? 400_000 : 200_000,
203
+ maxOutputTokens: wide ? 128_000 : 100_000,
204
+ thinking: wide ? FULL : STD,
205
+ images: !id.includes("mini") || id.includes("o4-mini") || id.includes("o3"),
206
+ company: antigravity ? "OpenAI via Antigravity" : "OpenAI",
207
+ };
208
+ }
209
+
210
+ // Google Gemini: 2.5+ and 3.x expose thinking; 1.5/2.0 do not.
211
+ const gemini = id.match(/^gemini-(\d+)(?:\.(\d+))?/);
212
+ if (gemini) {
213
+ const major = Number(gemini[1]);
214
+ const minor = Number(gemini[2] ?? 0);
215
+ const reasons = major >= 3 || (major === 2 && minor >= 5);
216
+ const big3 = major >= 3;
217
+ return {
218
+ canonical: raw,
219
+ provider: antigravity ? "antigravity" : "gemini",
220
+ providerModel: id,
221
+ contextTokens: 1_000_000,
222
+ maxOutputTokens: 65_536,
223
+ thinking: !reasons ? [] : big3 || id.includes("thinking") || id.includes("-high") || id.includes("-low") ? FULL : STD,
224
+ images: true,
225
+ company: antigravity ? "Google Antigravity" : "Google",
226
+ };
227
+ }
228
+
229
+ // xAI Grok 4+ reasoning variants.
230
+ const grok = id.match(/^grok-(\d+)/);
231
+ if (grok && Number(grok[1]) >= 4) {
232
+ const nonReasoning = id.includes("non-reasoning");
233
+ return {
234
+ canonical: raw,
235
+ provider: "xai",
236
+ providerModel: id,
237
+ contextTokens: id.includes("fast") ? 2_000_000 : 256_000,
238
+ maxOutputTokens: 64_000,
239
+ thinking: nonReasoning ? [] : FULL,
240
+ images: !id.includes("code"),
241
+ company: "xAI",
242
+ };
243
+ }
244
+
245
+ return undefined;
246
+ }
247
+
137
248
  /** Annotate a discovered/raw model id with catalog metadata, when known. */
138
249
  export function catalogMetadata(modelId: string): CatalogModel | undefined {
139
250
  const direct = findCatalogModel(modelId);
140
251
  if (direct) return direct;
141
252
  // Tolerate provider-prefixed or bare provider model ids.
142
253
  const bare = modelId.replace(/^[a-z-]+\//, "");
143
- return MODEL_CATALOG.find(m => m.providerModel === bare || m.providerModel.endsWith(`/${bare}`) || m.canonical === bare);
254
+ const hit = MODEL_CATALOG.find(m => m.providerModel === bare || m.providerModel.endsWith(`/${bare}`) || m.canonical === bare);
255
+ if (hit) return hit;
256
+ // Last resort: infer capabilities from the model family so a brand-new
257
+ // revision still surfaces reasoning/thinking like its catalogued siblings.
258
+ return inferCatalogMetadata(modelId);
144
259
  }
145
260
 
146
261
  /** Whether a model supports a given thinking level (per the catalog). */
@@ -157,6 +272,11 @@ export function companyLabel(provider: string, entry?: { company?: string }): st
157
272
  if (low === "openai") return "OpenAI";
158
273
  if (low === "gemini") return "Google";
159
274
  if (low === "ollama") return "Ollama";
275
+ if (low === "lmstudio") return "LM Studio";
276
+ if (low === "xai") return "xAI";
277
+ if (low === "kimi") return "Moonshot";
278
+ const compat = openaiCompatDef(low);
279
+ if (compat) return compat.label;
160
280
  if (low === "antigravity") return "Antigravity";
161
281
  return provider.charAt(0).toUpperCase() + provider.slice(1);
162
282
  }
@@ -13,6 +13,7 @@ import type { ProviderName } from "./types";
13
13
  import { PROVIDER_NAMES } from "./provider-status";
14
14
  import { catalogByProvider, CODEX_MODELS } from "./model-catalog";
15
15
  import { extractChatgptAccountId } from "./providers/openai-responses";
16
+ import { openaiCompatDef } from "./providers/openai-compatible-catalog";
16
17
 
17
18
  export interface ProviderModelsResult {
18
19
  provider: ProviderName;
@@ -68,7 +69,9 @@ function anthropicHeaders(cred: Credential): Record<string, string> {
68
69
  }
69
70
 
70
71
  function authProviderFor(provider: ProviderName): AuthProvider | undefined {
71
- if (provider === "ollama") return undefined;
72
+ // Local providers (ollama/lmstudio) are keyless and do not resolve through the
73
+ // auth core. API-key providers (incl. xai/kimi) DO — so discovery sends their key.
74
+ if (provider === "ollama" || provider === "lmstudio") return undefined;
72
75
  return provider;
73
76
  }
74
77
 
@@ -78,6 +81,17 @@ export function discoveryRequest(
78
81
  cred: Credential | undefined,
79
82
  baseUrl?: string,
80
83
  ): { url: string; headers: Record<string, string>; method?: "GET" | "POST"; body?: string } {
84
+ // Catalog-driven compat providers: OpenAI `${base}/models` (Bearer) or Anthropic
85
+ // `${base}/v1/models` (x-api-key). Both return { data: [{ id }] }.
86
+ const compat = openaiCompatDef(provider);
87
+ if (compat) {
88
+ const base = (baseUrl ?? compat.baseUrl).replace(/\/$/, "");
89
+ const token = cred?.kind === "api_key" || cred?.kind === "oauth" ? cred.token : "";
90
+ if (compat.protocol === "anthropic") {
91
+ return { url: `${base}/v1/models`, headers: token ? { "x-api-key": token, "anthropic-version": "2023-06-01" } : {} };
92
+ }
93
+ return { url: `${base}/models`, headers: token ? { Authorization: `Bearer ${token}` } : {} };
94
+ }
81
95
  switch (provider) {
82
96
  case "anthropic":
83
97
  return { url: "https://api.anthropic.com/v1/models", headers: anthropicHeaders(cred!) };
@@ -120,7 +134,21 @@ export function discoveryRequest(
120
134
  const base = (baseUrl ?? "http://localhost:11434").replace(/\/$/, "");
121
135
  return { url: `${base}/api/tags`, headers: {} };
122
136
  }
137
+ case "lmstudio": {
138
+ const base = (baseUrl ?? "http://localhost:1234/v1").replace(/\/$/, "");
139
+ return { url: `${base}/models`, headers: {} };
140
+ }
141
+ case "xai": {
142
+ const token = cred?.kind === "api_key" ? cred.token : "";
143
+ return { url: "https://api.x.ai/v1/models", headers: token ? { Authorization: `Bearer ${token}` } : {} };
144
+ }
145
+ case "kimi": {
146
+ const token = cred?.kind === "api_key" ? cred.token : "";
147
+ return { url: "https://api.moonshot.ai/v1/models", headers: token ? { Authorization: `Bearer ${token}` } : {} };
148
+ }
123
149
  }
150
+ // Unreachable: every ProviderName is a switch case or catalog-handled above.
151
+ throw new Error(`discoveryRequest: unhandled provider '${provider}'`);
124
152
  }
125
153
 
126
154
  /**
@@ -149,9 +177,26 @@ export function parseModelsBody(provider: ProviderName, body: unknown): string[]
149
177
  data?: { id?: string }[];
150
178
  models?: ({ name?: string; supportedGenerationMethods?: string[] } & CodexModelRow)[];
151
179
  };
180
+ if (openaiCompatDef(provider)) {
181
+ // Catalog OpenAI-compatible: { data: [{ id }] }. Prefix-qualify so the router
182
+ // maps the id back to this provider (the ids alone don't heuristically route).
183
+ return (data.data ?? []).map(m => (m.id ? `${provider}/${m.id}` : "")).filter(Boolean);
184
+ }
152
185
  if (provider === "ollama") {
153
186
  return (data.models ?? []).map(m => `ollama/${m.name ?? ""}`).filter(s => s !== "ollama/");
154
187
  }
188
+ if (provider === "lmstudio") {
189
+ // LM Studio is OpenAI-compatible: { data: [{ id }] }. Qualify with the routing prefix.
190
+ return (data.data ?? []).map(m => `lmstudio/${m.id ?? ""}`).filter(s => s !== "lmstudio/");
191
+ }
192
+ if (provider === "xai") {
193
+ // xAI is OpenAI-compatible: { data: [{ id }] }. Grok ids route to xai by name, so no prefix.
194
+ return (data.data ?? []).map(m => m.id ?? "").filter(Boolean);
195
+ }
196
+ if (provider === "kimi") {
197
+ // Moonshot is OpenAI-compatible: { data: [{ id }] }. kimi/moonshot ids route by name.
198
+ return (data.data ?? []).map(m => m.id ?? "").filter(Boolean);
199
+ }
155
200
  if (provider === "antigravity") {
156
201
  // fetchAvailableModels keys the map by the CALLABLE model id (e.g.
157
202
  // "gemini-3-flash"); the entry's `model` field is an internal enum
@@ -252,7 +297,14 @@ export async function listProviderModels(
252
297
 
253
298
  let cred: Credential | undefined;
254
299
  let source: ProviderModelsResult["source"] = "keyless";
255
- if (provider !== "ollama") {
300
+ if (provider === "xai") {
301
+ // xAI (Grok) is API-key only and not an OAuth AuthProvider: resolve its key
302
+ // directly from config/env instead of the AuthProvider credential store.
303
+ const key = (opts.config ?? (await readGlobalConfig())).providers?.xai;
304
+ if (!key) return { provider, models: [], ok: false, source: "none", error: "not logged in" };
305
+ cred = { kind: "api_key", provider: "openai", token: key };
306
+ source = "api_key";
307
+ } else if (provider !== "ollama" && provider !== "lmstudio") {
256
308
  const authProvider = authProviderFor(provider);
257
309
  const raw = await resolveCredential(authProvider!);
258
310
  cred = raw;
@@ -338,7 +390,7 @@ export async function discoverModels(
338
390
  listProviderModels(p, {
339
391
  ...opts,
340
392
  config: cfg,
341
- baseUrl: p === "ollama" ? (cfg.ollamaBaseUrl ?? opts.baseUrl) : p === "openai" ? (cfg.openaiBaseUrl ?? opts.baseUrl) : opts.baseUrl,
393
+ baseUrl: p === "ollama" ? (cfg.ollamaBaseUrl ?? opts.baseUrl) : p === "lmstudio" ? (cfg.lmstudioBaseUrl ?? opts.baseUrl) : p === "openai" ? (cfg.openaiBaseUrl ?? opts.baseUrl) : opts.baseUrl,
342
394
  }),
343
395
  ),
344
396
  );