jeo-code 0.7.27 → 0.7.29
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 +26 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +1 -1
- package/src/agent/config-schema.ts +1 -0
- package/src/agent/engine.ts +43 -17
- package/src/agent/loop-guards.ts +7 -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/provider-error.ts +1 -1
- package/src/util/retry.ts +91 -33
|
@@ -12,9 +12,14 @@ const CLAUDE_CODE_VERSION = "2.1.63";
|
|
|
12
12
|
const CLAUDE_CODE_SYSTEM_INSTRUCTION = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
13
13
|
const CLAUDE_BILLING_HEADER_PREFIX = "x-anthropic-billing-header:";
|
|
14
14
|
/** Betas needed for API-key requests: interleaved-thinking enables thinking+tools,
|
|
15
|
-
*
|
|
15
|
+
* context-management matches gjc's default beta set (sent on ALL requests),
|
|
16
|
+
* prompt-caching-scope gives scoped cache breakpoints.
|
|
17
|
+
* ponytail: fine-grained-tool-streaming-2025-05-14 — gjc sends it and repairs truncated
|
|
18
|
+
* tool-argument JSON; jeo parses accumulated tool JSON without that repair pass, so the
|
|
19
|
+
* beta stays off until a repair step exists. */
|
|
16
20
|
const ANTHROPIC_API_KEY_BETA = [
|
|
17
21
|
"interleaved-thinking-2025-05-14",
|
|
22
|
+
"context-management-2025-06-27",
|
|
18
23
|
"prompt-caching-scope-2026-01-05",
|
|
19
24
|
];
|
|
20
25
|
const ANTHROPIC_OAUTH_BETA = [
|
|
@@ -46,8 +51,10 @@ function stripAnthropicPrefix(model: string): string {
|
|
|
46
51
|
return model.startsWith("anthropic/") ? model.slice(10) : model;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
function shouldUseClaudeCodeOAuthShape(model: string, credential: Credential): boolean {
|
|
50
|
-
|
|
54
|
+
function shouldUseClaudeCodeOAuthShape(model: string, credential: Credential, baseUrl?: string): boolean {
|
|
55
|
+
// Claude-Code OAuth cloaking is for api.anthropic.com only. An OAuth credential
|
|
56
|
+
// against a custom base (e.g. Kimi Code at api.kimi.com/coding) is a plain bearer.
|
|
57
|
+
return credential.kind === "oauth" && !baseUrl && !model.startsWith("claude-3-5-haiku");
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
function createClaudeCloakingUserId(): string {
|
|
@@ -68,9 +75,10 @@ function anthropicSystemBlocks(
|
|
|
68
75
|
model: string,
|
|
69
76
|
credential: Credential,
|
|
70
77
|
billingPayload: Record<string, unknown>,
|
|
78
|
+
baseUrl?: string,
|
|
71
79
|
): AnthropicSystemBlock[] | undefined {
|
|
72
80
|
const blocks: AnthropicSystemBlock[] = [];
|
|
73
|
-
if (shouldUseClaudeCodeOAuthShape(model, credential)) {
|
|
81
|
+
if (shouldUseClaudeCodeOAuthShape(model, credential, baseUrl)) {
|
|
74
82
|
const billingSeed = systemPrompt ? { ...billingPayload, system: [systemPrompt] } : billingPayload;
|
|
75
83
|
blocks.push(
|
|
76
84
|
{ type: "text", text: createClaudeBillingHeader(billingSeed) },
|
|
@@ -89,17 +97,19 @@ function anthropicSystemBlocks(
|
|
|
89
97
|
return blocks;
|
|
90
98
|
}
|
|
91
99
|
|
|
92
|
-
/** Anthropic extended-thinking budget by reasoning effort (kept under max_tokens).
|
|
93
|
-
*
|
|
100
|
+
/** Anthropic extended-thinking budget by reasoning effort (kept under max_tokens). Tiers match
|
|
101
|
+
* gjc's ANTHROPIC_THINKING table: minimal/low/medium/high ALL enable thinking with scaling
|
|
94
102
|
* depth — reasoning works at every thinking level (gajae parity: Minimal is a real effort).
|
|
95
|
-
* Only an UNSET effort stays non-thinking (the explicit /fast off path).
|
|
96
|
-
|
|
103
|
+
* Only an UNSET effort stays non-thinking (the explicit /fast off path). The "xhigh" row is
|
|
104
|
+
* correct-by-table even though upstream currently folds xhigh→high before it arrives. */
|
|
105
|
+
function anthropicThinkingBudget(effort: CallOptions["reasoningEffort"] | "xhigh", maxTokens: number): number | undefined {
|
|
97
106
|
let budget: number;
|
|
98
107
|
switch (effort) {
|
|
99
|
-
case "minimal": budget =
|
|
100
|
-
case "low": budget =
|
|
101
|
-
case "medium": budget =
|
|
102
|
-
case "high": budget =
|
|
108
|
+
case "minimal": budget = 1024; break;
|
|
109
|
+
case "low": budget = 4096; break;
|
|
110
|
+
case "medium": budget = 8192; break;
|
|
111
|
+
case "high": budget = 16384; break;
|
|
112
|
+
case "xhigh": budget = 32768; break;
|
|
103
113
|
default: return undefined;
|
|
104
114
|
}
|
|
105
115
|
return Math.min(budget, Math.max(1024, maxTokens - 1024));
|
|
@@ -264,7 +274,7 @@ export function anthropicPayload(
|
|
|
264
274
|
// Extended thinking requires max_tokens strictly above the thinking budget.
|
|
265
275
|
max_tokens: thinkingBudget !== undefined ? Math.max(maxTokens, thinkingBudget + 1024) : maxTokens,
|
|
266
276
|
};
|
|
267
|
-
if (credential.
|
|
277
|
+
if (shouldUseClaudeCodeOAuthShape(model, credential, options.baseUrl)) payload.metadata = { user_id: createClaudeCloakingUserId() };
|
|
268
278
|
if (effort !== undefined) {
|
|
269
279
|
// Enable Claude extended thinking. Extended thinking forbids a custom temperature, so
|
|
270
280
|
// temperature is only set on the non-thinking path.
|
|
@@ -294,7 +304,7 @@ export function anthropicPayload(
|
|
|
294
304
|
payload.tool_choice = { type: "auto" };
|
|
295
305
|
}
|
|
296
306
|
if (stream) payload.stream = true;
|
|
297
|
-
const system = anthropicSystemBlocks(systemPrompt, model, credential, payload);
|
|
307
|
+
const system = anthropicSystemBlocks(systemPrompt, model, credential, payload, options.baseUrl);
|
|
298
308
|
if (system) payload.system = system;
|
|
299
309
|
return JSON.stringify(payload);
|
|
300
310
|
}
|
|
@@ -311,7 +321,7 @@ export function anthropicRequest(
|
|
|
311
321
|
// Anthropic-compatible providers (z.ai, MiniMax, …) accept the Messages wire
|
|
312
322
|
// format at their own host; an explicit baseUrl pins `${base}/v1/messages`.
|
|
313
323
|
url: options.baseUrl ? `${options.baseUrl.replace(/\/$/, "")}/v1/messages` : ANTHROPIC_URL,
|
|
314
|
-
headers: headersFor(credential, stream, stripAnthropicPrefix(options.model)),
|
|
324
|
+
headers: { ...headersFor(credential, stream, stripAnthropicPrefix(options.model), options.baseUrl), ...options.extraHeaders },
|
|
315
325
|
body: anthropicPayload(messages, options, stream, includeTemperature, credential, stripArtifacts),
|
|
316
326
|
};
|
|
317
327
|
}
|
|
@@ -327,6 +337,22 @@ function isReasoningArtifactError(status: number, detail: string): boolean {
|
|
|
327
337
|
return status === 400 && /thinking|signature|redacted_thinking/i.test(detail);
|
|
328
338
|
}
|
|
329
339
|
|
|
340
|
+
/** gjc parity (getSafeAnthropicHeaderEvidence): fold Anthropic's rate-limit response
|
|
341
|
+
* headers into a 429's error detail so the retry classifier can see them. The decisive
|
|
342
|
+
* usage-exhaustion signal (`anthropic-ratelimit-unified-overage-disabled-reason:
|
|
343
|
+
* out_of_credits`) often arrives ONLY as a header — without this, an out-of-credits 429
|
|
344
|
+
* reads like a generic per-minute rate limit and is retried pointlessly. */
|
|
345
|
+
function anthropicRateLimitEvidence(headers: Headers): string {
|
|
346
|
+
const evidence: string[] = [];
|
|
347
|
+
for (const [name, value] of headers) {
|
|
348
|
+
const lower = name.toLowerCase();
|
|
349
|
+
if (lower === "retry-after" || lower === "retry-after-ms" || lower.startsWith("anthropic-ratelimit-")) {
|
|
350
|
+
evidence.push(`${lower}=${value}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return evidence.length > 0 ? ` Anthropic rate-limit evidence: ${evidence.sort().join(", ")}` : "";
|
|
354
|
+
}
|
|
355
|
+
|
|
330
356
|
async function postAnthropic(
|
|
331
357
|
messages: Message[],
|
|
332
358
|
options: CallOptions,
|
|
@@ -354,10 +380,13 @@ async function postAnthropic(
|
|
|
354
380
|
throw await providerHttpError("Anthropic", response, stream ? "(stream)" : undefined);
|
|
355
381
|
}
|
|
356
382
|
|
|
383
|
+
// 429s carry their rate-limit headers in the DETAIL so isUsageLimitError /
|
|
384
|
+
// friendlyProviderError can classify on the full evidence (gjc parity).
|
|
385
|
+
const enrichedDetail = response.status === 429 ? `${detail}${anthropicRateLimitEvidence(response.headers)}` : detail;
|
|
357
386
|
throw new ProviderHttpError(
|
|
358
387
|
"Anthropic",
|
|
359
388
|
response.status,
|
|
360
|
-
|
|
389
|
+
enrichedDetail,
|
|
361
390
|
stream ? "(stream)" : undefined,
|
|
362
391
|
parseRetryAfter(response.headers.get("retry-after")) ?? parseRetryFromBody(detail),
|
|
363
392
|
);
|
|
@@ -543,8 +572,18 @@ function claudeCodeOAuthHeaders(stream: boolean, model: string): Record<string,
|
|
|
543
572
|
};
|
|
544
573
|
}
|
|
545
574
|
|
|
546
|
-
function headersFor(credential: Credential, stream: boolean, model: string): Record<string, string> {
|
|
575
|
+
function headersFor(credential: Credential, stream: boolean, model: string, baseUrl?: string): Record<string, string> {
|
|
547
576
|
if (credential.kind === "oauth") {
|
|
577
|
+
// OAuth against a NON-Anthropic base (Kimi Code, …): plain bearer, no Claude-Code
|
|
578
|
+
// cloaking headers/betas. gjc parity: buildAnthropicHeaders' generic-bearer branch.
|
|
579
|
+
if (baseUrl) {
|
|
580
|
+
return {
|
|
581
|
+
accept: stream ? "text/event-stream" : "application/json",
|
|
582
|
+
"content-type": "application/json",
|
|
583
|
+
authorization: `Bearer ${credential.token}`,
|
|
584
|
+
"anthropic-version": "2023-06-01",
|
|
585
|
+
};
|
|
586
|
+
}
|
|
548
587
|
return {
|
|
549
588
|
"content-type": "application/json",
|
|
550
589
|
authorization: `Bearer ${credential.token}`,
|
|
@@ -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),
|