pi-provider-kimi-code 0.6.2 → 0.6.5
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/README.md +7 -5
- package/index.ts +308 -469
- package/package.json +4 -4
- package/src/config.ts +18 -5
- package/src/constants.ts +2 -2
- package/src/device.ts +41 -0
- package/src/models.ts +100 -28
- package/src/oauth.ts +137 -35
- package/src/payload.ts +38 -25
- package/src/project-trust.ts +24 -0
- package/src/settings-ui.ts +204 -0
- package/src/stream.ts +25 -6
- package/src/tools/common.ts +6 -4
- package/src/tools/moonshot.ts +13 -35
- package/src/usage.ts +366 -0
package/src/payload.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { CacheRetention, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
|
6
6
|
import type { KimiResolvedModelConfig, ModelReasoningEntry } from "./config.ts";
|
|
7
7
|
|
|
8
8
|
import { getBaseUrl } from "./constants.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { getKimiProviderHeaders } from "./device.ts";
|
|
10
10
|
import { optimizeToolSchemas } from "./schema-dedup.ts";
|
|
11
11
|
|
|
12
12
|
// =============================================================================
|
|
@@ -116,7 +116,7 @@ export async function uploadKimiFile(
|
|
|
116
116
|
try {
|
|
117
117
|
const response = await fetch(uploadUrl, {
|
|
118
118
|
method: "POST",
|
|
119
|
-
headers: { Authorization: `Bearer ${apiKey}
|
|
119
|
+
headers: { ...getKimiProviderHeaders(), Authorization: `Bearer ${apiKey}` },
|
|
120
120
|
body: formData,
|
|
121
121
|
});
|
|
122
122
|
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
|
|
@@ -425,7 +425,19 @@ export async function applyKimiPayloadMutations(
|
|
|
425
425
|
: { include_usage: true };
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
-
// 5.
|
|
428
|
+
// 5. Spread extra_body into the top-level payload before normalization and
|
|
429
|
+
// config caps. Top-level fields retain precedence over extra_body.
|
|
430
|
+
if (isRecord(payload.extra_body)) {
|
|
431
|
+
const extraBody = payload.extra_body as JsonRecord;
|
|
432
|
+
delete payload.extra_body;
|
|
433
|
+
for (const [key, value] of Object.entries(extraBody)) {
|
|
434
|
+
if (payload[key] === undefined) {
|
|
435
|
+
payload[key] = value;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// 6. Normalize deprecated max_tokens (OpenAI path only — Anthropic
|
|
429
441
|
// /messages uses max_tokens natively).
|
|
430
442
|
if (ctx.api === "openai-completions") {
|
|
431
443
|
if (payload.max_completion_tokens === undefined && typeof payload.max_tokens === "number") {
|
|
@@ -438,44 +450,45 @@ export async function applyKimiPayloadMutations(
|
|
|
438
450
|
if (generation.temperature !== undefined) payload.temperature = generation.temperature;
|
|
439
451
|
if (generation.topP !== undefined) payload.top_p = generation.topP;
|
|
440
452
|
if (generation.maxCompletionTokens !== undefined) {
|
|
441
|
-
|
|
453
|
+
const maxTokensKey = ctx.api === "anthropic-messages" ? "max_tokens" : "max_completion_tokens";
|
|
454
|
+
const currentMaxTokens = payload[maxTokensKey];
|
|
455
|
+
payload[maxTokensKey] =
|
|
456
|
+
typeof currentMaxTokens === "number"
|
|
457
|
+
? Math.min(currentMaxTokens, generation.maxCompletionTokens)
|
|
458
|
+
: generation.maxCompletionTokens;
|
|
442
459
|
}
|
|
443
460
|
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
delete payload.
|
|
449
|
-
for (const [key, value] of Object.entries(extraBody)) {
|
|
450
|
-
if (payload[key] === undefined) {
|
|
451
|
-
payload[key] = value;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
461
|
+
// 7. Reasoning effort mapping. Kimi now accepts effort only inside the
|
|
462
|
+
// thinking object, and only for values advertised by the model catalog.
|
|
463
|
+
delete payload.reasoning_effort;
|
|
464
|
+
if (ctx.modelConfig.supportsThinkingType === "no") {
|
|
465
|
+
delete payload.thinking;
|
|
454
466
|
}
|
|
455
|
-
|
|
456
|
-
// 6. Reasoning effort mapping. Merges into any existing top-level thinking
|
|
457
|
-
// field (which may have come from extra_body above).
|
|
458
467
|
const resolvedReasoning = resolveThinkingLevel(ctx);
|
|
459
468
|
if (resolvedReasoning) {
|
|
460
469
|
const mapped = resolveReasoningForLevel(resolvedReasoning, ctx.modelConfig);
|
|
461
470
|
if (mapped) {
|
|
462
|
-
if (mapped.effort !== null) {
|
|
463
|
-
payload.reasoning_effort = mapped.effort;
|
|
464
|
-
} else {
|
|
465
|
-
delete payload.reasoning_effort;
|
|
466
|
-
}
|
|
467
471
|
const oldThinking = isRecord(payload.thinking) ? payload.thinking : {};
|
|
468
|
-
|
|
472
|
+
const thinking: JsonRecord = {
|
|
469
473
|
...oldThinking,
|
|
470
474
|
type: mapped.enabled ? "enabled" : "disabled",
|
|
471
475
|
};
|
|
476
|
+
delete thinking.effort;
|
|
477
|
+
if (!mapped.enabled) delete thinking.keep;
|
|
478
|
+
const effort = ctx.reasoning
|
|
479
|
+
? mapped.effort
|
|
480
|
+
: (ctx.modelConfig.defaultEffort ?? mapped.effort);
|
|
481
|
+
if (mapped.enabled && effort !== null && ctx.modelConfig.supportEfforts?.includes(effort)) {
|
|
482
|
+
thinking.effort = effort;
|
|
483
|
+
}
|
|
472
484
|
if (mapped.enabled && ctx.modelConfig.thinkingKeep) {
|
|
473
|
-
|
|
485
|
+
thinking.keep = ctx.modelConfig.thinkingKeep;
|
|
474
486
|
}
|
|
487
|
+
payload.thinking = thinking;
|
|
475
488
|
}
|
|
476
489
|
}
|
|
477
490
|
|
|
478
|
-
//
|
|
491
|
+
// 8. K2.7 Code API constraints: the server rejects non-default values for
|
|
479
492
|
// temperature (must be 1.0) and top_p (must be 0.95), and tool_choice
|
|
480
493
|
// "required" / function-specific when thinking is enabled (always-on).
|
|
481
494
|
if (payload.temperature !== undefined && payload.temperature !== 1) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export async function isKimiProjectConfigApproved(ctx: unknown, cwd: string): Promise<boolean> {
|
|
2
|
+
const check = (ctx as { isProjectTrusted?: () => boolean } | undefined)?.isProjectTrusted;
|
|
3
|
+
if (typeof check !== "function") return true;
|
|
4
|
+
|
|
5
|
+
let sessionTrusted: boolean;
|
|
6
|
+
try {
|
|
7
|
+
sessionTrusted = check.call(ctx);
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (!sessionTrusted) return false;
|
|
12
|
+
|
|
13
|
+
const mod = (await import("@earendil-works/pi-coding-agent")) as unknown as {
|
|
14
|
+
ProjectTrustStore?: new (agentDir: string) => { get(cwd: string): boolean | null };
|
|
15
|
+
getAgentDir?: () => string;
|
|
16
|
+
};
|
|
17
|
+
if (!mod.ProjectTrustStore || !mod.getAgentDir) return true;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
return new mod.ProjectTrustStore(mod.getAgentDir()).get(cwd) === true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { relative } from "node:path";
|
|
3
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { SettingsListTheme } from "@earendil-works/pi-tui";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type KimiCodeConfig,
|
|
8
|
+
type KimiCodeConfigSources,
|
|
9
|
+
KIMI_TOOL_NAMES,
|
|
10
|
+
type KimiToolName,
|
|
11
|
+
getGlobalKimiCodeConfigPath,
|
|
12
|
+
getProjectKimiCodeConfigPath,
|
|
13
|
+
} from "./config.ts";
|
|
14
|
+
import { PROVIDER_VERSION } from "./constants.ts";
|
|
15
|
+
import type { KimiOAuthExtras } from "./models.ts";
|
|
16
|
+
|
|
17
|
+
export type KimiConfigScope = "project" | "home";
|
|
18
|
+
|
|
19
|
+
export function moonshotStatus(config: KimiCodeConfig): string {
|
|
20
|
+
return KIMI_TOOL_NAMES.map((toolName) => {
|
|
21
|
+
const tool = config.tools[toolName];
|
|
22
|
+
const enabled = tool.enabled ? "enabled" : "disabled";
|
|
23
|
+
const collapsed = tool.default_collapsed ? "collapsed" : "expanded";
|
|
24
|
+
return `${toolName}: ${enabled}, default ${collapsed}`;
|
|
25
|
+
}).join("\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function toggleEnabled(config: KimiCodeConfig, toolName: KimiToolName): KimiCodeConfig {
|
|
29
|
+
return {
|
|
30
|
+
...config,
|
|
31
|
+
tools: {
|
|
32
|
+
...config.tools,
|
|
33
|
+
[toolName]: {
|
|
34
|
+
...config.tools[toolName],
|
|
35
|
+
enabled: !config.tools[toolName].enabled,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function toggleCollapsed(config: KimiCodeConfig, toolName: KimiToolName): KimiCodeConfig {
|
|
42
|
+
return {
|
|
43
|
+
...config,
|
|
44
|
+
tools: {
|
|
45
|
+
...config.tools,
|
|
46
|
+
[toolName]: {
|
|
47
|
+
...config.tools[toolName],
|
|
48
|
+
default_collapsed: !config.tools[toolName].default_collapsed,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function setProtocol(
|
|
55
|
+
config: KimiCodeConfig,
|
|
56
|
+
protocol: KimiCodeConfig["protocol"],
|
|
57
|
+
): KimiCodeConfig {
|
|
58
|
+
return { ...config, protocol };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function setUploadThreshold(config: KimiCodeConfig, thresholdBytes: number): KimiCodeConfig {
|
|
62
|
+
return { ...config, uploads: { ...config.uploads, thresholdBytes } };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildKimiMainTitle(
|
|
66
|
+
config: KimiCodeConfig,
|
|
67
|
+
extras: KimiOAuthExtras,
|
|
68
|
+
sources: KimiCodeConfigSources,
|
|
69
|
+
): string {
|
|
70
|
+
const modelName = extras.modelDisplay || "kimi-for-coding";
|
|
71
|
+
return [
|
|
72
|
+
`Kimi settings (provider v${PROVIDER_VERSION})`,
|
|
73
|
+
"",
|
|
74
|
+
`Model: ${modelName}`,
|
|
75
|
+
`Protocol: ${config.protocol} (${sources.protocol})`,
|
|
76
|
+
`Upload threshold: ${formatByteSize(config.uploads.thresholdBytes)} (${sources.uploads.thresholdBytes})`,
|
|
77
|
+
"",
|
|
78
|
+
"Effective tools:",
|
|
79
|
+
...KIMI_TOOL_NAMES.map((toolName) => {
|
|
80
|
+
const enabled = config.tools[toolName].enabled ? "enabled" : "disabled";
|
|
81
|
+
return `- ${toolName}: ${enabled} (${sources.tools[toolName].enabled})`;
|
|
82
|
+
}),
|
|
83
|
+
].join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function buildConfigScopeTitle(
|
|
87
|
+
scope: KimiConfigScope,
|
|
88
|
+
config: KimiCodeConfig,
|
|
89
|
+
filePath: string,
|
|
90
|
+
): string {
|
|
91
|
+
return [
|
|
92
|
+
`Edit ${scope} config`,
|
|
93
|
+
`File: ${filePath}`,
|
|
94
|
+
"",
|
|
95
|
+
formatProtocolStatus(config),
|
|
96
|
+
formatUploadThresholdStatus(config),
|
|
97
|
+
"",
|
|
98
|
+
moonshotStatus(config),
|
|
99
|
+
].join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function buildSettingsTheme(theme: Theme): SettingsListTheme {
|
|
103
|
+
return {
|
|
104
|
+
label: (text: string, selected: boolean) => (selected ? theme.fg("accent", text) : text),
|
|
105
|
+
value: (text: string, selected: boolean) =>
|
|
106
|
+
selected ? theme.bold(theme.fg("accent", text)) : theme.fg("muted", text),
|
|
107
|
+
description: (text: string) => theme.fg("dim", text),
|
|
108
|
+
cursor: theme.fg("accent", "> "),
|
|
109
|
+
hint: (text: string) => theme.fg("dim", text),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function formatScopeDescription(
|
|
114
|
+
scope: KimiConfigScope,
|
|
115
|
+
cwd: string,
|
|
116
|
+
home = os.homedir(),
|
|
117
|
+
): string {
|
|
118
|
+
const filePath =
|
|
119
|
+
scope === "project" ? getProjectKimiCodeConfigPath(cwd) : getGlobalKimiCodeConfigPath(home);
|
|
120
|
+
const displayPath = scope === "home" ? homeRelative(filePath, home) : relative(cwd, filePath);
|
|
121
|
+
return `Writes to the ${scope} config file: ${displayPath}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function homeRelative(filePath: string, home = os.homedir()): string {
|
|
125
|
+
const normalizedHome = home.replace(/[\\/]+$/, "");
|
|
126
|
+
if (filePath === normalizedHome) return "~";
|
|
127
|
+
|
|
128
|
+
const slashPrefix = `${normalizedHome}/`;
|
|
129
|
+
if (filePath.startsWith(slashPrefix)) return `~/${filePath.slice(slashPrefix.length)}`;
|
|
130
|
+
|
|
131
|
+
const backslashPrefix = `${normalizedHome}\\`;
|
|
132
|
+
if (filePath.startsWith(backslashPrefix)) return `~\\${filePath.slice(backslashPrefix.length)}`;
|
|
133
|
+
|
|
134
|
+
return filePath;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function toolMenuItem(config: KimiCodeConfig, toolName: KimiToolName): string {
|
|
138
|
+
return `${toolName} -> ${formatToolStatus(config, toolName)}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function protocolMenuItem(config: KimiCodeConfig): string {
|
|
142
|
+
return `Protocol -> ${config.protocol}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function uploadThresholdMenuItem(config: KimiCodeConfig): string {
|
|
146
|
+
return `Upload threshold -> ${formatByteSize(config.uploads.thresholdBytes)}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function formatByteSize(bytes: number): string {
|
|
150
|
+
const units = [
|
|
151
|
+
{ suffix: "GiB", size: 1024 ** 3 },
|
|
152
|
+
{ suffix: "MiB", size: 1024 ** 2 },
|
|
153
|
+
{ suffix: "KiB", size: 1024 },
|
|
154
|
+
];
|
|
155
|
+
for (const unit of units) {
|
|
156
|
+
if (bytes >= unit.size && bytes % unit.size === 0) {
|
|
157
|
+
return `${bytes / unit.size} ${unit.suffix}`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const unit of units) {
|
|
161
|
+
if (bytes >= unit.size) {
|
|
162
|
+
return `${(bytes / unit.size).toFixed(2).replace(/\.00$/, "")} ${unit.suffix}`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return `${bytes} B`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function parseByteSizeInput(input: string): number | undefined {
|
|
169
|
+
const match = input.trim().match(/^(\d+(?:\.\d+)?)\s*(b|bytes?|kib|kb|mib|mb|gib|gb)?$/i);
|
|
170
|
+
if (!match) return undefined;
|
|
171
|
+
const value = Number(match[1]);
|
|
172
|
+
if (!Number.isFinite(value) || value <= 0) return undefined;
|
|
173
|
+
const unit = (match[2] ?? "mib").toLowerCase();
|
|
174
|
+
const multipliers: Record<string, number> = {
|
|
175
|
+
b: 1,
|
|
176
|
+
byte: 1,
|
|
177
|
+
bytes: 1,
|
|
178
|
+
kib: 1024,
|
|
179
|
+
kb: 1000,
|
|
180
|
+
mib: 1024 ** 2,
|
|
181
|
+
mb: 1000 ** 2,
|
|
182
|
+
gib: 1024 ** 3,
|
|
183
|
+
gb: 1000 ** 3,
|
|
184
|
+
};
|
|
185
|
+
const multiplier = multipliers[unit];
|
|
186
|
+
if (!multiplier) return undefined;
|
|
187
|
+
const bytes = Math.round(value * multiplier);
|
|
188
|
+
return Number.isSafeInteger(bytes) && bytes > 0 ? bytes : undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function formatProtocolStatus(config: KimiCodeConfig): string {
|
|
192
|
+
return `protocol: ${config.protocol}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function formatUploadThresholdStatus(config: KimiCodeConfig): string {
|
|
196
|
+
return `upload threshold: ${formatByteSize(config.uploads.thresholdBytes)}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function formatToolStatus(config: KimiCodeConfig, toolName: KimiToolName): string {
|
|
200
|
+
const tool = config.tools[toolName];
|
|
201
|
+
const enabled = tool.enabled ? "enabled" : "disabled";
|
|
202
|
+
const collapsed = tool.default_collapsed ? "default collapsed" : "default expanded";
|
|
203
|
+
return `${enabled}, ${collapsed}`;
|
|
204
|
+
}
|
package/src/stream.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "./config.ts";
|
|
25
25
|
|
|
26
26
|
import { ENV_KIMI_CODE_PROTOCOL, getApiProtocol, getBaseUrl } from "./constants.ts";
|
|
27
|
-
import {
|
|
27
|
+
import { getKimiProviderHeaders } from "./device.ts";
|
|
28
28
|
import { isKimiAuthErrorMessage, refreshKimiAuthToken } from "./oauth.ts";
|
|
29
29
|
import {
|
|
30
30
|
type Uploader,
|
|
@@ -163,7 +163,7 @@ export function resolveKimiApiKey(apiKey: string | undefined): string {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
export function mergeKimiRequestHeaders(headers?: Record<string, string>): Record<string, string> {
|
|
166
|
-
return { ...
|
|
166
|
+
return { ...getKimiProviderHeaders(), ...headers };
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
export function streamSimpleKimi(
|
|
@@ -192,8 +192,27 @@ export function streamSimpleKimi(
|
|
|
192
192
|
protocol: ENV_KIMI_CODE_PROTOCOL,
|
|
193
193
|
uploads: DEFAULT_KIMI_CODE_CONFIG.uploads,
|
|
194
194
|
};
|
|
195
|
-
const
|
|
196
|
-
|
|
195
|
+
const discoveredModel = model as Model<Api> & {
|
|
196
|
+
supportsThinkingType?: "only" | "no" | "both";
|
|
197
|
+
wireProtocol?: KimiCodeConfig["protocol"];
|
|
198
|
+
supportEfforts?: string[];
|
|
199
|
+
defaultEffort?: string;
|
|
200
|
+
};
|
|
201
|
+
const modelConfig: KimiResolvedModelConfig = {
|
|
202
|
+
...streamConfig.model,
|
|
203
|
+
contextWindow: model.contextWindow,
|
|
204
|
+
maxTokens: model.maxTokens,
|
|
205
|
+
input: [...model.input],
|
|
206
|
+
reasoning: model.reasoning,
|
|
207
|
+
supportsThinkingType:
|
|
208
|
+
discoveredModel.supportsThinkingType ?? (model.reasoning ? undefined : "no"),
|
|
209
|
+
supportEfforts: discoveredModel.supportEfforts
|
|
210
|
+
? [...discoveredModel.supportEfforts]
|
|
211
|
+
: undefined,
|
|
212
|
+
defaultEffort: discoveredModel.defaultEffort,
|
|
213
|
+
};
|
|
214
|
+
const wireProtocol = discoveredModel.wireProtocol ?? streamConfig.protocol;
|
|
215
|
+
const apiProtocol = getApiProtocol(wireProtocol);
|
|
197
216
|
const originalOnPayload = options?.onPayload;
|
|
198
217
|
// The pi-side model id ("kimi-for-coding") is what users select via /model
|
|
199
218
|
// and what gets persisted into sessions. The wire model id discovered at
|
|
@@ -257,10 +276,10 @@ export function streamSimpleKimi(
|
|
|
257
276
|
const runtimeModel = {
|
|
258
277
|
...model,
|
|
259
278
|
api: apiProtocol,
|
|
260
|
-
baseUrl: getBaseUrl(
|
|
279
|
+
baseUrl: getBaseUrl(wireProtocol),
|
|
261
280
|
} as Model<Api>;
|
|
262
281
|
const upstream =
|
|
263
|
-
|
|
282
|
+
wireProtocol === "openai"
|
|
264
283
|
? streamSimpleOpenAICompletions(
|
|
265
284
|
runtimeModel as Model<"openai-completions">,
|
|
266
285
|
context,
|
package/src/tools/common.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { AuthStorage, type AgentToolResult } from "@earendil-works/pi-coding-age
|
|
|
2
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
4
|
import { PROVIDER_ID, getBaseUrl } from "../constants.ts";
|
|
5
|
-
import {
|
|
6
|
-
import { refreshKimiAuthToken } from "../oauth.ts";
|
|
5
|
+
import { getKimiProviderHeaders } from "../device.ts";
|
|
6
|
+
import { KIMI_LOGIN_REQUIRED_MESSAGE, refreshKimiAuthToken } from "../oauth.ts";
|
|
7
7
|
|
|
8
8
|
export const KIMI_TOOL_TIMEOUT_MS = 180_000;
|
|
9
9
|
|
|
@@ -67,7 +67,7 @@ export function getKimiDatasourceUrl(): string {
|
|
|
67
67
|
|
|
68
68
|
export function buildHeaders(accessToken: string, toolCallId: string): Record<string, string> {
|
|
69
69
|
return {
|
|
70
|
-
...
|
|
70
|
+
...getKimiProviderHeaders(),
|
|
71
71
|
Authorization: `Bearer ${accessToken}`,
|
|
72
72
|
"Content-Type": "application/json",
|
|
73
73
|
"X-Msh-Tool-Call-Id": toolCallId,
|
|
@@ -94,7 +94,9 @@ export async function fetchWithAuthRetry(
|
|
|
94
94
|
if (response.status !== 401) return response;
|
|
95
95
|
|
|
96
96
|
const refreshed = await deps.refreshAccessToken(accessToken);
|
|
97
|
-
if (!refreshed || refreshed === accessToken)
|
|
97
|
+
if (!refreshed || refreshed === accessToken) {
|
|
98
|
+
throw new Error(KIMI_LOGIN_REQUIRED_MESSAGE);
|
|
99
|
+
}
|
|
98
100
|
return request(refreshed);
|
|
99
101
|
}
|
|
100
102
|
|
package/src/tools/moonshot.ts
CHANGED
|
@@ -17,20 +17,6 @@ import {
|
|
|
17
17
|
|
|
18
18
|
export const moonshotSearchSchema = Type.Object({
|
|
19
19
|
query: Type.String({ description: "The query text to search for." }),
|
|
20
|
-
limit: Type.Optional(
|
|
21
|
-
Type.Number({
|
|
22
|
-
description: "The number of results to return.",
|
|
23
|
-
default: 5,
|
|
24
|
-
minimum: 1,
|
|
25
|
-
maximum: 20,
|
|
26
|
-
}),
|
|
27
|
-
),
|
|
28
|
-
include_content: Type.Optional(
|
|
29
|
-
Type.Boolean({
|
|
30
|
-
description: "Whether to include fetched page content in the search results.",
|
|
31
|
-
default: false,
|
|
32
|
-
}),
|
|
33
|
-
),
|
|
34
20
|
});
|
|
35
21
|
|
|
36
22
|
export const moonshotFetchSchema = Type.Object({
|
|
@@ -44,7 +30,8 @@ export interface MoonshotSearchResult {
|
|
|
44
30
|
url: string;
|
|
45
31
|
title: string;
|
|
46
32
|
snippet: string;
|
|
47
|
-
|
|
33
|
+
date?: string;
|
|
34
|
+
siteName?: string;
|
|
48
35
|
}
|
|
49
36
|
|
|
50
37
|
export interface MoonshotFetchResult {
|
|
@@ -52,11 +39,6 @@ export interface MoonshotFetchResult {
|
|
|
52
39
|
content: string;
|
|
53
40
|
}
|
|
54
41
|
|
|
55
|
-
function clampLimit(value: number | undefined): number {
|
|
56
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return 5;
|
|
57
|
-
return Math.max(1, Math.min(20, Math.trunc(value)));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
42
|
interface MoonshotSearchResponse {
|
|
61
43
|
search_results?: unknown;
|
|
62
44
|
}
|
|
@@ -66,7 +48,7 @@ function stringField(record: Record<string, unknown>, key: string): string {
|
|
|
66
48
|
return typeof value === "string" ? value : "";
|
|
67
49
|
}
|
|
68
50
|
|
|
69
|
-
function mapSearchResults(value: unknown
|
|
51
|
+
function mapSearchResults(value: unknown): MoonshotSearchResult[] {
|
|
70
52
|
if (!Array.isArray(value)) return [];
|
|
71
53
|
return value
|
|
72
54
|
.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null)
|
|
@@ -76,9 +58,10 @@ function mapSearchResults(value: unknown, includeContent: boolean): MoonshotSear
|
|
|
76
58
|
title: stringField(item, "title"),
|
|
77
59
|
snippet: stringField(item, "snippet"),
|
|
78
60
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
61
|
+
const date = stringField(item, "date");
|
|
62
|
+
if (date) result.date = date;
|
|
63
|
+
const siteName = stringField(item, "site_name");
|
|
64
|
+
if (siteName) result.siteName = siteName;
|
|
82
65
|
return result;
|
|
83
66
|
});
|
|
84
67
|
}
|
|
@@ -130,8 +113,10 @@ export function buildMoonshotSearchTool(options: BuildKimiToolOptions = {}) {
|
|
|
130
113
|
return defineTool({
|
|
131
114
|
name: "moonshot_search",
|
|
132
115
|
label: "Moonshot Search",
|
|
133
|
-
description:
|
|
134
|
-
|
|
116
|
+
description:
|
|
117
|
+
"Search the web through Kimi Coding. Results are summaries; use moonshot_fetch to read relevant pages.",
|
|
118
|
+
promptSnippet:
|
|
119
|
+
"Search the web with Kimi Coding, then fetch the few relevant result URLs before answering",
|
|
135
120
|
parameters: moonshotSearchSchema,
|
|
136
121
|
|
|
137
122
|
async execute(toolCallId, params, signal) {
|
|
@@ -142,20 +127,13 @@ export function buildMoonshotSearchTool(options: BuildKimiToolOptions = {}) {
|
|
|
142
127
|
);
|
|
143
128
|
}
|
|
144
129
|
|
|
145
|
-
const limit = clampLimit(params.limit);
|
|
146
|
-
const includeContent = params.include_content === true;
|
|
147
130
|
const timeout = buildTimeoutSignal(signal);
|
|
148
131
|
try {
|
|
149
132
|
const response = await fetchWithAuthRetry(deps, accessToken, (token) =>
|
|
150
133
|
deps.fetch(`${getKimiBaseV1()}/search`, {
|
|
151
134
|
method: "POST",
|
|
152
135
|
headers: buildHeaders(token, toolCallId),
|
|
153
|
-
body: JSON.stringify({
|
|
154
|
-
text_query: params.query,
|
|
155
|
-
limit,
|
|
156
|
-
enable_page_crawling: includeContent,
|
|
157
|
-
timeout_seconds: 30,
|
|
158
|
-
}),
|
|
136
|
+
body: JSON.stringify({ text_query: params.query }),
|
|
159
137
|
signal: timeout.signal,
|
|
160
138
|
}),
|
|
161
139
|
);
|
|
@@ -168,7 +146,7 @@ export function buildMoonshotSearchTool(options: BuildKimiToolOptions = {}) {
|
|
|
168
146
|
}
|
|
169
147
|
|
|
170
148
|
const data = (await response.json()) as MoonshotSearchResponse;
|
|
171
|
-
const results = mapSearchResults(data.search_results
|
|
149
|
+
const results = mapSearchResults(data.search_results);
|
|
172
150
|
return {
|
|
173
151
|
content: [{ type: "text", text: searchResultsText(results) }],
|
|
174
152
|
details: results,
|