pi-provider-kimi-code 0.5.0 → 0.6.0
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 +63 -40
- package/index.ts +60 -46
- package/package.json +25 -23
- package/src/config.ts +82 -37
- package/src/constants.ts +3 -4
- package/src/models.ts +23 -4
- package/src/oauth.ts +1 -0
- package/src/payload.ts +22 -6
- package/src/stream.ts +54 -8
- package/src/tools/common.ts +115 -0
- package/src/tools/datasource.ts +191 -0
- package/src/tools/moonshot.ts +43 -112
package/src/config.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
|
|
4
|
+
export const KIMI_TOOL_NAMES = ["moonshot_search", "moonshot_fetch", "kimi_datasource"] as const;
|
|
5
|
+
|
|
6
|
+
export type KimiToolName = (typeof KIMI_TOOL_NAMES)[number];
|
|
7
|
+
|
|
8
|
+
export type KimiConfigSource = "project" | "home" | "default";
|
|
9
|
+
|
|
4
10
|
export interface KimiCodeConfig {
|
|
5
|
-
tools: {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
tools: Record<KimiToolName, { enabled: boolean; default_collapsed: boolean }>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface KimiCodeConfigSources {
|
|
15
|
+
tools: Record<KimiToolName, { enabled: KimiConfigSource; default_collapsed: KimiConfigSource }>;
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
export interface LoadKimiCodeConfigOptions {
|
|
@@ -13,11 +20,16 @@ export interface LoadKimiCodeConfigOptions {
|
|
|
13
20
|
home: string;
|
|
14
21
|
}
|
|
15
22
|
|
|
23
|
+
function makeDefaultTools(): KimiCodeConfig["tools"] {
|
|
24
|
+
const tools = {} as KimiCodeConfig["tools"];
|
|
25
|
+
for (const name of KIMI_TOOL_NAMES) {
|
|
26
|
+
tools[name] = { enabled: false, default_collapsed: true };
|
|
27
|
+
}
|
|
28
|
+
return tools;
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
export const DEFAULT_KIMI_CODE_CONFIG: KimiCodeConfig = {
|
|
17
|
-
tools:
|
|
18
|
-
moonshot_search: { enabled: false, default_collapsed: true },
|
|
19
|
-
moonshot_fetch: { enabled: false, default_collapsed: true },
|
|
20
|
-
},
|
|
32
|
+
tools: makeDefaultTools(),
|
|
21
33
|
};
|
|
22
34
|
|
|
23
35
|
export function getGlobalKimiCodeConfigPath(home: string): string {
|
|
@@ -85,23 +97,61 @@ function readDefaultCollapsed(config: Record<string, unknown>, toolName: string)
|
|
|
85
97
|
return tool.default_collapsed !== false;
|
|
86
98
|
}
|
|
87
99
|
|
|
100
|
+
function hasToolField(
|
|
101
|
+
config: Record<string, unknown>,
|
|
102
|
+
toolName: KimiToolName,
|
|
103
|
+
field: "enabled" | "default_collapsed",
|
|
104
|
+
): boolean {
|
|
105
|
+
const tools = config.tools;
|
|
106
|
+
if (!isRecord(tools)) return false;
|
|
107
|
+
const tool = tools[toolName];
|
|
108
|
+
if (!isRecord(tool)) return false;
|
|
109
|
+
return Object.hasOwn(tool, field);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readToolConfig(
|
|
113
|
+
config: Record<string, unknown>,
|
|
114
|
+
toolName: KimiToolName,
|
|
115
|
+
): { enabled: boolean; default_collapsed: boolean } {
|
|
116
|
+
return {
|
|
117
|
+
enabled: readEnabled(config, toolName),
|
|
118
|
+
default_collapsed: readDefaultCollapsed(config, toolName),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
88
122
|
export function loadKimiCodeConfig(options: LoadKimiCodeConfigOptions): KimiCodeConfig {
|
|
89
123
|
const globalConfig = readConfigFile(getGlobalKimiCodeConfigPath(options.home));
|
|
90
124
|
const projectConfig = readConfigFile(getProjectKimiCodeConfigPath(options.cwd));
|
|
91
125
|
const merged = mergeConfig(globalConfig, projectConfig);
|
|
92
126
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
127
|
+
const tools = {} as KimiCodeConfig["tools"];
|
|
128
|
+
for (const name of KIMI_TOOL_NAMES) {
|
|
129
|
+
tools[name] = readToolConfig(merged, name);
|
|
130
|
+
}
|
|
131
|
+
return { tools };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function loadKimiCodeConfigSources(
|
|
135
|
+
options: LoadKimiCodeConfigOptions,
|
|
136
|
+
): KimiCodeConfigSources {
|
|
137
|
+
const homeConfig = readConfigFileQuiet(getGlobalKimiCodeConfigPath(options.home));
|
|
138
|
+
const projectConfig = readConfigFileQuiet(getProjectKimiCodeConfigPath(options.cwd));
|
|
139
|
+
const tools = {} as KimiCodeConfigSources["tools"];
|
|
140
|
+
for (const name of KIMI_TOOL_NAMES) {
|
|
141
|
+
tools[name] = {
|
|
142
|
+
enabled: hasToolField(projectConfig, name, "enabled")
|
|
143
|
+
? "project"
|
|
144
|
+
: hasToolField(homeConfig, name, "enabled")
|
|
145
|
+
? "home"
|
|
146
|
+
: "default",
|
|
147
|
+
default_collapsed: hasToolField(projectConfig, name, "default_collapsed")
|
|
148
|
+
? "project"
|
|
149
|
+
: hasToolField(homeConfig, name, "default_collapsed")
|
|
150
|
+
? "home"
|
|
151
|
+
: "default",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return { tools };
|
|
105
155
|
}
|
|
106
156
|
|
|
107
157
|
export function loadProjectKimiCodeConfig(cwd: string): KimiCodeConfig {
|
|
@@ -114,18 +164,11 @@ export function loadHomeKimiCodeConfig(home: string): KimiCodeConfig {
|
|
|
114
164
|
|
|
115
165
|
function loadKimiCodeConfigFile(path: string): KimiCodeConfig {
|
|
116
166
|
const projectConfig = readConfigFileQuiet(path);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
},
|
|
123
|
-
moonshot_fetch: {
|
|
124
|
-
enabled: readEnabled(projectConfig, "moonshot_fetch"),
|
|
125
|
-
default_collapsed: readDefaultCollapsed(projectConfig, "moonshot_fetch"),
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
};
|
|
167
|
+
const tools = {} as KimiCodeConfig["tools"];
|
|
168
|
+
for (const name of KIMI_TOOL_NAMES) {
|
|
169
|
+
tools[name] = readToolConfig(projectConfig, name);
|
|
170
|
+
}
|
|
171
|
+
return { tools };
|
|
129
172
|
}
|
|
130
173
|
|
|
131
174
|
export function saveProjectKimiCodeConfig(cwd: string, config: KimiCodeConfig): void {
|
|
@@ -138,13 +181,15 @@ export function saveHomeKimiCodeConfig(home: string, config: KimiCodeConfig): vo
|
|
|
138
181
|
|
|
139
182
|
function saveKimiCodeConfigFile(path: string, config: KimiCodeConfig): void {
|
|
140
183
|
const raw = readConfigFileQuiet(path);
|
|
184
|
+
const tools: Record<string, unknown> = {
|
|
185
|
+
...(isRecord(raw.tools) ? raw.tools : {}),
|
|
186
|
+
};
|
|
187
|
+
for (const name of KIMI_TOOL_NAMES) {
|
|
188
|
+
tools[name] = { ...config.tools[name] };
|
|
189
|
+
}
|
|
141
190
|
const next = {
|
|
142
191
|
...raw,
|
|
143
|
-
tools
|
|
144
|
-
...(isRecord(raw.tools) ? raw.tools : {}),
|
|
145
|
-
moonshot_search: { ...config.tools.moonshot_search },
|
|
146
|
-
moonshot_fetch: { ...config.tools.moonshot_fetch },
|
|
147
|
-
},
|
|
192
|
+
tools,
|
|
148
193
|
};
|
|
149
194
|
mkdirSync(dirname(path), { recursive: true });
|
|
150
195
|
writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
package/src/constants.ts
CHANGED
|
@@ -24,10 +24,9 @@ export const DEFAULT_BASE_URL = IS_OPENAI_PROTOCOL
|
|
|
24
24
|
? "https://api.kimi.com/coding/v1"
|
|
25
25
|
: "https://api.kimi.com/coding";
|
|
26
26
|
|
|
27
|
-
export const KIMI_CODE_VERSION = "0.
|
|
28
|
-
export const
|
|
29
|
-
export const
|
|
30
|
-
export const KIMI_PLATFORM = "kimi-code-cli";
|
|
27
|
+
export const KIMI_CODE_VERSION = "0.6.0";
|
|
28
|
+
export const KIMI_CODE_USER_AGENT = `kimi-code-cli/${KIMI_CODE_VERSION}`;
|
|
29
|
+
export const KIMI_PLATFORM = "kimi_code_cli";
|
|
31
30
|
|
|
32
31
|
export function getKimiCodeHome(): string {
|
|
33
32
|
const value = process.env.KIMI_CODE_HOME?.trim();
|
package/src/models.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface KimiOAuthExtras {
|
|
|
13
13
|
contextLength?: number;
|
|
14
14
|
supportsReasoning?: boolean;
|
|
15
15
|
supportsImageIn?: boolean;
|
|
16
|
+
supportsThinkingType?: "only" | "no" | "both";
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export type KimiOAuthCredentials = OAuthCredentials & KimiOAuthExtras;
|
|
@@ -23,6 +24,12 @@ interface KimiServerModel {
|
|
|
23
24
|
context_length?: unknown;
|
|
24
25
|
supports_reasoning?: unknown;
|
|
25
26
|
supports_image_in?: unknown;
|
|
27
|
+
supports_thinking_type?: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseSupportsThinkingType(value: unknown): "only" | "no" | "both" | undefined {
|
|
31
|
+
if (value === "only" || value === "no" || value === "both") return value;
|
|
32
|
+
return undefined;
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
export function buildModelsUrl(baseUrl: string): string {
|
|
@@ -55,7 +62,10 @@ export async function discoverKimiModelMetadata(accessToken: string): Promise<Ki
|
|
|
55
62
|
if (typeof preferred.context_length === "number" && preferred.context_length > 0) {
|
|
56
63
|
extras.contextLength = preferred.context_length;
|
|
57
64
|
}
|
|
58
|
-
|
|
65
|
+
const thinkingType = parseSupportsThinkingType(preferred.supports_thinking_type);
|
|
66
|
+
if (thinkingType) {
|
|
67
|
+
extras.supportsThinkingType = thinkingType;
|
|
68
|
+
} else if (typeof preferred.supports_reasoning === "boolean") {
|
|
59
69
|
extras.supportsReasoning = preferred.supports_reasoning;
|
|
60
70
|
}
|
|
61
71
|
if (typeof preferred.supports_image_in === "boolean") {
|
|
@@ -71,7 +81,8 @@ export function applyKimiOAuthExtrasToModel(
|
|
|
71
81
|
model: Model<Api>,
|
|
72
82
|
extras: KimiOAuthExtras,
|
|
73
83
|
): Model<Api> {
|
|
74
|
-
const next: Model<Api> & { wireModelId?: string
|
|
84
|
+
const next: Model<Api> & { wireModelId?: string; supportsThinkingType?: "only" | "no" | "both" } =
|
|
85
|
+
{ ...model };
|
|
75
86
|
if (typeof extras.modelDisplay === "string" && extras.modelDisplay) {
|
|
76
87
|
next.name = extras.modelDisplay;
|
|
77
88
|
}
|
|
@@ -81,8 +92,12 @@ export function applyKimiOAuthExtrasToModel(
|
|
|
81
92
|
if (typeof extras.wireModelId === "string" && extras.wireModelId) {
|
|
82
93
|
next.wireModelId = extras.wireModelId;
|
|
83
94
|
}
|
|
84
|
-
if (typeof extras.
|
|
95
|
+
if (typeof extras.supportsThinkingType === "string") {
|
|
96
|
+
next.reasoning = extras.supportsThinkingType !== "no";
|
|
97
|
+
next.supportsThinkingType = extras.supportsThinkingType;
|
|
98
|
+
} else if (typeof extras.supportsReasoning === "boolean") {
|
|
85
99
|
next.reasoning = extras.supportsReasoning;
|
|
100
|
+
next.supportsThinkingType = undefined;
|
|
86
101
|
}
|
|
87
102
|
if (typeof extras.supportsImageIn === "boolean") {
|
|
88
103
|
const input = ["text"];
|
|
@@ -101,7 +116,11 @@ function parseKimiModelCapabilities(value: string | undefined): KimiOAuthExtras
|
|
|
101
116
|
.filter(Boolean),
|
|
102
117
|
);
|
|
103
118
|
return {
|
|
104
|
-
|
|
119
|
+
supportsThinkingType: caps.has("always_thinking")
|
|
120
|
+
? "only"
|
|
121
|
+
: caps.has("thinking")
|
|
122
|
+
? "both"
|
|
123
|
+
: "no",
|
|
105
124
|
supportsImageIn: caps.has("image_in"),
|
|
106
125
|
};
|
|
107
126
|
}
|
package/src/oauth.ts
CHANGED
|
@@ -388,6 +388,7 @@ export async function refreshKimiAuthToken(currentKey: string): Promise<string |
|
|
|
388
388
|
console.error("[kimi-coding] auth refresh: requesting new access token");
|
|
389
389
|
const refreshed = await refreshAccessToken(cred.refresh);
|
|
390
390
|
const newCred: OAuthCredential = {
|
|
391
|
+
...cred,
|
|
391
392
|
type: "oauth",
|
|
392
393
|
access: refreshed.access_token,
|
|
393
394
|
refresh: refreshed.refresh_token,
|
package/src/payload.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface KimiPayloadContext {
|
|
|
39
39
|
reasoning?: ThinkingLevel;
|
|
40
40
|
thinkingKeep?: string;
|
|
41
41
|
envOverrides: KimiEnvOverrides;
|
|
42
|
+
supportsThinkingType?: "only" | "no" | "both";
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export function isRecord(value: unknown): value is JsonRecord {
|
|
@@ -54,6 +55,16 @@ function mapThinkingLevel(level?: string): { effort: string | null; enabled: boo
|
|
|
54
55
|
return undefined;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
function resolveThinkingLevel(ctx: KimiPayloadContext): ThinkingLevel | undefined {
|
|
59
|
+
if (ctx.supportsThinkingType === "no") return undefined;
|
|
60
|
+
if (ctx.supportsThinkingType === "only") {
|
|
61
|
+
if (!ctx.reasoning) return "low";
|
|
62
|
+
const mapped = mapThinkingLevel(ctx.reasoning);
|
|
63
|
+
if (mapped && !mapped.enabled) return "low";
|
|
64
|
+
}
|
|
65
|
+
return ctx.reasoning;
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
function parseInlineUploadThreshold(raw: string | undefined): number {
|
|
58
69
|
const parsed = Number.parseInt(raw ?? "", 10);
|
|
59
70
|
return Number.isFinite(parsed) && parsed >= 0
|
|
@@ -87,8 +98,7 @@ export function readEnvOverrides(): KimiEnvOverrides {
|
|
|
87
98
|
if (temp) out.temperature = parseFloat(temp);
|
|
88
99
|
const topP = process.env.KIMI_MODEL_TOP_P;
|
|
89
100
|
if (topP) out.topP = parseFloat(topP);
|
|
90
|
-
const maxCompletionTokens =
|
|
91
|
-
process.env.KIMI_MODEL_MAX_COMPLETION_TOKENS ?? process.env.KIMI_MODEL_MAX_TOKENS;
|
|
101
|
+
const maxCompletionTokens = process.env.KIMI_MODEL_MAX_COMPLETION_TOKENS;
|
|
92
102
|
if (maxCompletionTokens) out.maxCompletionTokens = parseInt(maxCompletionTokens, 10);
|
|
93
103
|
return out;
|
|
94
104
|
}
|
|
@@ -421,18 +431,24 @@ export async function applyKimiPayloadMutations(
|
|
|
421
431
|
if (resolved) payload.prompt_cache_key = resolved;
|
|
422
432
|
}
|
|
423
433
|
|
|
424
|
-
// 4.
|
|
434
|
+
// 4. Normalize deprecated max_tokens and apply env-level hyperparameter
|
|
435
|
+
// overrides (pre-parsed into numbers by caller).
|
|
436
|
+
if (payload.max_completion_tokens === undefined && typeof payload.max_tokens === "number") {
|
|
437
|
+
payload.max_completion_tokens = payload.max_tokens;
|
|
438
|
+
}
|
|
439
|
+
delete payload.max_tokens;
|
|
440
|
+
|
|
425
441
|
const { temperature, topP, maxCompletionTokens } = ctx.envOverrides;
|
|
426
442
|
if (temperature !== undefined) payload.temperature = temperature;
|
|
427
443
|
if (topP !== undefined) payload.top_p = topP;
|
|
428
444
|
if (maxCompletionTokens !== undefined) {
|
|
429
445
|
payload.max_completion_tokens = maxCompletionTokens;
|
|
430
|
-
delete payload.max_tokens;
|
|
431
446
|
}
|
|
432
447
|
|
|
433
448
|
// 5. Reasoning effort mapping.
|
|
434
|
-
|
|
435
|
-
|
|
449
|
+
const resolvedReasoning = resolveThinkingLevel(ctx);
|
|
450
|
+
if (resolvedReasoning) {
|
|
451
|
+
const mapped = mapThinkingLevel(resolvedReasoning);
|
|
436
452
|
if (mapped) {
|
|
437
453
|
payload.reasoning_effort = mapped.effort;
|
|
438
454
|
const extraBody = isRecord(payload.extra_body) ? payload.extra_body : {};
|
package/src/stream.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "@earendil-works/pi-ai";
|
|
20
20
|
|
|
21
21
|
import { IS_OPENAI_PROTOCOL, PROTOCOL } from "./constants.ts";
|
|
22
|
+
import { getCommonHeaders } from "./device.ts";
|
|
22
23
|
import { isKimiAuthErrorMessage, refreshKimiAuthToken } from "./oauth.ts";
|
|
23
24
|
import {
|
|
24
25
|
type Uploader,
|
|
@@ -46,24 +47,50 @@ export async function* filterEmptyResponseStream(
|
|
|
46
47
|
): AsyncIterable<AssistantMessageEvent> {
|
|
47
48
|
const suppressedIndices = new Set<number>();
|
|
48
49
|
let textBuffer: AssistantMessageEvent[] = [];
|
|
50
|
+
let bufferedText = "";
|
|
49
51
|
let bufferingIndex: number | null = null;
|
|
50
52
|
|
|
53
|
+
const suppressBufferedTextBlock = (): void => {
|
|
54
|
+
if (bufferingIndex !== null) suppressedIndices.add(bufferingIndex);
|
|
55
|
+
textBuffer = [];
|
|
56
|
+
bufferedText = "";
|
|
57
|
+
bufferingIndex = null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const flushBufferedTextBlock = async function* (): AsyncIterable<AssistantMessageEvent> {
|
|
61
|
+
for (const buffered of textBuffer) yield buffered;
|
|
62
|
+
textBuffer = [];
|
|
63
|
+
bufferedText = "";
|
|
64
|
+
bufferingIndex = null;
|
|
65
|
+
};
|
|
66
|
+
|
|
51
67
|
for await (const event of upstream) {
|
|
52
|
-
// Start buffering
|
|
68
|
+
// Start buffering only long enough to decide whether this text block is
|
|
69
|
+
// Kimi's synthetic "(Empty response: ...)" wrapper. Normal text is flushed
|
|
70
|
+
// as soon as it diverges from the marker so output still streams.
|
|
53
71
|
if (event.type === "text_start") {
|
|
54
72
|
bufferingIndex = event.contentIndex;
|
|
55
73
|
textBuffer = [event];
|
|
74
|
+
bufferedText = "";
|
|
56
75
|
continue;
|
|
57
76
|
}
|
|
58
77
|
|
|
59
|
-
// Accumulate text deltas + detect the empty-response marker on text_end.
|
|
60
78
|
if (
|
|
61
79
|
bufferingIndex !== null &&
|
|
62
80
|
"contentIndex" in event &&
|
|
63
81
|
event.contentIndex === bufferingIndex
|
|
64
82
|
) {
|
|
65
83
|
if (event.type === "text_delta") {
|
|
84
|
+
bufferedText += event.delta;
|
|
66
85
|
textBuffer.push(event);
|
|
86
|
+
if (bufferedText.startsWith(EMPTY_RESPONSE_PREFIX)) {
|
|
87
|
+
suppressBufferedTextBlock();
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (EMPTY_RESPONSE_PREFIX.startsWith(bufferedText)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
yield* flushBufferedTextBlock();
|
|
67
94
|
continue;
|
|
68
95
|
}
|
|
69
96
|
if (event.type === "text_end") {
|
|
@@ -72,14 +99,11 @@ export async function* filterEmptyResponseStream(
|
|
|
72
99
|
// array: it is a shared reference into session state, and mutating
|
|
73
100
|
// it would shift subsequent contentIndex values, corrupting the
|
|
74
101
|
// stream.
|
|
75
|
-
|
|
102
|
+
suppressBufferedTextBlock();
|
|
76
103
|
} else {
|
|
77
|
-
|
|
78
|
-
for (const buffered of textBuffer) yield buffered;
|
|
104
|
+
yield* flushBufferedTextBlock();
|
|
79
105
|
yield event;
|
|
80
106
|
}
|
|
81
|
-
textBuffer = [];
|
|
82
|
-
bufferingIndex = null;
|
|
83
107
|
continue;
|
|
84
108
|
}
|
|
85
109
|
}
|
|
@@ -103,19 +127,36 @@ export async function* filterEmptyResponseStream(
|
|
|
103
127
|
|
|
104
128
|
yield event;
|
|
105
129
|
}
|
|
130
|
+
|
|
131
|
+
if (bufferingIndex !== null) {
|
|
132
|
+
yield* flushBufferedTextBlock();
|
|
133
|
+
}
|
|
106
134
|
}
|
|
107
135
|
|
|
108
136
|
// =============================================================================
|
|
109
137
|
// Stream wrapper: orchestrates payload mutation + event filter
|
|
110
138
|
// =============================================================================
|
|
111
139
|
|
|
140
|
+
const KIMI_API_KEY_ENV_REFERENCES = new Set(["$KIMI_API_KEY", "${KIMI_API_KEY}"]);
|
|
141
|
+
|
|
142
|
+
export function resolveKimiApiKey(apiKey: string | undefined): string {
|
|
143
|
+
if (apiKey !== undefined && KIMI_API_KEY_ENV_REFERENCES.has(apiKey)) {
|
|
144
|
+
return process.env.KIMI_API_KEY?.trim() || "";
|
|
145
|
+
}
|
|
146
|
+
return apiKey || process.env.KIMI_API_KEY || "";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function mergeKimiRequestHeaders(headers?: Record<string, string>): Record<string, string> {
|
|
150
|
+
return { ...getCommonHeaders(), ...headers };
|
|
151
|
+
}
|
|
152
|
+
|
|
112
153
|
export function streamSimpleKimi(
|
|
113
154
|
model: Model<Api>,
|
|
114
155
|
context: Context,
|
|
115
156
|
options?: SimpleStreamOptions,
|
|
116
157
|
): AssistantMessageEventStream {
|
|
117
158
|
const filtered = createAssistantMessageEventStream();
|
|
118
|
-
const initialKey = options?.apiKey
|
|
159
|
+
const initialKey = resolveKimiApiKey(options?.apiKey);
|
|
119
160
|
|
|
120
161
|
const cacheKeyOverride = (
|
|
121
162
|
options as (SimpleStreamOptions & { prompt_cache_key?: unknown }) | undefined
|
|
@@ -131,6 +172,9 @@ export function streamSimpleKimi(
|
|
|
131
172
|
// the model object via modifyModels and rewritten into the request payload
|
|
132
173
|
// here so /v1/chat/completions and /v1/messages see the real wire id.
|
|
133
174
|
const wireModelId = (model as Model<Api> & { wireModelId?: unknown }).wireModelId;
|
|
175
|
+
const supportsThinkingType = (
|
|
176
|
+
model as Model<Api> & { supportsThinkingType?: "only" | "no" | "both" }
|
|
177
|
+
).supportsThinkingType;
|
|
134
178
|
|
|
135
179
|
const buildPatchedOptions = (apiKey: string): SimpleStreamOptions => {
|
|
136
180
|
const upload: Uploader | undefined = apiKey
|
|
@@ -142,6 +186,7 @@ export function streamSimpleKimi(
|
|
|
142
186
|
return {
|
|
143
187
|
...options,
|
|
144
188
|
...apiKeyOverride,
|
|
189
|
+
headers: mergeKimiRequestHeaders(options?.headers),
|
|
145
190
|
onPayload: async (payload, modelData) => {
|
|
146
191
|
let nextPayload: unknown = payload;
|
|
147
192
|
|
|
@@ -154,6 +199,7 @@ export function streamSimpleKimi(
|
|
|
154
199
|
reasoning: options?.reasoning,
|
|
155
200
|
thinkingKeep,
|
|
156
201
|
envOverrides,
|
|
202
|
+
supportsThinkingType,
|
|
157
203
|
});
|
|
158
204
|
if (
|
|
159
205
|
typeof wireModelId === "string" &&
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { AuthStorage, type AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
import { PROVIDER_ID, getBaseUrl } from "../constants.ts";
|
|
5
|
+
import { getCommonHeaders } from "../device.ts";
|
|
6
|
+
import { refreshKimiAuthToken } from "../oauth.ts";
|
|
7
|
+
|
|
8
|
+
export const KIMI_TOOL_TIMEOUT_MS = 180_000;
|
|
9
|
+
|
|
10
|
+
export interface KimiToolDeps {
|
|
11
|
+
fetch: typeof fetch;
|
|
12
|
+
getAccessToken: () => string | null;
|
|
13
|
+
refreshAccessToken: (currentToken: string) => Promise<string | null>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface BuildKimiToolOptions {
|
|
17
|
+
deps?: Partial<KimiToolDeps>;
|
|
18
|
+
defaultCollapsed?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function defaultGetAccessToken(): string | null {
|
|
22
|
+
const credential = AuthStorage.create().get(PROVIDER_ID);
|
|
23
|
+
if (!credential || credential.type !== "oauth") return null;
|
|
24
|
+
return credential.access;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildKimiToolDeps(options: BuildKimiToolOptions = {}): KimiToolDeps {
|
|
28
|
+
return {
|
|
29
|
+
fetch: options.deps?.fetch ?? fetch,
|
|
30
|
+
getAccessToken: options.deps?.getAccessToken ?? defaultGetAccessToken,
|
|
31
|
+
refreshAccessToken: options.deps?.refreshAccessToken ?? refreshKimiAuthToken,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildTimeoutSignal(signal: AbortSignal | undefined): {
|
|
36
|
+
signal: AbortSignal;
|
|
37
|
+
cleanup: () => void;
|
|
38
|
+
} {
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
const timeout = setTimeout(() => controller.abort(), KIMI_TOOL_TIMEOUT_MS);
|
|
41
|
+
const abort = () => controller.abort();
|
|
42
|
+
if (signal?.aborted) {
|
|
43
|
+
controller.abort();
|
|
44
|
+
} else {
|
|
45
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
signal: controller.signal,
|
|
50
|
+
cleanup: () => {
|
|
51
|
+
clearTimeout(timeout);
|
|
52
|
+
signal?.removeEventListener("abort", abort);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getKimiBaseV1(): string {
|
|
58
|
+
const base = getBaseUrl().replace(/\/+$/, "");
|
|
59
|
+
return base.endsWith("/v1") ? base : `${base}/v1`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getKimiDatasourceUrl(): string {
|
|
63
|
+
const explicit = process.env.KIMI_DATASOURCE_API_URL?.trim();
|
|
64
|
+
if (explicit) return explicit;
|
|
65
|
+
return `${getKimiBaseV1()}/tools`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildHeaders(accessToken: string, toolCallId: string): Record<string, string> {
|
|
69
|
+
return {
|
|
70
|
+
...getCommonHeaders(),
|
|
71
|
+
Authorization: `Bearer ${accessToken}`,
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
"X-Msh-Tool-Call-Id": toolCallId,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function errorResult<T>(message: string): AgentToolResult<T> {
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: "text", text: message }],
|
|
80
|
+
details: undefined as T,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function readErrorBody(response: Response): Promise<string> {
|
|
85
|
+
return response.text().catch(() => "");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function fetchWithAuthRetry(
|
|
89
|
+
deps: KimiToolDeps,
|
|
90
|
+
accessToken: string,
|
|
91
|
+
request: (token: string) => Promise<Response>,
|
|
92
|
+
): Promise<Response> {
|
|
93
|
+
const response = await request(accessToken);
|
|
94
|
+
if (response.status !== 401) return response;
|
|
95
|
+
|
|
96
|
+
const refreshed = await deps.refreshAccessToken(accessToken);
|
|
97
|
+
if (!refreshed || refreshed === accessToken) return response;
|
|
98
|
+
return request(refreshed);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function textComponent(text: string) {
|
|
102
|
+
return {
|
|
103
|
+
render: (width: number) => text.split("\n").map((line) => truncateToWidth(line, width, "…")),
|
|
104
|
+
invalidate: () => {},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function firstText(result: AgentToolResult<unknown>): string {
|
|
109
|
+
const first = result.content[0];
|
|
110
|
+
return first?.type === "text" ? first.text : "";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function shouldCollapse(defaultCollapsed: boolean, expanded: boolean | undefined): boolean {
|
|
114
|
+
return defaultCollapsed && expanded !== true;
|
|
115
|
+
}
|