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 +24 -0
- package/README.ja.md +3 -3
- package/README.ko.md +3 -3
- package/README.md +3 -3
- package/README.zh.md +3 -3
- package/package.json +1 -1
- package/src/ai/model-manager.ts +7 -4
- package/src/ai/provider-status.ts +19 -0
- package/src/ai/providers/anthropic.ts +4 -2
- package/src/ai/providers/antigravity.ts +5 -1
- package/src/ai/providers/gemini.ts +4 -3
- package/src/ai/providers/openai-compatible-catalog.ts +19 -8
- package/src/ai/providers/openai-compatible.ts +4 -1
- package/src/ai/providers/openai-responses.ts +11 -0
- package/src/ai/providers/openai.ts +99 -7
- package/src/ai/register-providers.ts +1 -1
- package/src/ai/types.ts +5 -0
- package/src/commands/auth.ts +4 -2
- package/src/commands/launch.ts +230 -15
- package/src/tui/app.ts +11 -11
- package/src/tui/components/ascii-art.ts +86 -122
- package/src/tui/components/provider-picker.ts +162 -0
- package/src/tui/components/slash.ts +2 -2
- package/src/tui/components/welcome.ts +8 -8
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
package/src/ai/model-manager.ts
CHANGED
|
@@ -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`
|
|
104
|
-
* (
|
|
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"
|
|
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
|
|
77
|
-
*
|
|
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
|
|
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?:
|
|
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 (
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
const reason =
|
|
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;
|
package/src/commands/auth.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 = "—";
|