jeo-code 0.6.23 → 0.6.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,30 @@ 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.26] - 2026-06-19
10
+ _The forge emblem is redrawn again as the mascot crayfish, foregrounding its signature pincer claws (집게)._
11
+
12
+ ### Changed
13
+ - **Forge emblem redrawn as the mascot crayfish with raised pincer claws (집게).** The compact and grand forge marks (`FORGE_MARK_ART` / `_GRAND`) now read as the neon crayfish (가재) from `assets/character.png`, foregrounding its defining feature — two raised pincer claws on angled arms (`◣◣ ◢◢` jaws over `◆══╲ ╱══◆` arms) above the glowing eye/terminal cluster (`◉◉◉`) and a rounded carapace/tail. Purely pictographic and wordless (no embedded lettering), width-1 glyphs only so the TUI's padding/centering math stays exact; the blink frames snap the claws shut so the crayfish "clicks". Cross-checked against gajae-code's image-based crab/crayfish brand and the shared blue→violet→pink neon palette. The grand variant stays wide enough (30 cols) to keep the narrow-box compact fallback reachable.
14
+
15
+ ## [0.6.25] - 2026-06-19
16
+ _Reasoning works at every thinking level (gajae parity), and the forge emblem is redrawn as the neon-lens coding wizard._
17
+
18
+ ### Changed
19
+ - **Reasoning now activates at EVERY thinking level — no level restriction (gajae parity).** Previously the lowest tier disabled reasoning entirely: `thinkingToReasoningEffort` collapsed `minimal`→`low`, and the provider budgets treated `minimal`/unset effort as OFF, so picking the lowest level (or `/fast`) silently turned thinking off. `minimal` is now a genuine lightest reasoning effort threaded end to end — Anthropic (`minimal → 2000` budget_tokens), Gemini (`minimal → 2000`, clamped under the output cap), and Antigravity-Claude (`minimal → 2000`) all enable scaling-depth thinking for `minimal`/`low`/`medium`/`high`, matching gajae-code's `[Minimal, Low, Medium, High]` effort set. Only a fully UNSET effort stays non-thinking (the explicit off path). `xhigh` still maps to the deepest `high` tier the provider APIs accept.
20
+ - **Forge emblem redrawn as the mascot neon-lens coding wizard.** The compact and grand forge marks (`FORGE_MARK_ART` / `_GRAND`) now read as the character from `assets/character.png` — a pointed wizard hat with a twinkling star tip, the glowing asymmetric ◆/◇ neon lens eyes on a nose-bridge, and the violet gown shoulders cradling the glowing terminal screen the wizard holds (`◉◉◉`). Purely pictographic and wordless (no embedded lettering), width-1 glyphs only so the TUI's padding/centering math stays exact; the blink frames twinkle the star and wink the lenses.
21
+
22
+ ## [0.6.24] - 2026-06-19
23
+ _`/provider` opens an interactive onboarding selector (OAuth vs API-compatible), and OpenAI-compatible backends gain per-vendor native-reasoning formats._
24
+
25
+ ### Added
26
+ - **Interactive `/provider` onboarding selector (gjc parity).** A bare `/provider` (or `/login`) in a TTY now opens a picker — OAuth login (the common path) vs API-compatible endpoint setup — instead of printing static usage. New pure builders in `provider-picker.ts` (`buildOnboardingChoices` / `onboardingPicker` / `renderOnboardingPicker`); the scriptable/non-TTY path still falls through to the readiness panel unchanged, and a "Headless OAuth: paste the redirect URL or code" hint is shown for remote sessions.
27
+ - **Per-vendor native-reasoning formats for OpenAI-compatible providers.** A `reasoningFormat` setting (`openai` → `reasoning_effort`, `openrouter` → `reasoning:{effort}`, `qwen` → `enable_thinking`, `zai` → `thinking:{type:"enabled"}`) lets the OpenAI-compatible factory enable streamed reasoning per backend, so OpenRouter/Qwen/Z.ai models surface thinking like the first-party providers.
28
+
29
+ ### Changed
30
+ - **`/provider` and `/login` descriptions** updated to mention the interactive selector and the headless paste flow.
31
+ - **ASCII-art / welcome rendering refactor** with refreshed tests (`ascii-art`, `pickers`, `tui-welcome`); the legacy `dna-claw-anim` animation test was retired.
32
+
9
33
  ## [0.6.23] - 2026-06-19
10
34
  _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
35
 
package/README.ja.md CHANGED
@@ -204,11 +204,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
204
204
  ## 変更履歴 (Changelog)
205
205
 
206
206
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
207
+ - **[0.6.26]** (2026-06-19) — The forge emblem is redrawn again as the mascot crayfish, foregrounding its signature pincer claws (집게).
208
+ - **[0.6.25]** (2026-06-19) — Reasoning works at every thinking level (gajae parity), and the forge emblem is redrawn as the neon-lens coding wizard.
209
+ - **[0.6.24]** (2026-06-19) — `/provider` opens an interactive onboarding selector (OAuth vs API-compatible), and OpenAI-compatible backends gain per-vendor native-reasoning formats.
207
210
  - **[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.
208
211
  - **[0.6.22]** (2026-06-18) — Extended-thinking activation is now consistent across providers: a `low` session thinking level enables reasoning everywhere.
209
- - **[0.6.21]** (2026-06-18) — Session thinking level now reaches the provider's actual reasoning depth, not just the token ceiling.
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.
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.
212
212
 
213
213
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
214
214
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -204,11 +204,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
204
204
  ## 변경 이력 (Changelog)
205
205
 
206
206
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
207
+ - **[0.6.26]** (2026-06-19) — The forge emblem is redrawn again as the mascot crayfish, foregrounding its signature pincer claws (집게).
208
+ - **[0.6.25]** (2026-06-19) — Reasoning works at every thinking level (gajae parity), and the forge emblem is redrawn as the neon-lens coding wizard.
209
+ - **[0.6.24]** (2026-06-19) — `/provider` opens an interactive onboarding selector (OAuth vs API-compatible), and OpenAI-compatible backends gain per-vendor native-reasoning formats.
207
210
  - **[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.
208
211
  - **[0.6.22]** (2026-06-18) — Extended-thinking activation is now consistent across providers: a `low` session thinking level enables reasoning everywhere.
209
- - **[0.6.21]** (2026-06-18) — Session thinking level now reaches the provider's actual reasoning depth, not just the token ceiling.
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.
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.
212
212
 
213
213
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
214
214
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -204,11 +204,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
204
204
  ## Changelog
205
205
 
206
206
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
207
+ - **[0.6.26]** (2026-06-19) — The forge emblem is redrawn again as the mascot crayfish, foregrounding its signature pincer claws (집게).
208
+ - **[0.6.25]** (2026-06-19) — Reasoning works at every thinking level (gajae parity), and the forge emblem is redrawn as the neon-lens coding wizard.
209
+ - **[0.6.24]** (2026-06-19) — `/provider` opens an interactive onboarding selector (OAuth vs API-compatible), and OpenAI-compatible backends gain per-vendor native-reasoning formats.
207
210
  - **[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.
208
211
  - **[0.6.22]** (2026-06-18) — Extended-thinking activation is now consistent across providers: a `low` session thinking level enables reasoning everywhere.
209
- - **[0.6.21]** (2026-06-18) — Session thinking level now reaches the provider's actual reasoning depth, not just the token ceiling.
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.
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.
212
212
 
213
213
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
214
214
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -204,11 +204,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
204
204
  ## 更新日志 (Changelog)
205
205
 
206
206
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
207
+ - **[0.6.26]** (2026-06-19) — The forge emblem is redrawn again as the mascot crayfish, foregrounding its signature pincer claws (집게).
208
+ - **[0.6.25]** (2026-06-19) — Reasoning works at every thinking level (gajae parity), and the forge emblem is redrawn as the neon-lens coding wizard.
209
+ - **[0.6.24]** (2026-06-19) — `/provider` opens an interactive onboarding selector (OAuth vs API-compatible), and OpenAI-compatible backends gain per-vendor native-reasoning formats.
207
210
  - **[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.
208
211
  - **[0.6.22]** (2026-06-18) — Extended-thinking activation is now consistent across providers: a `low` session thinking level enables reasoning everywhere.
209
- - **[0.6.21]** (2026-06-18) — Session thinking level now reaches the provider's actual reasoning depth, not just the token ceiling.
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.
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.
212
212
 
213
213
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
214
214
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.6.23",
3
+ "version": "0.6.26",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -100,13 +100,16 @@ export function thinkingMaxTokens(level?: "minimal" | "low" | "medium" | "high"
100
100
  return 16000;
101
101
  }
102
102
 
103
- /** Map the thinking level to an OpenAI reasoning-effort tier. `minimal` maps to `low`
104
- * (the lowest tier o-series reliably accepts; gpt-5's `minimal` is opt-in via options). */
103
+ /** Map the thinking level to an OpenAI reasoning-effort tier. `minimal` is preserved as a
104
+ * genuine (lightest) reasoning effort NOT collapsed to `low` so reasoning works at EVERY
105
+ * thinking level (gajae parity: Minimal is a real effort). Only an unset level returns undefined
106
+ * (reasoning off). `xhigh` maps to `high`, the deepest tier the provider APIs accept. */
105
107
  export function thinkingToReasoningEffort(
106
108
  level?: "minimal" | "low" | "medium" | "high" | "xhigh",
107
- ): "low" | "medium" | "high" | undefined {
109
+ ): "minimal" | "low" | "medium" | "high" | undefined {
108
110
  if (!level) return undefined;
109
- if (level === "minimal" || level === "low") return "low";
111
+ if (level === "minimal") return "minimal";
112
+ if (level === "low") return "low";
110
113
  if (level === "high" || level === "xhigh") return "high";
111
114
  return "medium";
112
115
  }
@@ -29,6 +29,12 @@ export interface ProviderStatus {
29
29
  envVar?: string;
30
30
  /** True when the provider can serve a request right now. */
31
31
  ready: boolean;
32
+ /** True when an OAuth credential is stored for this provider (logged in via OAuth). */
33
+ loggedIn?: boolean;
34
+ /** Account email from the stored OAuth credential, when known. */
35
+ oauthEmail?: string;
36
+ /** Epoch ms expiry of the stored OAuth access token, when known. */
37
+ oauthExpires?: number;
32
38
  }
33
39
 
34
40
  /** The env var that supplies a provider's API key. Catalog providers carry their
@@ -59,6 +65,13 @@ function oauthAccess(stored: string | StoredOAuth | undefined): string | undefin
59
65
  return typeof stored === "string" ? stored : stored.access;
60
66
  }
61
67
 
68
+ /** Login metadata (account email / expiry) from a stored OAuth record, when present. */
69
+ function oauthLoginInfo(stored: string | StoredOAuth | undefined): { loggedIn: boolean; oauthEmail?: string; oauthExpires?: number } {
70
+ if (!stored) return { loggedIn: false };
71
+ if (typeof stored === "string") return { loggedIn: true };
72
+ return { loggedIn: true, oauthEmail: stored.email, oauthExpires: stored.expires };
73
+ }
74
+
62
75
  function configuredCredential(provider: AuthProvider, cfg: Config): Credential {
63
76
  const stored = cfg.oauth?.[provider];
64
77
  const oauth = oauthAccess(stored);
@@ -123,6 +136,9 @@ export async function describeProvider(name: ProviderName, config?: Config): Pro
123
136
  // gemini-cli OAuth is served end-to-end via Cloud Code Assist — no API key.
124
137
  label = "OAuth (Gemini CLI / Cloud Code Assist)";
125
138
  }
139
+ // Login status reflects the provider's OWN stored OAuth (e.g. "logged in to antigravity"),
140
+ // independent of any cross-provider credential fallback used for readiness.
141
+ const login = oauthLoginInfo(cfg.oauth?.[ownProvider]);
126
142
  return {
127
143
  name,
128
144
  kind,
@@ -130,6 +146,9 @@ export async function describeProvider(name: ProviderName, config?: Config): Pro
130
146
  baseUrl,
131
147
  envVar: providerEnvVar(name),
132
148
  ready,
149
+ loggedIn: login.loggedIn,
150
+ oauthEmail: login.oauthEmail,
151
+ oauthExpires: login.oauthExpires,
133
152
  };
134
153
  }
135
154
 
@@ -73,11 +73,13 @@ function anthropicSystemBlocks(
73
73
  }
74
74
 
75
75
  /** Anthropic extended-thinking budget by reasoning effort (kept under max_tokens). Cross-provider
76
- * parity (matches Gemini's tiers): low/medium/high all enable thinking with scaling depth; only
77
- * minimal/unset stay non-thinking so /fast and minimal thinking remain cheaper/faster. */
76
+ * parity (matches Gemini's tiers): minimal/low/medium/high ALL enable thinking with scaling
77
+ * depth reasoning works at every thinking level (gajae parity: Minimal is a real effort).
78
+ * Only an UNSET effort stays non-thinking (the explicit /fast off path). */
78
79
  function anthropicThinkingBudget(effort: CallOptions["reasoningEffort"], maxTokens: number): number | undefined {
79
80
  let budget: number;
80
81
  switch (effort) {
82
+ case "minimal": budget = 2000; break;
81
83
  case "low": budget = 4000; break;
82
84
  case "medium": budget = 10000; break;
83
85
  case "high": budget = 24000; break;
@@ -9,11 +9,15 @@ import { geminiThinkingBudget } from "./gemini";
9
9
  const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
10
10
  const ANTIGRAVITY_SANDBOX_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
11
11
 
12
+
12
13
  /** Anthropic-style thinking budget for Claude served via CCA. gemini's budget fn
13
14
  * returns undefined for claude ids, which left antigravity Claude with NO thinking
14
- * requested (the opus "no reasoning" gap). Mirrors anthropic's effort→budget tiers. */
15
+ * requested (the opus "no reasoning" gap). Mirrors anthropic's effort→budget tiers
16
+ * minimal/low/medium/high ALL think (gajae parity: reasoning at every level); only an
17
+ * UNSET effort stays non-thinking. */
15
18
  function antigravityClaudeThinkingBudget(effort: CallOptions["reasoningEffort"]): number | undefined {
16
19
  switch (effort) {
20
+ case "minimal": return 2000;
17
21
  case "low": return 4000;
18
22
  case "medium": return 10000;
19
23
  case "high": return 24000;
@@ -24,14 +24,15 @@ export function geminiThinkingBudget(model: string, effort?: CallOptions["reason
24
24
  const floor = m.includes("pro") ? 128 : 0; // pro-class cannot fully disable thinking
25
25
  let budget: number;
26
26
  switch (effort) {
27
+ // minimal/low/medium/high ALL enable thinking with scaling depth — reasoning works at
28
+ // every thinking level (gajae parity: Minimal is a real effort). Only an UNSET effort
29
+ // falls through to the floor (off for flash-class, the API minimum for pro-class).
30
+ case "minimal": budget = Math.max(floor, 2000); break;
27
31
  case "low": budget = 4000; break;
28
32
  case "medium": budget = 10000; break;
29
33
  case "high": budget = 24000; break;
30
- case "minimal":
31
34
  default: budget = floor;
32
35
  }
33
- // Thought tokens bill against maxOutputTokens: keep at least ~1K of the output
34
- // budget for visible text, or thinking starves the reply to an empty MAX_TOKENS.
35
36
  if (typeof maxTokens === "number") budget = Math.min(budget, Math.max(floor, maxTokens - 1024));
36
37
  return budget;
37
38
  }
@@ -25,32 +25,39 @@ export interface OpenAICompatProviderDef {
25
25
  readonly defaultModel: string;
26
26
  /** Wire protocol: "openai" (/chat/completions, default) or "anthropic" (/v1/messages). */
27
27
  readonly protocol?: "openai" | "anthropic";
28
+ /** True for subscription/plan products (coding-plan, portal, token-plan, code) rather than
29
+ * pay-per-token APIs. Surfaced under the `/provider` "OAuth / subscription" onboarding path. */
30
+ readonly subscription?: boolean;
31
+ /** gjc-parity native-reasoning enablement: how this backend turns thinking ON.
32
+ * "openrouter" → `reasoning:{effort}`; "qwen" → `enable_thinking:true`; "zai" →
33
+ * `thinking:{type:"enabled"}`. Omitted → OpenAI `reasoning_effort` (o/gpt-5 only). */
34
+ readonly thinkingFormat?: "openai" | "openrouter" | "qwen" | "zai";
28
35
  }
29
36
 
30
37
  export const OPENAI_COMPAT_PROVIDERS: readonly OpenAICompatProviderDef[] = [
31
38
  { name: "groq", label: "Groq", baseUrl: "https://api.groq.com/openai/v1", apiKeyEnv: "GROQ_API_KEY", defaultModel: "groq/llama-3.3-70b-versatile" },
32
39
  { name: "deepseek", label: "DeepSeek", baseUrl: "https://api.deepseek.com/v1", apiKeyEnv: "DEEPSEEK_API_KEY", defaultModel: "deepseek/deepseek-chat" },
33
40
  { name: "mistral", label: "Mistral", baseUrl: "https://api.mistral.ai/v1", apiKeyEnv: "MISTRAL_API_KEY", defaultModel: "mistral/mistral-large-latest" },
34
- { name: "openrouter", label: "OpenRouter", baseUrl: "https://openrouter.ai/api/v1", apiKeyEnv: "OPENROUTER_API_KEY", defaultModel: "openrouter/openai/gpt-4o-mini" },
41
+ { name: "openrouter", label: "OpenRouter", baseUrl: "https://openrouter.ai/api/v1", apiKeyEnv: "OPENROUTER_API_KEY", defaultModel: "openrouter/openai/gpt-4o-mini", thinkingFormat: "openrouter" },
35
42
  { name: "together", label: "Together", baseUrl: "https://api.together.xyz/v1", apiKeyEnv: "TOGETHER_API_KEY", defaultModel: "together/meta-llama/Llama-3.3-70B-Instruct-Turbo" },
36
43
  { name: "cerebras", label: "Cerebras", baseUrl: "https://api.cerebras.ai/v1", apiKeyEnv: "CEREBRAS_API_KEY", defaultModel: "cerebras/llama-3.3-70b" },
37
44
  { name: "fireworks", label: "Fireworks", baseUrl: "https://api.fireworks.ai/inference/v1", apiKeyEnv: "FIREWORKS_API_KEY", defaultModel: "fireworks/accounts/fireworks/models/llama-v3p3-70b-instruct" },
38
45
  { name: "nvidia", label: "NVIDIA", baseUrl: "https://integrate.api.nvidia.com/v1", apiKeyEnv: "NVIDIA_API_KEY", defaultModel: "nvidia/meta/llama-3.3-70b-instruct" },
39
46
  // Additional gjc-parity OpenAI-compatible clouds (authoritative base URLs + env vars).
40
- { name: "alibaba-coding-plan", label: "Alibaba Coding Plan", baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", apiKeyEnv: "ALIBABA_CODING_PLAN_API_KEY", defaultModel: "alibaba-coding-plan/qwen3.5-plus" },
47
+ { name: "alibaba-coding-plan", label: "Alibaba Coding Plan", baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", apiKeyEnv: "ALIBABA_CODING_PLAN_API_KEY", defaultModel: "alibaba-coding-plan/qwen3.5-plus", subscription: true, thinkingFormat: "qwen" },
41
48
  { name: "huggingface", label: "Hugging Face", baseUrl: "https://router.huggingface.co/v1", apiKeyEnv: "HF_TOKEN", defaultModel: "huggingface/deepseek-ai/DeepSeek-R1" },
42
49
  { name: "nanogpt", label: "NanoGPT", baseUrl: "https://nano-gpt.com/api/v1", apiKeyEnv: "NANO_GPT_API_KEY", defaultModel: "nanogpt/openai/gpt-5.4" },
43
- { name: "qwen-portal", label: "Qwen Portal", baseUrl: "https://portal.qwen.ai/v1", apiKeyEnv: "QWEN_PORTAL_API_KEY", defaultModel: "qwen-portal/coder-model" },
50
+ { name: "qwen-portal", label: "Qwen Portal", baseUrl: "https://portal.qwen.ai/v1", apiKeyEnv: "QWEN_PORTAL_API_KEY", defaultModel: "qwen-portal/coder-model", subscription: true, thinkingFormat: "qwen" },
44
51
  { name: "synthetic", label: "Synthetic", baseUrl: "https://api.synthetic.new/openai/v1", apiKeyEnv: "SYNTHETIC_API_KEY", defaultModel: "synthetic/hf:moonshotai/Kimi-K2.5" },
45
52
  { name: "venice", label: "Venice", baseUrl: "https://api.venice.ai/api/v1", apiKeyEnv: "VENICE_API_KEY", defaultModel: "venice/llama-3.3-70b" },
46
53
  { name: "zenmux", label: "ZenMux", baseUrl: "https://zenmux.ai/api/v1", apiKeyEnv: "ZENMUX_API_KEY", defaultModel: "zenmux/anthropic/claude-opus-4.6" },
47
54
  { name: "qianfan", label: "Qianfan", baseUrl: "https://qianfan.baidubce.com/v2", apiKeyEnv: "QIANFAN_API_KEY", defaultModel: "qianfan/deepseek-v3.2" },
48
55
  { name: "xiaomi", label: "Xiaomi", baseUrl: "https://api.xiaomimimo.com/v1", apiKeyEnv: "XIAOMI_API_KEY", defaultModel: "xiaomi/mimo-v2-flash" },
49
- { name: "xiaomi-token-plan-ams", label: "Xiaomi Token Plan (Europe)", baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", apiKeyEnv: "XIAOMI_TOKEN_PLAN_AMS_API_KEY", defaultModel: "xiaomi-token-plan-ams/mimo-v2.5" },
50
- { name: "xiaomi-token-plan-cn", label: "Xiaomi Token Plan (China)", baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", apiKeyEnv: "XIAOMI_TOKEN_PLAN_CN_API_KEY", defaultModel: "xiaomi-token-plan-cn/mimo-v2.5" },
51
- { name: "xiaomi-token-plan-sgp", label: "Xiaomi Token Plan (Singapore)", baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", apiKeyEnv: "XIAOMI_TOKEN_PLAN_SGP_API_KEY", defaultModel: "xiaomi-token-plan-sgp/mimo-v2.5" },
52
- { name: "minimax-code", label: "MiniMax Code", baseUrl: "https://api.minimax.io/v1", apiKeyEnv: "MINIMAX_CODE_API_KEY", defaultModel: "minimax-code/minimax-m3" },
53
- { name: "minimax-code-cn", label: "MiniMax Code (China)", baseUrl: "https://api.minimaxi.com/v1", apiKeyEnv: "MINIMAX_CODE_CN_API_KEY", defaultModel: "minimax-code-cn/minimax-m3" },
56
+ { name: "xiaomi-token-plan-ams", label: "Xiaomi Token Plan (Europe)", baseUrl: "https://token-plan-ams.xiaomimimo.com/v1", apiKeyEnv: "XIAOMI_TOKEN_PLAN_AMS_API_KEY", defaultModel: "xiaomi-token-plan-ams/mimo-v2.5", subscription: true },
57
+ { name: "xiaomi-token-plan-cn", label: "Xiaomi Token Plan (China)", baseUrl: "https://token-plan-cn.xiaomimimo.com/v1", apiKeyEnv: "XIAOMI_TOKEN_PLAN_CN_API_KEY", defaultModel: "xiaomi-token-plan-cn/mimo-v2.5", subscription: true },
58
+ { name: "xiaomi-token-plan-sgp", label: "Xiaomi Token Plan (Singapore)", baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", apiKeyEnv: "XIAOMI_TOKEN_PLAN_SGP_API_KEY", defaultModel: "xiaomi-token-plan-sgp/mimo-v2.5", subscription: true },
59
+ { name: "minimax-code", label: "MiniMax Code", baseUrl: "https://api.minimax.io/v1", apiKeyEnv: "MINIMAX_CODE_API_KEY", defaultModel: "minimax-code/minimax-m3", subscription: true },
60
+ { name: "minimax-code-cn", label: "MiniMax Code (China)", baseUrl: "https://api.minimaxi.com/v1", apiKeyEnv: "MINIMAX_CODE_CN_API_KEY", defaultModel: "minimax-code-cn/minimax-m3", subscription: true },
54
61
  // Anthropic-Messages-protocol providers (served via makeAnthropicCompatibleAdapter).
55
62
  { name: "zai", label: "z.ai", baseUrl: "https://api.z.ai/api/anthropic", apiKeyEnv: "ZAI_API_KEY", defaultModel: "zai/glm-5.2", protocol: "anthropic" },
56
63
  { name: "minimax", label: "MiniMax", baseUrl: "https://api.minimax.io/anthropic", apiKeyEnv: "MINIMAX_API_KEY", defaultModel: "minimax/minimax-m3", protocol: "anthropic" },
@@ -61,6 +68,10 @@ const BY_NAME = new Map<string, OpenAICompatProviderDef>(OPENAI_COMPAT_PROVIDERS
61
68
  /** All catalog provider names (for PROVIDER_NAMES / AuthProvider unions). */
62
69
  export const OPENAI_COMPAT_NAMES: readonly ProviderName[] = OPENAI_COMPAT_PROVIDERS.map(p => p.name);
63
70
 
71
+ /** Subscription/plan-tier provider names (coding-plan, portal, token-plan, code) — surfaced
72
+ * under the `/provider` "OAuth / subscription" onboarding path rather than the generic API-key list. */
73
+ export const SUBSCRIPTION_PROVIDER_NAMES: readonly ProviderName[] = OPENAI_COMPAT_PROVIDERS.filter(p => p.subscription).map(p => p.name);
74
+
64
75
  /** Catalog entry for a provider name, or undefined when it is not catalog-driven. */
65
76
  export function openaiCompatDef(name: string): OpenAICompatProviderDef | undefined {
66
77
  return BY_NAME.get(name);
@@ -12,12 +12,15 @@ import { openaiAdapter } from "./openai";
12
12
  */
13
13
  const KEYLESS: Credential = { kind: "none", provider: "openai" };
14
14
 
15
- export function makeOpenAICompatibleAdapter(opts: { name: ProviderName; baseUrl: string; keyless?: boolean }): ProviderAdapter {
15
+ export function makeOpenAICompatibleAdapter(opts: { name: ProviderName; baseUrl: string; keyless?: boolean; thinkingFormat?: CallOptions["reasoningFormat"] }): ProviderAdapter {
16
16
  const prefix = `${opts.name}/`;
17
17
  const prep = (o: CallOptions): CallOptions => ({
18
18
  ...o,
19
19
  model: o.model.startsWith(prefix) ? o.model.slice(prefix.length) : o.model,
20
20
  baseUrl: o.baseUrl ?? opts.baseUrl,
21
+ // Carry the backend's native-reasoning enablement so openaiRequest can turn thinking
22
+ // on with the right param (gjc parity) — without it OpenRouter/Qwen models stay silent.
23
+ reasoningFormat: o.reasoningFormat ?? opts.thinkingFormat,
21
24
  });
22
25
  const credFor = (c: Credential): Credential => (opts.keyless ? KEYLESS : c);
23
26
  return {
@@ -76,6 +76,17 @@ export function codexResponsesRequest(
76
76
  // frame can show the model's thinking instead of a frozen "calling model (Ns)…".
77
77
  payload.reasoning = { effort: options.reasoningEffort, summary: "auto" };
78
78
  }
79
+ // OAuth → the undocumented ChatGPT/Codex backend (codex headers + account-id).
80
+ // API key → the public OpenAI Responses API (`/v1/responses`) with a plain Bearer.
81
+ // Both speak the same Responses schema (the body above), so only url+headers differ.
82
+ if (credential.kind === "api_key") {
83
+ const base = (options.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "");
84
+ return {
85
+ url: `${base}/responses`,
86
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}`, accept: "text/event-stream" },
87
+ body: JSON.stringify(payload),
88
+ };
89
+ }
79
90
  const accountId = extractChatgptAccountId(token);
80
91
  const headers: Record<string, string> = {
81
92
  "content-type": "application/json",
@@ -6,6 +6,36 @@ import { codexResponsesCall, codexResponsesStream } from "./openai-responses";
6
6
  import { serializeToolCalls, serializeAccumulatedToolCalls } from "../../agent/tool-schemas";
7
7
  import { createThinkSplitter } from "../think-tags";
8
8
 
9
+ /** True for OpenAI reasoning models (o-series + gpt-5+ family). Digit-count agnostic
10
+ * (gpt-6/o10 stay reasoning). Strips the `openai/` routing prefix first. */
11
+ export function isOpenAIReasoningModel(model: string): boolean {
12
+ const m = model.startsWith("openai/") ? model.slice(7) : model;
13
+ const gptMajor = m.match(/^gpt-(\d+)/);
14
+ return /^o\d/.test(m) || (gptMajor ? Number(gptMajor[1]) >= 5 : false);
15
+ }
16
+
17
+ /** gjc-parity: write the backend-specific param that turns NATIVE reasoning ON, so the
18
+ * model streams thinking we can surface. Mutates `payload`. "openai" needs no param here
19
+ * (handled by `reasoning_effort` for true o-series/gpt-5 models). */
20
+ export function applyCompatThinking(
21
+ payload: Record<string, unknown>,
22
+ format: CallOptions["reasoningFormat"],
23
+ effort: NonNullable<CallOptions["reasoningEffort"]>,
24
+ ): void {
25
+ switch (format) {
26
+ case "openrouter":
27
+ payload.reasoning = { effort };
28
+ break;
29
+ case "qwen":
30
+ payload.enable_thinking = true;
31
+ break;
32
+ case "zai":
33
+ payload.thinking = { type: "enabled" };
34
+ break;
35
+ // "openai" / undefined: no extra param (reasoning_effort path covers real OpenAI models).
36
+ }
37
+ }
38
+
9
39
  export function openaiRequest(messages: Message[], options: CallOptions, credential: Credential, stream: boolean): { url: string; headers: Record<string, string>; body: string } {
10
40
  const model = options.model.startsWith("openai/") ? options.model.slice(7) : options.model;
11
41
  const systemPrompt = options.systemPrompt ?? messages.find(m => m.role === "system")?.content;
@@ -26,8 +56,7 @@ export function openaiRequest(messages: Message[], options: CallOptions, credent
26
56
  // Reasoning models (o-series, gpt-5+ family) take max_completion_tokens + reasoning_effort
27
57
  // and reject temperature; classic chat models (gpt-4o, …) take max_tokens + temperature.
28
58
  // Digit-count agnostic (gpt-6/o10 stay reasoning) — mirrors inferCatalogMetadata.
29
- const gptMajorMatch = model.match(/^gpt-(\d+)/);
30
- const isReasoning = /^o\d/.test(model) || (gptMajorMatch ? Number(gptMajorMatch[1]) >= 5 : false);
59
+ const isReasoning = isOpenAIReasoningModel(model);
31
60
  const payload: Record<string, unknown> = {
32
61
  model,
33
62
  messages: openaiMessages,
@@ -39,6 +68,13 @@ export function openaiRequest(messages: Message[], options: CallOptions, credent
39
68
  payload.temperature = options.temperature ?? 0.2;
40
69
  payload.max_tokens = options.maxTokens ?? 4000;
41
70
  }
71
+ // gjc parity — enable NATIVE reasoning per the backend's thinking format so the model
72
+ // actually emits reasoning (otherwise OpenRouter/Qwen/z.ai stay silent and the TUI has
73
+ // nothing to show). `reasoning_effort` (OpenAI-style) only suits o-series/gpt-5; other
74
+ // backends need their own param. Gated on a requested effort (off → no thinking).
75
+ if (options.reasoningEffort && !isReasoning) {
76
+ applyCompatThinking(payload, options.reasoningFormat, options.reasoningEffort);
77
+ }
42
78
  if (stream) {
43
79
  payload.stream = true;
44
80
  payload.stream_options = { include_usage: true };
@@ -65,12 +101,56 @@ function emptyCompletionError(finishReason: string | undefined): Error {
65
101
  return new Error(`OpenAI returned no content${finishReason ? ` (finish_reason=${finishReason})` : ""}${hint}.`);
66
102
  }
67
103
 
104
+ /** A streamed `choices[].delta`. `reasoning` is `unknown` because OpenAI-compatible
105
+ * servers disagree on its shape: a plain string (OpenRouter/xAI), an object
106
+ * `{ text|content }`, or absent (the `reasoning_details[]` array carries it instead). */
107
+ export interface OpenAIDelta {
108
+ content?: string;
109
+ reasoning_content?: string;
110
+ reasoning_text?: string;
111
+ reasoning?: unknown;
112
+ reasoning_details?: { text?: string; content?: string }[];
113
+ tool_calls?: { index?: number; function?: { name?: string; arguments?: string } }[];
114
+ }
115
+
116
+ /** Pull a reasoning-text delta out of the many OpenAI-compatible shapes. Returns the
117
+ * first non-empty of: `reasoning_content`, `reasoning_text`, a string/`{text|content}`
118
+ * `reasoning`, or the concatenated `reasoning_details[].text|content`. */
119
+ export function reasoningDeltaOf(delta: OpenAIDelta | undefined): string | undefined {
120
+ if (!delta) return undefined;
121
+ if (typeof delta.reasoning_content === "string" && delta.reasoning_content) return delta.reasoning_content;
122
+ if (typeof delta.reasoning_text === "string" && delta.reasoning_text) return delta.reasoning_text;
123
+ const r = delta.reasoning;
124
+ if (typeof r === "string" && r) return r;
125
+ if (r && typeof r === "object") {
126
+ const o = r as { text?: unknown; content?: unknown };
127
+ if (typeof o.text === "string" && o.text) return o.text;
128
+ if (typeof o.content === "string" && o.content) return o.content;
129
+ }
130
+ if (Array.isArray(delta.reasoning_details)) {
131
+ const t = delta.reasoning_details
132
+ .map(x => (typeof x?.text === "string" ? x.text : typeof x?.content === "string" ? x.content : ""))
133
+ .join("");
134
+ if (t) return t;
135
+ }
136
+ return undefined;
137
+ }
138
+
68
139
  export const openaiAdapter: ProviderAdapter = {
69
140
  name: "openai",
70
141
  supportsNativeTools: true,
71
142
  async call(messages, options, credential) {
72
143
  // ChatGPT/Codex OAuth can't use /chat/completions — route to the Codex Responses backend.
73
144
  if (credential.kind === "oauth") return codexResponsesCall(messages, options, credential);
145
+ // OpenAI reasoning models (o-series/gpt-5) expose reasoning ONLY via the Responses
146
+ // API — /chat/completions hides it. Use Responses for a real-OpenAI API key (no
147
+ // custom baseUrl); OpenAI-compatible servers (groq/xai/lmstudio/… set baseUrl) keep
148
+ // the chat path + reasoning_content. Fall back to chat if /responses is unavailable.
149
+ if (credential.kind === "api_key" && !options.baseUrl && isOpenAIReasoningModel(options.model)) {
150
+ try {
151
+ return await codexResponsesCall(messages, options, credential);
152
+ } catch { /* /responses unsupported for this model/account — fall through to chat */ }
153
+ }
74
154
  const { url, headers, body } = openaiRequest(messages, options, credential, false);
75
155
  const response = await fetch(url, { method: "POST", headers, body, signal: options.signal });
76
156
  if (!response.ok) throw await providerHttpError("OpenAI", response);
@@ -88,6 +168,18 @@ export const openaiAdapter: ProviderAdapter = {
88
168
  yield* codexResponsesStream(messages, options, credential);
89
169
  return;
90
170
  }
171
+ // OpenAI reasoning models surface reasoning only via Responses (see call()). Pre-stream
172
+ // fallback: if it fails before any chunk, retry on chat completions (no regression).
173
+ if (credential.kind === "api_key" && !options.baseUrl && isOpenAIReasoningModel(options.model)) {
174
+ let started = false;
175
+ try {
176
+ for await (const chunk of codexResponsesStream(messages, options, credential)) { started = true; yield chunk; }
177
+ return;
178
+ } catch (e) {
179
+ if (started) throw e; // mid-stream failure — cannot safely restart on another endpoint
180
+ // else fall through to chat completions below
181
+ }
182
+ }
91
183
  const { url, headers, body } = openaiRequest(messages, options, credential, true);
92
184
  let response = await fetch(url, { method: "POST", headers, body, signal: options.signal });
93
185
  if (response.status === 400) {
@@ -110,7 +202,7 @@ export const openaiAdapter: ProviderAdapter = {
110
202
  const think = createThinkSplitter(options.onReasoning);
111
203
  const toolAcc = new Map<number, { name: string; args: string }>();
112
204
  for await (const data of readSse(response.body)) {
113
- let chunk: { choices?: { delta?: { content?: string; reasoning_content?: string; reasoning?: string; reasoning_text?: string; tool_calls?: { index?: number; function?: { name?: string; arguments?: string } }[] }; finish_reason?: string }[]; usage?: { prompt_tokens?: number; completion_tokens?: number } };
205
+ let chunk: { choices?: { delta?: OpenAIDelta; finish_reason?: string }[]; usage?: { prompt_tokens?: number; completion_tokens?: number } };
114
206
  try {
115
207
  chunk = JSON.parse(data);
116
208
  } catch {
@@ -124,10 +216,10 @@ export const openaiAdapter: ProviderAdapter = {
124
216
  yield visible;
125
217
  }
126
218
  }
127
- // Structured reasoning channel (DeepSeek `reasoning_content`, OpenRouter/xAI
128
- // `reasoning`): a SEPARATE field from content, so it bypasses the <think> splitter.
129
- const d = chunk.choices?.[0]?.delta;
130
- const reason = d?.reasoning_content ?? d?.reasoning ?? d?.reasoning_text;
219
+ // Structured reasoning channel (separate from `content`, so it bypasses the
220
+ // <think> splitter): handles string fields, an object `reasoning`, and the
221
+ // `reasoning_details[]` array form (OpenRouter/xAI/DeepSeek variants).
222
+ const reason = reasoningDeltaOf(chunk.choices?.[0]?.delta);
131
223
  if (reason) options.onReasoning?.(reason);
132
224
  const tcs = chunk.choices?.[0]?.delta?.tool_calls;
133
225
  if (tcs) {
@@ -34,6 +34,6 @@ providerRegistry.register("kimi", kimiAdapter);
34
34
  for (const def of OPENAI_COMPAT_PROVIDERS) {
35
35
  const adapter = def.protocol === "anthropic"
36
36
  ? makeAnthropicCompatibleAdapter({ name: def.name, baseUrl: def.baseUrl })
37
- : makeOpenAICompatibleAdapter({ name: def.name, baseUrl: def.baseUrl });
37
+ : makeOpenAICompatibleAdapter({ name: def.name, baseUrl: def.baseUrl, thinkingFormat: def.thinkingFormat });
38
38
  providerRegistry.register(def.name, adapter);
39
39
  }
package/src/ai/types.ts CHANGED
@@ -55,6 +55,11 @@ export interface CallOptions {
55
55
  signal?: AbortSignal;
56
56
  /** Reasoning effort for reasoning models (o-series / gpt-5), mapped from thinkingLevel. */
57
57
  reasoningEffort?: "minimal" | "low" | "medium" | "high";
58
+ /** How an OpenAI-compatible backend enables/streams native reasoning (gjc parity):
59
+ * "openai" → `reasoning_effort`; "openrouter" → `reasoning: {effort}`; "qwen" →
60
+ * `enable_thinking: true`; "zai" → `thinking: {type:"enabled"}`. Set per provider by
61
+ * the openai-compatible factory; without it a model never emits reasoning to surface. */
62
+ reasoningFormat?: "openai" | "openrouter" | "qwen" | "zai";
58
63
  /** Notified before each auto-retry backoff wait (rate limits / transient errors).
59
64
  * NOT forwarded to provider adapters — consumed by the manager's retry layer. */
60
65
  onRetry?: (attempt: number, err: unknown, delayMs: number) => void;
@@ -16,6 +16,7 @@ import {
16
16
  snapshotProvider,
17
17
  setApiKey,
18
18
  isOAuthProvider,
19
+ OAUTH_PROVIDERS,
19
20
  API_KEY_ONLY_PROVIDERS,
20
21
  type AuthProvider,
21
22
  type OAuthController,
@@ -32,7 +33,8 @@ export async function runAuthCommand(args: string[]): Promise<void> {
32
33
  process.exitCode = 1;
33
34
  }
34
35
 
35
- const CLOUD_PROVIDERS: readonly AuthProvider[] = ["anthropic", "openai", "gemini", "antigravity", "xai", "kimi"];
36
+ // Every loginable provider: OAuth-capable API-key-only (keyless ollama/lmstudio excluded).
37
+ const CLOUD_PROVIDERS: readonly AuthProvider[] = [...OAUTH_PROVIDERS, ...API_KEY_ONLY_PROVIDERS];
36
38
  /** True (and prints an error + sets exit code) when `p` is given but not a known provider. */
37
39
  function rejectInvalidProvider(p: string | undefined): boolean {
38
40
  if (p !== undefined && !(CLOUD_PROVIDERS as readonly string[]).includes(p)) {
@@ -56,7 +58,7 @@ async function runAuthStatus(): Promise<void> {
56
58
  const cfg = await readGlobalConfig();
57
59
  console.log("\n=== jeo auth status ===");
58
60
  console.log("Provider API key OAuth");
59
- for (const p of ["anthropic", "openai", "gemini", "antigravity", "xai", "kimi"] as AuthProvider[]) {
61
+ for (const p of CLOUD_PROVIDERS) {
60
62
  const snap = await snapshotProvider(p);
61
63
  const key = p === "antigravity" ? "—" : (snap.apiKey ? "set" : "—");
62
64
  let oauth = "—";