jeo-code 0.7.27 → 0.7.28
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 +16 -0
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/package.json +1 -1
- package/src/agent/config-schema.ts +1 -0
- package/src/agent/engine.ts +9 -2
- package/src/agent/loop.ts +3 -0
- package/src/agent/state.ts +84 -5
- package/src/agent/task-tool.ts +3 -0
- package/src/ai/model-catalog.ts +23 -0
- package/src/ai/model-discovery.ts +6 -2
- package/src/ai/model-manager.ts +33 -12
- package/src/ai/providers/anthropic.ts +56 -17
- package/src/ai/providers/antigravity.ts +34 -22
- package/src/ai/providers/gemini.ts +81 -27
- package/src/ai/providers/kimi.ts +44 -5
- package/src/ai/providers/openai-responses.ts +36 -2
- package/src/ai/types.ts +10 -0
- package/src/auth/callback-server.ts +19 -5
- package/src/auth/flows/AGENTS.md +7 -6
- package/src/auth/flows/antigravity.ts +3 -0
- package/src/auth/flows/google-project.ts +8 -2
- package/src/auth/flows/google.ts +15 -1
- package/src/auth/flows/index.ts +10 -0
- package/src/auth/flows/kimi.ts +263 -0
- package/src/auth/flows/openai.ts +103 -3
- package/src/auth/oauth.ts +9 -0
- package/src/auth/refresh.ts +6 -1
- package/src/auth/storage.ts +5 -5
- package/src/commands/launch.ts +6 -2
- package/src/commands/team.ts +3 -0
- package/src/tui/components/welcome.ts +98 -39
- package/src/util/retry.ts +68 -31
|
@@ -4,7 +4,7 @@ import type { CallOptions, Message, ProviderAdapter } from "../types";
|
|
|
4
4
|
import { readSse } from "../sse";
|
|
5
5
|
import { providerHttpError } from "./errors";
|
|
6
6
|
import { serializeToolCalls } from "../../agent/tool-schemas";
|
|
7
|
-
import {
|
|
7
|
+
import { geminiThinkingConfig, getGeminiCliHeaders, type GeminiThinkingConfig } from "./gemini";
|
|
8
8
|
|
|
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";
|
|
@@ -37,15 +37,24 @@ function antigravityModelId(model: string): string {
|
|
|
37
37
|
return model.replace(/^antigravity\//, "");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** The
|
|
41
|
-
* Anthropic-style budget, native Gemini
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
40
|
+
/** The thinkingConfig actually requested for an Antigravity turn — Claude-via-CCA uses an
|
|
41
|
+
* Anthropic-style numeric budget, native Gemini goes through geminiThinkingConfig
|
|
42
|
+
* (gemini-3.x → thinkingLevel enum, 2.5/latest → numeric budget; in-name depth markers
|
|
43
|
+
* like `-high`/`-low` honoured). Centralised so the request builder and the streaming
|
|
44
|
+
* start-signal stay in agreement. */
|
|
45
|
+
function antigravityThinkingConfig(options: CallOptions): GeminiThinkingConfig | undefined {
|
|
45
46
|
const model = antigravityModelId(options.model);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
:
|
|
47
|
+
if (model.toLowerCase().includes("claude")) {
|
|
48
|
+
const budget = antigravityClaudeThinkingBudget(options.reasoningEffort);
|
|
49
|
+
return budget === undefined ? undefined : { includeThoughts: true, thinkingBudget: budget };
|
|
50
|
+
}
|
|
51
|
+
return geminiThinkingConfig(model, options.reasoningEffort);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** True when this turn requested thinking (a thinkingLevel or a positive budget). */
|
|
55
|
+
function antigravityThinkingActive(options: CallOptions): boolean {
|
|
56
|
+
const cfg = antigravityThinkingConfig(options);
|
|
57
|
+
return cfg !== undefined && ("thinkingLevel" in cfg || cfg.thinkingBudget > 0);
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
function projectIdFor(credential: Credential): string | undefined {
|
|
@@ -89,8 +98,8 @@ export async function resolveAntigravityProjectId(
|
|
|
89
98
|
// Antigravity-client tokens discover with ANTIGRAVITY metadata; gemini-cli
|
|
90
99
|
// tokens use the default gemini-cli metadata shape.
|
|
91
100
|
return discoverGoogleProjectId(token, credential.provider === "antigravity"
|
|
92
|
-
? { metadata: { ...ANTIGRAVITY_DISCOVERY_METADATA }, extraHeaders: { "User-Agent": getAntigravityUserAgent() }, signal: opts.signal }
|
|
93
|
-
: { signal: opts.signal });
|
|
101
|
+
? { metadata: { ...ANTIGRAVITY_DISCOVERY_METADATA }, extraHeaders: { "User-Agent": getAntigravityUserAgent() }, alwaysOnboard: true, signal: opts.signal }
|
|
102
|
+
: { extraHeaders: getGeminiCliHeaders(), signal: opts.signal });
|
|
94
103
|
});
|
|
95
104
|
let projectId: string;
|
|
96
105
|
try {
|
|
@@ -165,18 +174,21 @@ export function antigravityRequest(messages: Message[], options: CallOptions, cr
|
|
|
165
174
|
const isClaude = model.toLowerCase().includes("claude");
|
|
166
175
|
// Upstream Antigravity strips maxOutputTokens for non-Claude models; do the same.
|
|
167
176
|
if (isClaude) generationConfig.maxOutputTokens = options.maxTokens ?? 4000;
|
|
168
|
-
// Apply the thinking
|
|
169
|
-
// includeThoughts set. Gemini scales via
|
|
170
|
-
// Anthropic-style budget (gemini's
|
|
171
|
-
// interleaved-thinking beta header below —
|
|
172
|
-
// never streamed reasoning while native
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
// Apply the thinking config. CCA emits `thought` parts ONLY when thinkingConfig has
|
|
178
|
+
// includeThoughts set. Gemini scales via geminiThinkingConfig (level enum on 3.x,
|
|
179
|
+
// numeric budget otherwise); Claude-via-CCA needs an Anthropic-style budget (gemini's
|
|
180
|
+
// fn returns undefined for claude) PLUS the interleaved-thinking beta header below —
|
|
181
|
+
// without both, antigravity Claude (e.g. opus) never streamed reasoning while native
|
|
182
|
+
// sonnet did.
|
|
183
|
+
const agThinking = antigravityThinkingConfig(options);
|
|
184
|
+
const claudeThinkingOn = isClaude && agThinking !== undefined;
|
|
185
|
+
if (agThinking !== undefined) {
|
|
186
|
+
generationConfig.thinkingConfig = agThinking;
|
|
177
187
|
// Claude (via CCA) enforces max_tokens > thinking.budget_tokens — bump the output cap
|
|
178
188
|
// above the budget (mirrors the native Anthropic provider) or CCA returns HTTP 400.
|
|
179
|
-
if (claudeThinkingOn
|
|
189
|
+
if (claudeThinkingOn && "thinkingBudget" in agThinking) {
|
|
190
|
+
generationConfig.maxOutputTokens = Math.max((options.maxTokens ?? 4000), agThinking.thinkingBudget + 1024);
|
|
191
|
+
}
|
|
180
192
|
}
|
|
181
193
|
|
|
182
194
|
const request: Record<string, unknown> = {
|
|
@@ -288,7 +300,7 @@ export const antigravityAdapter: ProviderAdapter = {
|
|
|
288
300
|
if (!response.body) return;
|
|
289
301
|
// Signal the thinking phase up front (parity with anthropic's content_block_start) so the
|
|
290
302
|
// UI shows it even before/without `thought` parts — otherwise reasoning looked dead here.
|
|
291
|
-
if ((
|
|
303
|
+
if (antigravityThinkingActive(options)) options.onReasoningStart?.();
|
|
292
304
|
let yielded = false;
|
|
293
305
|
let usage: CcaUsage | undefined;
|
|
294
306
|
const fnCalls: { tool: string; arguments: Record<string, unknown> }[] = [];
|
|
@@ -11,7 +11,7 @@ import { serializeToolCalls } from "../../agent/tool-schemas";
|
|
|
11
11
|
* Pin an explicit budget: off unless reasoning was requested. Pro-class models
|
|
12
12
|
* cannot disable thinking (API minimum 128), so they keep a floor instead of 0.
|
|
13
13
|
* Older models (1.5/2.0) reject `thinkingConfig` entirely → undefined (omit). */
|
|
14
|
-
export function geminiThinkingBudget(model: string, effort?: CallOptions["reasoningEffort"], maxTokens?: number): number | undefined {
|
|
14
|
+
export function geminiThinkingBudget(model: string, effort?: CallOptions["reasoningEffort"] | "xhigh", maxTokens?: number): number | undefined {
|
|
15
15
|
const m = model.toLowerCase();
|
|
16
16
|
// Reasoning-capable when Gemini >= 2.5 (any 2.5+ minor) or major >= 3 (digit-count
|
|
17
17
|
// agnostic so gemini-10+ never silently loses thinking the way opus-4-8 did), plus
|
|
@@ -22,41 +22,85 @@ export function geminiThinkingBudget(model: string, effort?: CallOptions["reason
|
|
|
22
22
|
const thinkingCapable = (major >= 3 || (major === 2 && minor >= 5)) || /flash-latest|pro-latest/.test(m);
|
|
23
23
|
if (!thinkingCapable) return undefined;
|
|
24
24
|
const floor = m.includes("pro") ? 128 : 0; // pro-class cannot fully disable thinking
|
|
25
|
-
|
|
26
|
-
// VARIANT name itself encodes a thinking depth — `-high`/`-low` (e.g. gemini-3-pro-high)
|
|
27
|
-
// or an explicit `-thinking` suffix (gemini-2.5-flash-thinking) — that selection IS the
|
|
28
|
-
// user's thinking opt-in, so it overrides the silent floor and maps to a real budget.
|
|
29
|
-
// Unmarked ids (gemini-3-flash, gemini-2.5-flash) keep the off-by-default floor (parity
|
|
30
|
-
// with the other providers + the documented thought-token burn protection).
|
|
31
|
-
const named: CallOptions["reasoningEffort"] | undefined =
|
|
32
|
-
m.includes("-high") ? "high"
|
|
33
|
-
: m.includes("-low") ? "low"
|
|
34
|
-
: m.includes("thinking") ? "medium"
|
|
35
|
-
: undefined;
|
|
36
|
-
const effectiveEffort = effort ?? named;
|
|
25
|
+
const effectiveEffort = effort ?? geminiNamedEffort(m);
|
|
37
26
|
let budget: number;
|
|
38
27
|
switch (effectiveEffort) {
|
|
39
28
|
// minimal/low/medium/high ALL enable thinking with scaling depth — reasoning works at
|
|
40
29
|
// every thinking level (gajae parity: Minimal is a real effort). Only an UNSET effort
|
|
41
30
|
// (and no in-name depth marker) falls through to the floor.
|
|
42
|
-
|
|
43
|
-
case "
|
|
44
|
-
case "
|
|
45
|
-
case "
|
|
31
|
+
// Tier values mirror gjc's GOOGLE_THINKING table (stream.ts).
|
|
32
|
+
case "minimal": budget = Math.max(floor, 1024); break;
|
|
33
|
+
case "low": budget = 4096; break;
|
|
34
|
+
case "medium": budget = 8192; break;
|
|
35
|
+
case "high": budget = 16384; break;
|
|
36
|
+
case "xhigh": budget = 24575; break;
|
|
46
37
|
default: budget = floor;
|
|
47
38
|
}
|
|
48
39
|
if (typeof maxTokens === "number") budget = Math.min(budget, Math.max(floor, maxTokens - 1024));
|
|
49
40
|
return budget;
|
|
50
41
|
}
|
|
51
42
|
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
43
|
+
/** In-name thinking depth marker. An UNSET effort normally falls to the floor (off for
|
|
44
|
+
* flash-class). But when the model VARIANT name itself encodes a depth — `-high`/`-low`
|
|
45
|
+
* (e.g. gemini-3-pro-high) or an explicit `-thinking` suffix (gemini-2.5-flash-thinking) —
|
|
46
|
+
* that selection IS the user's thinking opt-in. Unmarked ids (gemini-3-flash,
|
|
47
|
+
* gemini-2.5-flash) keep the off-by-default behavior (parity with the other providers +
|
|
48
|
+
* the documented thought-token burn protection). Expects a lowercased id. */
|
|
49
|
+
function geminiNamedEffort(m: string): CallOptions["reasoningEffort"] | undefined {
|
|
50
|
+
return m.includes("-high") ? "high"
|
|
51
|
+
: m.includes("-low") ? "low"
|
|
52
|
+
: m.includes("thinking") ? "medium"
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Gemini 3.x is driven by the `thinkingLevel` ENUM, not a numeric budget (gjc
|
|
57
|
+
* inferThinkingControlMode: gemini family with version.major === 3 → "google-level";
|
|
58
|
+
* everything else stays on the numeric thinkingBudget). */
|
|
59
|
+
export function geminiUsesThinkingLevel(model: string): boolean {
|
|
60
|
+
const ver = model.toLowerCase().match(/gemini-(\d+)(?:\.(\d+))?/);
|
|
61
|
+
return !!ver && Number(ver[1]) === 3;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Effort → Google `thinkingLevel` enum value (gjc mapEffortToGoogleThinkingLevel):
|
|
65
|
+
* minimal→MINIMAL, low→LOW, medium→MEDIUM, high/xhigh→HIGH. */
|
|
66
|
+
export function geminiThinkingLevel(effort: NonNullable<CallOptions["reasoningEffort"]> | "xhigh"): "MINIMAL" | "LOW" | "MEDIUM" | "HIGH" {
|
|
67
|
+
switch (effort) {
|
|
68
|
+
case "minimal": return "MINIMAL";
|
|
69
|
+
case "low": return "LOW";
|
|
70
|
+
case "medium": return "MEDIUM";
|
|
71
|
+
case "high":
|
|
72
|
+
case "xhigh": return "HIGH";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The `thinkingConfig` object for a Gemini request, or undefined to omit it entirely:
|
|
77
|
+
* gemini-3.x → thinkingLevel enum (numeric budgets are NOT sent — gjc emits level XOR
|
|
78
|
+
* budget, never both); with no effort requested (and no in-name marker) the config is
|
|
79
|
+
* omitted so the model keeps its default (gjc parity: thinking not enabled → no config).
|
|
80
|
+
* Other thinking-capable models → numeric thinkingBudget; pre-2.5 → undefined. */
|
|
81
|
+
export type GeminiThinkingConfig =
|
|
82
|
+
| { includeThoughts: true; thinkingLevel: "MINIMAL" | "LOW" | "MEDIUM" | "HIGH" }
|
|
83
|
+
| { includeThoughts: true; thinkingBudget: number };
|
|
84
|
+
|
|
85
|
+
export function geminiThinkingConfig(model: string, effort?: CallOptions["reasoningEffort"], maxTokens?: number): GeminiThinkingConfig | undefined {
|
|
86
|
+
if (geminiUsesThinkingLevel(model)) {
|
|
87
|
+
const effective = effort ?? geminiNamedEffort(model.toLowerCase());
|
|
88
|
+
if (effective === undefined) return undefined;
|
|
89
|
+
return { includeThoughts: true, thinkingLevel: geminiThinkingLevel(effective) };
|
|
90
|
+
}
|
|
91
|
+
const budget = geminiThinkingBudget(model, effort, maxTokens);
|
|
92
|
+
return budget === undefined ? undefined : { includeThoughts: true, thinkingBudget: budget };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** True when this turn was asked to think (a thinkingLevel or a positive budget) — lets
|
|
96
|
+
* the streaming paths fire onReasoningStart so the UI shows the thinking phase even
|
|
97
|
+
* before/without any `thought` parts arrive (the Gemini/CCA analog of Anthropic's
|
|
98
|
+
* content_block_start thinking signal, which is why reasoning otherwise appeared
|
|
99
|
+
* "not to run" on gemini/antigravity). */
|
|
56
100
|
export function geminiThinkingActive(options: CallOptions): boolean {
|
|
57
101
|
const model = options.model.replace(/^(google|gemini)\//, "");
|
|
58
|
-
const
|
|
59
|
-
return
|
|
102
|
+
const cfg = geminiThinkingConfig(model, options.reasoningEffort, options.maxTokens);
|
|
103
|
+
return cfg !== undefined && ("thinkingLevel" in cfg || cfg.thinkingBudget > 0);
|
|
60
104
|
}
|
|
61
105
|
|
|
62
106
|
|
|
@@ -76,8 +120,8 @@ export function buildGeminiPayload(messages: Message[], options: CallOptions, st
|
|
|
76
120
|
if (!geminiModel || geminiModel.startsWith("claude-")) geminiModel = "gemini-2.0-flash";
|
|
77
121
|
|
|
78
122
|
const systemPrompt = options.systemPrompt ?? messages.find(m => m.role === "system")?.content;
|
|
79
|
-
const
|
|
80
|
-
const thinkingEnabled =
|
|
123
|
+
const thinking = geminiThinkingConfig(geminiModel, options.reasoningEffort, options.maxTokens);
|
|
124
|
+
const thinkingEnabled = thinking !== undefined && !stripArtifacts;
|
|
81
125
|
// Gemini requires strictly ALTERNATING user/model turns. jeo histories can carry
|
|
82
126
|
// consecutive same-role messages (a compaction summary prepended before a tool-result,
|
|
83
127
|
// back-to-back tool results, etc.), so coalesce adjacent same-role turns into one
|
|
@@ -126,7 +170,17 @@ export function buildGeminiPayload(messages: Message[], options: CallOptions, st
|
|
|
126
170
|
|
|
127
171
|
// includeThoughts: required for Gemini to STREAM thought summaries (the `thought:true`
|
|
128
172
|
// parts thoughtOf() routes to onReasoning) — without it the model thinks silently.
|
|
129
|
-
if (
|
|
173
|
+
if (thinking !== undefined) {
|
|
174
|
+
generationConfig.thinkingConfig = thinking;
|
|
175
|
+
if ("thinkingBudget" in thinking && thinking.thinkingBudget > 0) {
|
|
176
|
+
// Additive budget (gjc stream.ts): the caller's maxTokens is the DESIRED OUTPUT;
|
|
177
|
+
// thought tokens ride ON TOP of it, capped at the model's output limit — otherwise
|
|
178
|
+
// thinking silently eats the visible-answer budget (the MAX_TOKENS empty-reply bug).
|
|
179
|
+
// ponytail: 65536 hard cap — swap in the model's catalog max-output once a cheap
|
|
180
|
+
// per-model lookup is available at this layer.
|
|
181
|
+
generationConfig.maxOutputTokens = Math.min((options.maxTokens ?? 4000) + thinking.thinkingBudget, 65536);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
130
184
|
|
|
131
185
|
const payload: Record<string, unknown> = { contents, generationConfig };
|
|
132
186
|
if (systemPrompt) payload.systemInstruction = { parts: [{ text: systemPrompt }] };
|
|
@@ -158,7 +212,7 @@ const CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
|
158
212
|
|
|
159
213
|
/** gemini-cli identification headers Cloud Code Assist expects (gjc parity). */
|
|
160
214
|
export function getGeminiCliHeaders(modelId?: string): Record<string, string> {
|
|
161
|
-
const version = jeoEnv("GEMINI_CLI_VERSION") || "0.
|
|
215
|
+
const version = jeoEnv("GEMINI_CLI_VERSION") || "0.49.0";
|
|
162
216
|
return {
|
|
163
217
|
"User-Agent": `GeminiCLI/${version}/${modelId ?? "gemini-2.5-flash"} (${process.platform}; ${process.arch}; terminal)`,
|
|
164
218
|
"Client-Metadata": "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI",
|
package/src/ai/providers/kimi.ts
CHANGED
|
@@ -1,15 +1,54 @@
|
|
|
1
1
|
import type { Credential } from "../../auth";
|
|
2
|
+
import { getKimiCommonHeaders } from "../../auth/flows/kimi";
|
|
3
|
+
import type { CallOptions, ProviderAdapter } from "../types";
|
|
4
|
+
import { anthropicAdapter } from "./anthropic";
|
|
2
5
|
import { makeOpenAICompatibleAdapter } from "./openai-compatible";
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
|
-
* Kimi (Moonshot) —
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `reasoning_content`/`<think
|
|
8
|
+
* Kimi (Moonshot) — two credential-dependent backends (gjc parity):
|
|
9
|
+
* - API key (KIMI_API_KEY / `providers.kimi`): OpenAI-compatible cloud API at
|
|
10
|
+
* https://api.moonshot.ai/v1. Thinking models (kimi-thinking-preview) stream
|
|
11
|
+
* reasoning via `reasoning_content`/`<think>` → onReasoning.
|
|
12
|
+
* - OAuth (Kimi Code subscription, device-code login): Anthropic-compatible API at
|
|
13
|
+
* https://api.kimi.com/coding (`${base}/v1/messages` pinned by the anthropic
|
|
14
|
+
* adapter), authenticated with `Authorization: Bearer` + X-Msh-* device headers.
|
|
9
15
|
*/
|
|
10
16
|
export const KIMI_BASE_URL = "https://api.moonshot.ai/v1";
|
|
17
|
+
// gjc: KIMI_ANTHROPIC_BASE_URL — the SDK/adapter appends /v1/messages, so no /v1 here.
|
|
18
|
+
export const KIMI_ANTHROPIC_BASE_URL = "https://api.kimi.com/coding";
|
|
11
19
|
|
|
12
|
-
|
|
20
|
+
const openaiCompatKimi = makeOpenAICompatibleAdapter({ name: "kimi", baseUrl: KIMI_BASE_URL });
|
|
21
|
+
|
|
22
|
+
/** Route the `kimi/` prefix + OAuth base/headers onto the anthropic adapter. */
|
|
23
|
+
// ponytail: model ids pass through untranslated — the moonshot catalog ids (kimi-latest, …)
|
|
24
|
+
// may not exist on the Kimi Code endpoint (which serves e.g. kimi-for-coding). Upgrade path:
|
|
25
|
+
// add kimi-code entries to model-catalog.ts and map them here per credential kind.
|
|
26
|
+
function prepOAuth(o: CallOptions): CallOptions {
|
|
27
|
+
return {
|
|
28
|
+
...o,
|
|
29
|
+
model: o.model.startsWith("kimi/") ? o.model.slice(5) : o.model,
|
|
30
|
+
baseUrl: o.baseUrl ?? KIMI_ANTHROPIC_BASE_URL,
|
|
31
|
+
extraHeaders: { ...getKimiCommonHeaders(), ...o.extraHeaders },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Dispatch on credential kind: OAuth → Kimi Code Anthropic endpoint; else the
|
|
36
|
+
* original OpenAI-compatible moonshot adapter (API-key behavior unchanged). */
|
|
37
|
+
export const kimiAdapter: ProviderAdapter = {
|
|
38
|
+
name: "kimi",
|
|
39
|
+
supportsNativeTools: openaiCompatKimi.supportsNativeTools,
|
|
40
|
+
call: (messages, options, credential) =>
|
|
41
|
+
credential.kind === "oauth"
|
|
42
|
+
? anthropicAdapter.call(messages, prepOAuth(options), credential)
|
|
43
|
+
: openaiCompatKimi.call(messages, options, credential),
|
|
44
|
+
async *stream(messages, options, credential) {
|
|
45
|
+
if (credential.kind === "oauth") {
|
|
46
|
+
yield* anthropicAdapter.stream!(messages, prepOAuth(options), credential);
|
|
47
|
+
} else {
|
|
48
|
+
yield* openaiCompatKimi.stream!(messages, options, credential);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
13
52
|
|
|
14
53
|
/** Credential carrier for Kimi calls — an api_key bearer (the adapter only reads the
|
|
15
54
|
* token); a keyless `none` when no key is set. */
|
|
@@ -15,11 +15,21 @@ import type { CallOptions, Message } from "../types";
|
|
|
15
15
|
import { readSse } from "../sse";
|
|
16
16
|
import { providerHttpError, fetchWithArtifactFailSafe } from "./errors";
|
|
17
17
|
import { serializeAccumulatedToolCalls } from "../../agent/tool-schemas";
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
import pkg from "../../../package.json";
|
|
18
20
|
|
|
19
21
|
export const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
20
22
|
|
|
21
23
|
export const VALID_REASONING_EFFORTS = new Set(["minimal", "low", "medium", "high"]);
|
|
22
24
|
|
|
25
|
+
const CODEX_ORIGINATOR = "codex_cli_rs";
|
|
26
|
+
|
|
27
|
+
/** Codex-CLI-shaped User-Agent (gjc getCodexUserAgent parity: `originator/version (platform release; arch)`).
|
|
28
|
+
* Sent on BOTH the OAuth and api-key paths — without it Bun's default UA leaks to the backend. */
|
|
29
|
+
export function codexUserAgent(): string {
|
|
30
|
+
return `${CODEX_ORIGINATOR}/${pkg.version} (${os.platform()} ${os.release()}; ${os.arch()})`;
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
/** Extract `chatgpt_account_id` from a ChatGPT/Codex OAuth access JWT. */
|
|
24
34
|
export function extractChatgptAccountId(token: string): string | undefined {
|
|
25
35
|
const parts = token.split(".");
|
|
@@ -93,12 +103,16 @@ export function codexResponsesRequest(
|
|
|
93
103
|
const token = credential.kind === "none" ? "" : credential.token;
|
|
94
104
|
const systemPrompt = options.systemPrompt ?? messages.find(m => m.role === "system")?.content;
|
|
95
105
|
const input = buildResponsesInput(messages, options.model, stripArtifacts);
|
|
106
|
+
const promptCacheKey = normalizePromptCacheKey(options.sessionKey);
|
|
96
107
|
const payload: Record<string, unknown> = {
|
|
97
108
|
model,
|
|
98
109
|
instructions: systemPrompt ?? "You are a helpful coding assistant.",
|
|
99
110
|
input,
|
|
100
111
|
stream: true, // the Codex backend only streams
|
|
101
112
|
store: false,
|
|
113
|
+
// Provider-side prompt caching (gjc parity): key the cache on the stable session id
|
|
114
|
+
// so an agent loop replaying the same history each step re-reads a cached prefix.
|
|
115
|
+
...(promptCacheKey ? { prompt_cache_key: promptCacheKey } : {}),
|
|
102
116
|
};
|
|
103
117
|
if (options.tools?.length) {
|
|
104
118
|
// Responses API function tools (flat shape). tool_choice "auto" keeps prose + `done`.
|
|
@@ -112,6 +126,9 @@ export function codexResponsesRequest(
|
|
|
112
126
|
// frame can show the model's thinking instead of a frozen "calling model (Ns)…".
|
|
113
127
|
payload.reasoning = { effort: options.reasoningEffort, summary: "auto" };
|
|
114
128
|
}
|
|
129
|
+
// Cap the response length (gjc parity): the computed per-call budget must reach the wire,
|
|
130
|
+
// otherwise the backend free-runs to its own default.
|
|
131
|
+
if (options.maxTokens) payload.max_output_tokens = options.maxTokens;
|
|
115
132
|
// OAuth → the undocumented ChatGPT/Codex backend (codex headers + account-id).
|
|
116
133
|
// API key → the public OpenAI Responses API (`/v1/responses`) with a plain Bearer.
|
|
117
134
|
// Both speak the same Responses schema (the body above), so only url+headers differ.
|
|
@@ -122,7 +139,7 @@ export function codexResponsesRequest(
|
|
|
122
139
|
payload.include = ["reasoning.encrypted_content"];
|
|
123
140
|
return {
|
|
124
141
|
url: `${base}/responses`,
|
|
125
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${token}`, accept: "text/event-stream" },
|
|
142
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}`, accept: "text/event-stream", "user-agent": codexUserAgent() },
|
|
126
143
|
body: JSON.stringify(payload),
|
|
127
144
|
};
|
|
128
145
|
}
|
|
@@ -131,13 +148,30 @@ export function codexResponsesRequest(
|
|
|
131
148
|
"content-type": "application/json",
|
|
132
149
|
authorization: `Bearer ${token}`,
|
|
133
150
|
"OpenAI-Beta": "responses=experimental",
|
|
134
|
-
originator:
|
|
151
|
+
originator: CODEX_ORIGINATOR,
|
|
152
|
+
"user-agent": codexUserAgent(),
|
|
135
153
|
accept: "text/event-stream",
|
|
136
154
|
};
|
|
137
155
|
if (accountId) headers["chatgpt-account-id"] = accountId;
|
|
156
|
+
// Correlation headers (gjc parity): the Codex backend keys its prompt cache on the
|
|
157
|
+
// session/conversation pair; x-client-request-id aids server-side tracing.
|
|
158
|
+
if (promptCacheKey) {
|
|
159
|
+
headers.session_id = promptCacheKey;
|
|
160
|
+
headers.conversation_id = promptCacheKey;
|
|
161
|
+
headers["x-client-request-id"] = promptCacheKey;
|
|
162
|
+
}
|
|
138
163
|
return { url: CODEX_RESPONSES_URL, headers, body: JSON.stringify(payload) };
|
|
139
164
|
}
|
|
140
165
|
|
|
166
|
+
/** gjc parity (normalizeOpenAIResponsesPromptCacheKey): well-formed unicode, ≤64 chars
|
|
167
|
+
* verbatim, longer keys hashed to a stable compact form. */
|
|
168
|
+
export function normalizePromptCacheKey(sessionKey: string | undefined): string | undefined {
|
|
169
|
+
if (!sessionKey || sessionKey.length === 0) return undefined;
|
|
170
|
+
const wellFormed = sessionKey.toWellFormed();
|
|
171
|
+
if (wellFormed.length <= 64) return wellFormed;
|
|
172
|
+
return `pc_${Bun.hash(wellFormed).toString(36)}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
141
175
|
export interface ResponsesEvent {
|
|
142
176
|
delta?: string;
|
|
143
177
|
/** Reasoning-summary text delta (`response.reasoning_summary_text.delta` and the
|
package/src/ai/types.ts
CHANGED
|
@@ -113,6 +113,12 @@ export interface CallOptions {
|
|
|
113
113
|
* `enable_thinking: true`; "zai" → `thinking: {type:"enabled"}`. Set per provider by
|
|
114
114
|
* the openai-compatible factory; without it a model never emits reasoning to surface. */
|
|
115
115
|
reasoningFormat?: "openai" | "openrouter" | "qwen" | "zai";
|
|
116
|
+
/** Stable per-conversation key (the session id) for provider-side prompt caching.
|
|
117
|
+
* gjc parity: the Codex/Responses backends key their prompt cache on
|
|
118
|
+
* `prompt_cache_key` (+ `session_id`/`conversation_id` headers on the Codex
|
|
119
|
+
* backend), so an agent loop replaying the same history each step gets cache
|
|
120
|
+
* hits instead of full-prompt re-reads. Optional — absent for one-shot calls. */
|
|
121
|
+
sessionKey?: string;
|
|
116
122
|
/** Notified before each auto-retry backoff wait (rate limits / transient errors).
|
|
117
123
|
* NOT forwarded to provider adapters — consumed by the manager's retry layer. */
|
|
118
124
|
onRetry?: (attempt: number, err: unknown, delayMs: number) => void;
|
|
@@ -140,6 +146,10 @@ export interface CallOptions {
|
|
|
140
146
|
* these on the wire and re-serialize the structured tool call back into the engine's
|
|
141
147
|
* canonical {"tool":...}/{"tools":[...]} string; others ignore it. */
|
|
142
148
|
tools?: NativeToolSchema[];
|
|
149
|
+
/** Extra HTTP headers merged into the outgoing provider request (adapter-specific;
|
|
150
|
+
* currently honored by the anthropic adapter). Used by Anthropic-compatible OAuth
|
|
151
|
+
* providers — e.g. Kimi Code sends its X-Msh-* device headers on every call. */
|
|
152
|
+
extraHeaders?: Record<string, string>;
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
export interface ProviderAdapter {
|
|
@@ -27,6 +27,19 @@ export interface OAuthCallbackFlowOptions {
|
|
|
27
27
|
|
|
28
28
|
export type CallbackResult = { code: string; state: string };
|
|
29
29
|
|
|
30
|
+
/** Thrown when the fixed callback port cannot be bound and the provider forbids a
|
|
31
|
+
* port fallback (fixed redirect URI). Typed so callers (e.g. the OpenAI flow) can
|
|
32
|
+
* detect the bind failure and fall back to the device-code flow instead of
|
|
33
|
+
* string-matching the error message. */
|
|
34
|
+
export class CallbackPortUnavailableError extends Error {
|
|
35
|
+
readonly port: number;
|
|
36
|
+
constructor(port: number) {
|
|
37
|
+
super(`OAuth callback port ${port} unavailable and this provider requires a fixed redirect URI.`);
|
|
38
|
+
this.name = "CallbackPortUnavailableError";
|
|
39
|
+
this.port = port;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
30
43
|
// "jeo" wordmark — bold forged monospace lettering on a neon blue→violet→pink
|
|
31
44
|
// gradient (the mascot's synthwave palette), generated via god-tibo-imagen and
|
|
32
45
|
// matching jeo-pi's bold-wordmark typographic treatment. Embedded as a data URI
|
|
@@ -125,6 +138,7 @@ export abstract class OAuthCallbackFlow {
|
|
|
125
138
|
Bun.serve({
|
|
126
139
|
hostname: this.callbackHostname,
|
|
127
140
|
port,
|
|
141
|
+
reusePort: false, // gjc parity: never share the port — a busy port must FAIL, not silently split traffic
|
|
128
142
|
fetch: req => this.#handle(req, expectedState),
|
|
129
143
|
});
|
|
130
144
|
try {
|
|
@@ -134,9 +148,7 @@ export abstract class OAuthCallbackFlow {
|
|
|
134
148
|
return { server, redirectUri };
|
|
135
149
|
} catch {
|
|
136
150
|
if (this.fixedRedirectUri) {
|
|
137
|
-
throw new
|
|
138
|
-
`OAuth callback port ${this.preferredPort} unavailable and this provider requires a fixed redirect URI.`
|
|
139
|
-
);
|
|
151
|
+
throw new CallbackPortUnavailableError(this.preferredPort);
|
|
140
152
|
}
|
|
141
153
|
const server = serve(0);
|
|
142
154
|
const redirectUri = `http://${this.callbackHostname}:${server.port}${this.callbackPath}`;
|
|
@@ -172,7 +184,7 @@ export abstract class OAuthCallbackFlow {
|
|
|
172
184
|
});
|
|
173
185
|
|
|
174
186
|
return new Response(renderHtml(ok, message), {
|
|
175
|
-
status: ok ? 200 : 400
|
|
187
|
+
status: ok ? 200 : 500, // gjc parity: provider/CSRF failures are server-side errors (500), not 400
|
|
176
188
|
headers: { "content-type": "text/html" },
|
|
177
189
|
});
|
|
178
190
|
}
|
|
@@ -206,7 +218,9 @@ export abstract class OAuthCallbackFlow {
|
|
|
206
218
|
.then(input => {
|
|
207
219
|
const parsed = parseCallbackInput(input);
|
|
208
220
|
if (!parsed.code) return null;
|
|
209
|
-
|
|
221
|
+
// gjc parity: a bare pasted code WITHOUT a state is accepted (the provider
|
|
222
|
+
// may strip it); only an explicitly MISMATCHED state is rejected.
|
|
223
|
+
if (expectedState && parsed.state && parsed.state !== expectedState) return null;
|
|
210
224
|
return { code: parsed.code, state: parsed.state ?? "" } as CallbackResult;
|
|
211
225
|
})
|
|
212
226
|
.catch(() => null),
|
package/src/auth/flows/AGENTS.md
CHANGED
|
@@ -9,12 +9,13 @@ Specific OAuth flow implementations for various providers.
|
|
|
9
9
|
## Key Files
|
|
10
10
|
| File | Description |
|
|
11
11
|
|------|-------------|
|
|
12
|
-
| `anthropic.ts` |
|
|
13
|
-
| `antigravity.ts` |
|
|
14
|
-
| `google-project.ts` |
|
|
15
|
-
| `google.ts` |
|
|
16
|
-
| `index.ts` |
|
|
17
|
-
| `
|
|
12
|
+
| `anthropic.ts` | Anthropic (claude.ai) PKCE OAuth |
|
|
13
|
+
| `antigravity.ts` | Google Antigravity OAuth (desktop-app client, dedicated project discovery) |
|
|
14
|
+
| `google-project.ts` | Cloud Code Assist project discovery/onboarding shared by gemini + antigravity |
|
|
15
|
+
| `google.ts` | Google Gemini CLI OAuth (authorization-code) |
|
|
16
|
+
| `index.ts` | OAUTH_FLOW_REGISTRY wiring flows to providers |
|
|
17
|
+
| `kimi.ts` | Kimi Code (Moonshot subscription) device-authorization OAuth — no callback server |
|
|
18
|
+
| `openai.ts` | OpenAI Codex PKCE OAuth + device-code fallback when port 1455 is busy |
|
|
18
19
|
|
|
19
20
|
## Subdirectories
|
|
20
21
|
*(None)*
|
|
@@ -56,6 +56,9 @@ export function discoverAntigravityProjectId(
|
|
|
56
56
|
return discoverGoogleProjectId(accessToken, {
|
|
57
57
|
metadata: { ...ANTIGRAVITY_DISCOVERY_METADATA },
|
|
58
58
|
extraHeaders: { "User-Agent": getAntigravityUserAgent() },
|
|
59
|
+
// gjc antigravity parity: a reported tier without a project ONBOARDS the
|
|
60
|
+
// default/legacy tier instead of throwing the workspace-project hint.
|
|
61
|
+
alwaysOnboard: true,
|
|
59
62
|
onProgress: opts.onProgress,
|
|
60
63
|
});
|
|
61
64
|
}
|
|
@@ -56,6 +56,11 @@ export interface DiscoverProjectOptions {
|
|
|
56
56
|
/** Per-request deadline; a stalled discovery fetch must NEVER hang the turn
|
|
57
57
|
* forever (round-5 #2 — this path runs BEFORE the manager's guarded call). */
|
|
58
58
|
requestTimeoutMs?: number;
|
|
59
|
+
/** Antigravity semantics (gjc google-antigravity discoverProject): when the account
|
|
60
|
+
* reports a tier but names no project, ONBOARD the default/legacy tier instead of
|
|
61
|
+
* demanding an explicit workspace project. The gemini-cli flow keeps the strict
|
|
62
|
+
* workspace hint (default false). */
|
|
63
|
+
alwaysOnboard?: boolean;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
const DISCOVERY_REQUEST_TIMEOUT_MS = 30_000;
|
|
@@ -147,12 +152,13 @@ export async function discoverGoogleProjectId(
|
|
|
147
152
|
if (data.currentTier) {
|
|
148
153
|
const existing = readProjectId(data.cloudaicompanionProject) ?? envProjectId;
|
|
149
154
|
if (existing) return existing;
|
|
150
|
-
throw new Error(WORKSPACE_PROJECT_HINT);
|
|
155
|
+
if (!opts.alwaysOnboard) throw new Error(WORKSPACE_PROJECT_HINT);
|
|
156
|
+
// alwaysOnboard: fall through and provision the default/legacy tier (gjc antigravity parity).
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
// Not onboarded yet: provision the default tier (free tier needs no project).
|
|
154
160
|
const tierId = defaultTierId(data.allowedTiers) || TIER_FREE;
|
|
155
|
-
if (tierId !== TIER_FREE && tierId !== TIER_LEGACY && !envProjectId) {
|
|
161
|
+
if (!opts.alwaysOnboard && tierId !== TIER_FREE && tierId !== TIER_LEGACY && !envProjectId) {
|
|
156
162
|
throw new Error(WORKSPACE_PROJECT_HINT);
|
|
157
163
|
}
|
|
158
164
|
|
package/src/auth/flows/google.ts
CHANGED
|
@@ -9,8 +9,22 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { OAuthCallbackFlow } from "../callback-server";
|
|
11
11
|
import { discoverGoogleProjectId } from "./google-project";
|
|
12
|
+
import { getGeminiCliHeaders } from "../../ai/providers/gemini";
|
|
12
13
|
import type { OAuthController, OAuthCredentials } from "../types";
|
|
13
14
|
|
|
15
|
+
/** Login-time Cloud Code Assist project discovery for a gemini-cli token. Sends the
|
|
16
|
+
* gemini-cli identity headers (User-Agent + Client-Metadata) on loadCodeAssist/
|
|
17
|
+
* onboardUser, matching gjc's google-gemini-cli discovery requests. */
|
|
18
|
+
export function discoverGeminiProjectId(
|
|
19
|
+
accessToken: string,
|
|
20
|
+
opts: { onProgress?: (message: string) => void } = {},
|
|
21
|
+
): Promise<string> {
|
|
22
|
+
return discoverGoogleProjectId(accessToken, {
|
|
23
|
+
extraHeaders: getGeminiCliHeaders(),
|
|
24
|
+
onProgress: opts.onProgress,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
const decode = (s: string) => atob(s);
|
|
15
29
|
const CLIENT_ID = decode(
|
|
16
30
|
[
|
|
@@ -101,7 +115,7 @@ class GoogleOAuthFlow extends OAuthCallbackFlow {
|
|
|
101
115
|
// gjc parity: auto-discover (or provision) the Cloud Code Assist project so
|
|
102
116
|
// Antigravity models work straight after login — best-effort, never fails login.
|
|
103
117
|
try {
|
|
104
|
-
projectId = await
|
|
118
|
+
projectId = await discoverGeminiProjectId(data.access_token);
|
|
105
119
|
} catch {
|
|
106
120
|
projectId = undefined;
|
|
107
121
|
}
|
package/src/auth/flows/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { loginAnthropic, refreshAnthropicToken } from "./anthropic";
|
|
|
5
5
|
import { loginOpenAI, refreshOpenAIToken } from "./openai";
|
|
6
6
|
import { loginGoogle, refreshGoogleToken } from "./google";
|
|
7
7
|
import { loginAntigravity, refreshAntigravityToken } from "./antigravity";
|
|
8
|
+
import { loginKimi, refreshKimiToken } from "./kimi";
|
|
8
9
|
|
|
9
10
|
export interface OAuthFlow {
|
|
10
11
|
readonly provider: OAuthProvider;
|
|
@@ -52,9 +53,18 @@ export const OAUTH_FLOW_REGISTRY: Record<OAuthProvider, OAuthFlow> = {
|
|
|
52
53
|
verifiedEndToEnd: true,
|
|
53
54
|
note: "Antigravity desktop-app OAuth client; serves antigravity/* models (Gemini 3, Claude, GPT-OSS via Cloud Code Assist). The Google Cloud projectId is discovered automatically at login.",
|
|
54
55
|
},
|
|
56
|
+
kimi: {
|
|
57
|
+
provider: "kimi",
|
|
58
|
+
label: "Kimi Code (Moonshot subscription)",
|
|
59
|
+
login: loginKimi,
|
|
60
|
+
refresh: refreshKimiToken,
|
|
61
|
+
verifiedEndToEnd: true,
|
|
62
|
+
note: "Device-code OAuth (auth.kimi.com); the token is served via the Anthropic-compatible endpoint at api.kimi.com/coding. A KIMI_API_KEY, when set and no OAuth is stored, uses the OpenAI-compatible api.moonshot.ai/v1.",
|
|
63
|
+
},
|
|
55
64
|
};
|
|
56
65
|
|
|
57
66
|
export { loginAnthropic, refreshAnthropicToken } from "./anthropic";
|
|
58
67
|
export { loginOpenAI, refreshOpenAIToken } from "./openai";
|
|
59
68
|
export { loginGoogle, refreshGoogleToken } from "./google";
|
|
60
69
|
export { loginAntigravity, refreshAntigravityToken, discoverAntigravityProjectId, antigravityClientSecret } from "./antigravity";
|
|
70
|
+
export { loginKimi, refreshKimiToken, getKimiCommonHeaders } from "./kimi";
|