pi-provider-kimi-code 0.6.3 → 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 +44 -16
- package/package.json +1 -1
- package/src/config.ts +2 -0
- 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/stream.ts +25 -6
- package/src/tools/common.ts +6 -4
- package/src/tools/moonshot.ts +13 -35
- package/src/usage.ts +94 -2
package/README.md
CHANGED
|
@@ -102,16 +102,18 @@ KIMI_API_KEY=sk-... pi
|
|
|
102
102
|
|
|
103
103
|
## Model
|
|
104
104
|
|
|
105
|
-
This provider publishes
|
|
105
|
+
This provider publishes two Pi model IDs:
|
|
106
106
|
|
|
107
107
|
```text
|
|
108
108
|
kimi-coding/kimi-for-coding
|
|
109
|
+
kimi-coding/kimi-for-coding-highspeed
|
|
109
110
|
```
|
|
110
111
|
|
|
111
|
-
Select
|
|
112
|
+
Select standard or highspeed mode inside Pi:
|
|
112
113
|
|
|
113
114
|
```text
|
|
114
115
|
/model kimi-coding/kimi-for-coding
|
|
116
|
+
/model kimi-coding/kimi-for-coding-highspeed
|
|
115
117
|
```
|
|
116
118
|
|
|
117
119
|
Kimi keeps Coding models behind aliases. Rather than hardcoding a stale model list, this extension asks Kimi for the current model info when you log in or refresh. If your account is on a newer rollout (e.g., Kimi K2.7) or internal test, Pi can pick up the latest model name and context size without waiting for a package release.
|
|
@@ -123,7 +125,7 @@ Fallback values:
|
|
|
123
125
|
- Input: text and image
|
|
124
126
|
- Reasoning: enabled
|
|
125
127
|
|
|
126
|
-
The provider maps Pi's reasoning levels to Kimi's `
|
|
128
|
+
The provider maps Pi's reasoning levels to Kimi's top-level `thinking` parameter. It sends `thinking.effort` only when `/models` advertises supported effort values. The mapping refreshes automatically on credential refresh. Opening `/kimi-settings` also re-discovers the latest model metadata.
|
|
127
129
|
|
|
128
130
|
## Optional tools
|
|
129
131
|
|
|
@@ -135,7 +137,7 @@ Inside Pi, run:
|
|
|
135
137
|
/kimi-settings
|
|
136
138
|
```
|
|
137
139
|
|
|
138
|
-
That command shows the current server-side model name (e.g. "K2.7 Code High Speed"), your Kimi
|
|
140
|
+
That command shows the current server-side model name (e.g. "K2.7 Code High Speed"), your Kimi quota and Extra Usage balance, and lets you edit the home or project config. Changes apply to the active session tool set.
|
|
139
141
|
|
|
140
142
|
Configurable settings include protocol mode, upload threshold, and per-tool enable/collapse.
|
|
141
143
|
|
|
@@ -277,7 +279,7 @@ Kimi's cache is **content-based**: it fires automatically when your prompt prefi
|
|
|
277
279
|
- A timestamp, request ID, or randomized header is being interpolated into the prompt.
|
|
278
280
|
- The first ~256 tokens of the prompt differ between turns.
|
|
279
281
|
|
|
280
|
-
`PI_CACHE_RETENTION=none` skips `prompt_cache_key` injection but **does not** disable Kimi's caching (the cache is unconditional). For deterministic measurement, run `scripts/
|
|
282
|
+
`PI_CACHE_RETENTION=none` skips `prompt_cache_key` injection but **does not** disable Kimi's caching (the cache is unconditional). For deterministic measurement, run a focused suite under `scripts/e2e/cache/`, starting with `scripts/e2e/cache/ttl.sh`. See [docs/caching.md](docs/caching.md) for full mechanics.
|
|
281
283
|
|
|
282
284
|
### OpenAI-compatible tools complain about a `developer` role
|
|
283
285
|
|
package/index.ts
CHANGED
|
@@ -44,10 +44,13 @@ import {
|
|
|
44
44
|
type KimiOAuthExtras,
|
|
45
45
|
buildKimiModelFromConfig,
|
|
46
46
|
applyKimiOAuthExtrasToModel,
|
|
47
|
+
KIMI_CODING_HIGHSPEED_MODEL_ID,
|
|
48
|
+
KIMI_CODING_MODEL_ID,
|
|
47
49
|
discoverKimiModelMetadata,
|
|
50
|
+
getKimiModelMetadata,
|
|
48
51
|
resolveKimiModelConfig,
|
|
49
52
|
} from "./src/models.ts";
|
|
50
|
-
import { loginKimiCode, refreshKimiCodeToken } from "./src/oauth.ts";
|
|
53
|
+
import { getKimiApiKey, loginKimiCode, refreshKimiCodeToken } from "./src/oauth.ts";
|
|
51
54
|
import { isKimiProjectConfigApproved } from "./src/project-trust.ts";
|
|
52
55
|
import {
|
|
53
56
|
type KimiConfigScope,
|
|
@@ -114,7 +117,10 @@ function reloadEffectiveKimiRuntimeConfig(
|
|
|
114
117
|
state.config = config;
|
|
115
118
|
state.projectTrusted = projectTrusted;
|
|
116
119
|
setStoreResolvedKimiConfig({
|
|
117
|
-
model: resolveKimiModelConfig(
|
|
120
|
+
model: resolveKimiModelConfig(
|
|
121
|
+
config.model,
|
|
122
|
+
getKimiModelMetadata(state.modelExtras, KIMI_CODING_MODEL_ID),
|
|
123
|
+
),
|
|
118
124
|
protocol: config.protocol,
|
|
119
125
|
uploads: config.uploads,
|
|
120
126
|
});
|
|
@@ -132,13 +138,13 @@ function applyEffectiveKimiRuntimeConfig(
|
|
|
132
138
|
return config;
|
|
133
139
|
}
|
|
134
140
|
|
|
135
|
-
async function refreshModelExtras(state: KimiRuntimeState): Promise<
|
|
141
|
+
async function refreshModelExtras(state: KimiRuntimeState): Promise<boolean> {
|
|
136
142
|
const token = getKimiUsageToken();
|
|
137
|
-
if (!token) return;
|
|
143
|
+
if (!token) return false;
|
|
138
144
|
const extras = await discoverKimiModelMetadata(token, state.config.protocol);
|
|
139
|
-
if (Object.keys(extras).length
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
if (Object.keys(extras).length === 0) return false;
|
|
146
|
+
Object.assign(state.modelExtras, extras);
|
|
147
|
+
return true;
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
async function openSettingsMenu(
|
|
@@ -146,7 +152,11 @@ async function openSettingsMenu(
|
|
|
146
152
|
ctx: ExtensionCommandContext,
|
|
147
153
|
state: KimiRuntimeState,
|
|
148
154
|
): Promise<void> {
|
|
149
|
-
const [usage] = await Promise.all([
|
|
155
|
+
const [usage, modelsRefreshed] = await Promise.all([
|
|
156
|
+
fetchKimiUsageSummary(),
|
|
157
|
+
refreshModelExtras(state),
|
|
158
|
+
]);
|
|
159
|
+
if (modelsRefreshed) registerKimiProvider(pi, state);
|
|
150
160
|
|
|
151
161
|
const projectTrusted = await isKimiProjectConfigApproved(ctx, ctx.cwd);
|
|
152
162
|
const homeDraft = loadHomeKimiCodeConfig(os.homedir());
|
|
@@ -354,10 +364,22 @@ function saveScopeKimiCodeConfig(
|
|
|
354
364
|
}
|
|
355
365
|
}
|
|
356
366
|
|
|
367
|
+
function filterAvailableKimiModels<T extends { id: string }>(
|
|
368
|
+
models: T[],
|
|
369
|
+
extras: KimiOAuthExtras,
|
|
370
|
+
): T[] {
|
|
371
|
+
const available = extras.modelCatalog ? new Set(Object.keys(extras.modelCatalog)) : null;
|
|
372
|
+
return available ? models.filter((model) => available.has(model.id)) : models;
|
|
373
|
+
}
|
|
374
|
+
|
|
357
375
|
function registerKimiProvider(pi: ExtensionAPI, state: KimiRuntimeState): void {
|
|
358
|
-
const
|
|
376
|
+
const standardModel = applyKimiOAuthExtrasToModel(
|
|
359
377
|
buildKimiModelFromConfig(state.config.model),
|
|
360
|
-
state.modelExtras,
|
|
378
|
+
getKimiModelMetadata(state.modelExtras, KIMI_CODING_MODEL_ID),
|
|
379
|
+
);
|
|
380
|
+
const highSpeedModel = applyKimiOAuthExtrasToModel(
|
|
381
|
+
buildKimiModelFromConfig(state.config.model, KIMI_CODING_HIGHSPEED_MODEL_ID),
|
|
382
|
+
getKimiModelMetadata(state.modelExtras, KIMI_CODING_HIGHSPEED_MODEL_ID),
|
|
361
383
|
);
|
|
362
384
|
|
|
363
385
|
pi.registerProvider(PROVIDER_ID, {
|
|
@@ -366,13 +388,13 @@ function registerKimiProvider(pi: ExtensionAPI, state: KimiRuntimeState): void {
|
|
|
366
388
|
api: getKimiApiType(state.config.protocol),
|
|
367
389
|
streamSimple: streamSimpleKimi,
|
|
368
390
|
|
|
369
|
-
models: [
|
|
391
|
+
models: filterAvailableKimiModels([standardModel, highSpeedModel], state.modelExtras),
|
|
370
392
|
|
|
371
393
|
oauth: {
|
|
372
394
|
name: "Kimi Code (OAuth)",
|
|
373
395
|
login: loginKimiCode,
|
|
374
396
|
refreshToken: refreshKimiCodeToken,
|
|
375
|
-
getApiKey:
|
|
397
|
+
getApiKey: getKimiApiKey,
|
|
376
398
|
// Reflect server-side model identity on the registered model after login
|
|
377
399
|
// / refresh. We never rewrite the model id (pi-side `/model` selections
|
|
378
400
|
// and persisted sessions reference it); only the human-facing name, the
|
|
@@ -382,10 +404,16 @@ function registerKimiProvider(pi: ExtensionAPI, state: KimiRuntimeState): void {
|
|
|
382
404
|
const extras = cred as KimiOAuthCredentials;
|
|
383
405
|
state.modelExtras = extras;
|
|
384
406
|
reloadEffectiveKimiRuntimeConfig(state, state.cwd, state.projectTrusted);
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
407
|
+
const available = extras.modelCatalog ? new Set(Object.keys(extras.modelCatalog)) : null;
|
|
408
|
+
return models
|
|
409
|
+
.filter(
|
|
410
|
+
(model) => model.provider !== PROVIDER_ID || !available || available.has(model.id),
|
|
411
|
+
)
|
|
412
|
+
.map((model) =>
|
|
413
|
+
model.provider === PROVIDER_ID
|
|
414
|
+
? applyKimiOAuthExtrasToModel(model, getKimiModelMetadata(extras, model.id))
|
|
415
|
+
: model,
|
|
416
|
+
);
|
|
389
417
|
},
|
|
390
418
|
},
|
|
391
419
|
});
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
package/src/constants.ts
CHANGED
|
@@ -41,12 +41,12 @@ export function getDefaultBaseUrl(protocol: KimiWireProtocol): string {
|
|
|
41
41
|
|
|
42
42
|
export const DEFAULT_BASE_URL = getDefaultBaseUrl(ENV_KIMI_CODE_PROTOCOL);
|
|
43
43
|
|
|
44
|
-
export const PROVIDER_VERSION = "0.6.
|
|
44
|
+
export const PROVIDER_VERSION = "0.6.5";
|
|
45
45
|
|
|
46
46
|
// Upstream kimi-code CLI version — used in User-Agent and X-Msh-Version
|
|
47
47
|
// headers to match the official client's identity. Update this when
|
|
48
48
|
// syncing with https://github.com/anthropics/kimi-code.
|
|
49
|
-
export const KIMI_UPSTREAM_VERSION = "0.
|
|
49
|
+
export const KIMI_UPSTREAM_VERSION = "0.23.4";
|
|
50
50
|
export const KIMI_CODE_USER_AGENT = `kimi-code-cli/${KIMI_UPSTREAM_VERSION}`;
|
|
51
51
|
export const KIMI_PLATFORM = "kimi_code_cli";
|
|
52
52
|
|
package/src/device.ts
CHANGED
|
@@ -152,6 +152,22 @@ function getStableDeviceId(): string {
|
|
|
152
152
|
return DEVICE_ID;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
export function parseKimiCodeCustomHeaders(
|
|
156
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
157
|
+
): Record<string, string> {
|
|
158
|
+
const raw = env.KIMI_CODE_CUSTOM_HEADERS?.trim();
|
|
159
|
+
if (!raw) return {};
|
|
160
|
+
const headers: Record<string, string> = {};
|
|
161
|
+
for (const line of raw.split("\n")) {
|
|
162
|
+
const colon = line.indexOf(":");
|
|
163
|
+
if (colon < 0) continue;
|
|
164
|
+
const name = line.slice(0, colon).trim();
|
|
165
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) continue;
|
|
166
|
+
headers[name] = asciiHeaderValue(line.slice(colon + 1));
|
|
167
|
+
}
|
|
168
|
+
return headers;
|
|
169
|
+
}
|
|
170
|
+
|
|
155
171
|
export function getCommonHeaders(): Record<string, string> {
|
|
156
172
|
const headers = {
|
|
157
173
|
"User-Agent": KIMI_CODE_USER_AGENT,
|
|
@@ -166,3 +182,28 @@ export function getCommonHeaders(): Record<string, string> {
|
|
|
166
182
|
Object.entries(headers).map(([key, value]) => [key, asciiHeaderValue(value)]),
|
|
167
183
|
) as Record<string, string>;
|
|
168
184
|
}
|
|
185
|
+
|
|
186
|
+
const RESERVED_PROVIDER_HEADERS = new Set([
|
|
187
|
+
"accept",
|
|
188
|
+
"anthropic-version",
|
|
189
|
+
"authorization",
|
|
190
|
+
"content-type",
|
|
191
|
+
"user-agent",
|
|
192
|
+
"x-api-key",
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
function isReservedProviderHeader(name: string): boolean {
|
|
196
|
+
const normalized = name.toLowerCase();
|
|
197
|
+
return RESERVED_PROVIDER_HEADERS.has(normalized) || normalized.startsWith("x-msh-");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function getKimiProviderHeaders(
|
|
201
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
202
|
+
): Record<string, string> {
|
|
203
|
+
const customHeaders = Object.fromEntries(
|
|
204
|
+
Object.entries(parseKimiCodeCustomHeaders(env)).filter(
|
|
205
|
+
([name]) => !isReservedProviderHeader(name),
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
return { ...customHeaders, ...getCommonHeaders() };
|
|
209
|
+
}
|
package/src/models.ts
CHANGED
|
@@ -5,9 +5,9 @@ import type { Api, Model, OAuthCredentials } from "@earendil-works/pi-ai";
|
|
|
5
5
|
import type { KimiInputModality, KimiResolvedModelConfig, ModelConfig } from "./config.ts";
|
|
6
6
|
|
|
7
7
|
import { type KimiWireProtocol, getBaseUrl } from "./constants.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { getKimiProviderHeaders } from "./device.ts";
|
|
9
9
|
|
|
10
|
-
export interface
|
|
10
|
+
export interface KimiModelMetadata {
|
|
11
11
|
wireModelId?: string;
|
|
12
12
|
modelDisplay?: string;
|
|
13
13
|
contextLength?: number;
|
|
@@ -15,6 +15,13 @@ export interface KimiOAuthExtras {
|
|
|
15
15
|
supportsImageIn?: boolean;
|
|
16
16
|
supportsVideoIn?: boolean;
|
|
17
17
|
supportsThinkingType?: "only" | "no" | "both";
|
|
18
|
+
protocol?: KimiWireProtocol;
|
|
19
|
+
supportEfforts?: string[];
|
|
20
|
+
defaultEffort?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface KimiOAuthExtras extends KimiModelMetadata {
|
|
24
|
+
modelCatalog?: Record<string, KimiModelMetadata>;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
export type KimiOAuthCredentials = OAuthCredentials & KimiOAuthExtras;
|
|
@@ -47,6 +54,9 @@ function mergeInputModalities(
|
|
|
47
54
|
const COST_STANDARD = { input: 0.897, output: 3.724, cacheRead: 0.179, cacheWrite: 0.897 };
|
|
48
55
|
const COST_HIGH_SPEED = { input: 1.793, output: 7.448, cacheRead: 0.359, cacheWrite: 1.793 };
|
|
49
56
|
|
|
57
|
+
export const KIMI_CODING_MODEL_ID = "kimi-for-coding";
|
|
58
|
+
export const KIMI_CODING_HIGHSPEED_MODEL_ID = "kimi-for-coding-highspeed";
|
|
59
|
+
|
|
50
60
|
function resolveModelCost(modelDisplay: string | undefined): {
|
|
51
61
|
input: number;
|
|
52
62
|
output: number;
|
|
@@ -57,13 +67,17 @@ function resolveModelCost(modelDisplay: string | undefined): {
|
|
|
57
67
|
return COST_STANDARD;
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
export function buildKimiModelFromConfig(
|
|
70
|
+
export function buildKimiModelFromConfig(
|
|
71
|
+
config: ModelConfig,
|
|
72
|
+
modelId = KIMI_CODING_MODEL_ID,
|
|
73
|
+
): Model<Api> {
|
|
74
|
+
const isHighSpeed = modelId === KIMI_CODING_HIGHSPEED_MODEL_ID;
|
|
61
75
|
return {
|
|
62
|
-
id:
|
|
63
|
-
name: "Kimi for Coding",
|
|
76
|
+
id: modelId,
|
|
77
|
+
name: isHighSpeed ? "Kimi for Coding High Speed" : "Kimi for Coding",
|
|
64
78
|
reasoning: config.reasoning,
|
|
65
79
|
input: [...config.input] as unknown as ("text" | "image" | "video")[],
|
|
66
|
-
cost: { ...COST_STANDARD },
|
|
80
|
+
cost: { ...(isHighSpeed ? COST_HIGH_SPEED : COST_STANDARD) },
|
|
67
81
|
contextWindow: config.contextWindow,
|
|
68
82
|
maxTokens: config.maxTokens,
|
|
69
83
|
} as Model<Api>;
|
|
@@ -86,6 +100,8 @@ export function resolveKimiModelConfig(
|
|
|
86
100
|
if (typeof extras.supportsImageIn === "boolean" || typeof extras.supportsVideoIn === "boolean") {
|
|
87
101
|
resolved.input = mergeInputModalities(resolved.input, extras);
|
|
88
102
|
}
|
|
103
|
+
if (extras.supportEfforts) resolved.supportEfforts = [...extras.supportEfforts];
|
|
104
|
+
if (extras.defaultEffort) resolved.defaultEffort = extras.defaultEffort;
|
|
89
105
|
return resolved;
|
|
90
106
|
}
|
|
91
107
|
|
|
@@ -97,6 +113,8 @@ interface KimiServerModel {
|
|
|
97
113
|
supports_image_in?: unknown;
|
|
98
114
|
supports_video_in?: unknown;
|
|
99
115
|
supports_thinking_type?: unknown;
|
|
116
|
+
protocol?: unknown;
|
|
117
|
+
think_efforts?: unknown;
|
|
100
118
|
}
|
|
101
119
|
|
|
102
120
|
function parseSupportsThinkingType(value: unknown): "only" | "no" | "both" | undefined {
|
|
@@ -104,6 +122,54 @@ function parseSupportsThinkingType(value: unknown): "only" | "no" | "both" | und
|
|
|
104
122
|
return undefined;
|
|
105
123
|
}
|
|
106
124
|
|
|
125
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
126
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseThinkEfforts(value: unknown): {
|
|
130
|
+
supportEfforts?: string[];
|
|
131
|
+
defaultEffort?: string;
|
|
132
|
+
} {
|
|
133
|
+
if (!isRecord(value) || value.support !== true) return {};
|
|
134
|
+
const validEfforts = Array.isArray(value.valid_efforts)
|
|
135
|
+
? value.valid_efforts.filter(
|
|
136
|
+
(effort): effort is string => typeof effort === "string" && !!effort,
|
|
137
|
+
)
|
|
138
|
+
: [];
|
|
139
|
+
return {
|
|
140
|
+
...(validEfforts.length > 0 ? { supportEfforts: validEfforts } : {}),
|
|
141
|
+
...(typeof value.default_effort === "string" && value.default_effort
|
|
142
|
+
? { defaultEffort: value.default_effort }
|
|
143
|
+
: {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseKimiModelMetadata(model: KimiServerModel): KimiModelMetadata | undefined {
|
|
148
|
+
if (typeof model.id !== "string" || !model.id) return undefined;
|
|
149
|
+
const metadata: KimiModelMetadata = { wireModelId: model.id };
|
|
150
|
+
if (typeof model.display_name === "string") metadata.modelDisplay = model.display_name;
|
|
151
|
+
if (typeof model.context_length === "number" && model.context_length > 0) {
|
|
152
|
+
metadata.contextLength = model.context_length;
|
|
153
|
+
}
|
|
154
|
+
const thinkingType = parseSupportsThinkingType(model.supports_thinking_type);
|
|
155
|
+
if (thinkingType) {
|
|
156
|
+
metadata.supportsThinkingType = thinkingType;
|
|
157
|
+
} else if (typeof model.supports_reasoning === "boolean") {
|
|
158
|
+
metadata.supportsReasoning = model.supports_reasoning;
|
|
159
|
+
}
|
|
160
|
+
if (typeof model.supports_image_in === "boolean") {
|
|
161
|
+
metadata.supportsImageIn = model.supports_image_in;
|
|
162
|
+
}
|
|
163
|
+
if (typeof model.supports_video_in === "boolean") {
|
|
164
|
+
metadata.supportsVideoIn = model.supports_video_in;
|
|
165
|
+
}
|
|
166
|
+
if (model.protocol === "openai" || model.protocol === "anthropic") {
|
|
167
|
+
metadata.protocol = model.protocol;
|
|
168
|
+
}
|
|
169
|
+
Object.assign(metadata, parseThinkEfforts(model.think_efforts));
|
|
170
|
+
return metadata;
|
|
171
|
+
}
|
|
172
|
+
|
|
107
173
|
export function buildModelsUrl(baseUrl: string): string {
|
|
108
174
|
const base = baseUrl.replace(/\/+$/, "");
|
|
109
175
|
return base.endsWith("/v1") ? `${base}/models` : `${base}/v1/models`;
|
|
@@ -127,7 +193,7 @@ export async function discoverKimiModelMetadata(
|
|
|
127
193
|
const response = await fetch(getModelsUrl(protocol), {
|
|
128
194
|
signal: controller.signal,
|
|
129
195
|
headers: {
|
|
130
|
-
...
|
|
196
|
+
...getKimiProviderHeaders(),
|
|
131
197
|
Authorization: `Bearer ${accessToken}`,
|
|
132
198
|
Accept: "application/json",
|
|
133
199
|
},
|
|
@@ -135,26 +201,18 @@ export async function discoverKimiModelMetadata(
|
|
|
135
201
|
if (!response.ok) return {};
|
|
136
202
|
const json = (await response.json()) as { data?: unknown };
|
|
137
203
|
const list = Array.isArray(json.data) ? (json.data as KimiServerModel[]) : [];
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
const thinkingType = parseSupportsThinkingType(preferred.supports_thinking_type);
|
|
146
|
-
if (thinkingType) {
|
|
147
|
-
extras.supportsThinkingType = thinkingType;
|
|
148
|
-
} else if (typeof preferred.supports_reasoning === "boolean") {
|
|
149
|
-
extras.supportsReasoning = preferred.supports_reasoning;
|
|
150
|
-
}
|
|
151
|
-
if (typeof preferred.supports_image_in === "boolean") {
|
|
152
|
-
extras.supportsImageIn = preferred.supports_image_in;
|
|
204
|
+
const supportedIds = new Set([KIMI_CODING_MODEL_ID, KIMI_CODING_HIGHSPEED_MODEL_ID]);
|
|
205
|
+
const modelCatalog: Record<string, KimiModelMetadata> = {};
|
|
206
|
+
for (const model of list) {
|
|
207
|
+
if (typeof model.id !== "string" || !supportedIds.has(model.id)) continue;
|
|
208
|
+
const metadata = parseKimiModelMetadata(model);
|
|
209
|
+
if (metadata) modelCatalog[model.id] = metadata;
|
|
153
210
|
}
|
|
154
|
-
|
|
155
|
-
|
|
211
|
+
const preferred = modelCatalog[KIMI_CODING_MODEL_ID];
|
|
212
|
+
if (!preferred) {
|
|
213
|
+
return Object.keys(modelCatalog).length > 0 ? { modelCatalog } : {};
|
|
156
214
|
}
|
|
157
|
-
return
|
|
215
|
+
return { ...preferred, modelCatalog };
|
|
158
216
|
} catch {
|
|
159
217
|
return {};
|
|
160
218
|
} finally {
|
|
@@ -162,12 +220,23 @@ export async function discoverKimiModelMetadata(
|
|
|
162
220
|
}
|
|
163
221
|
}
|
|
164
222
|
|
|
223
|
+
export function getKimiModelMetadata(extras: KimiOAuthExtras, modelId: string): KimiModelMetadata {
|
|
224
|
+
const discovered = extras.modelCatalog?.[modelId];
|
|
225
|
+
if (discovered) return discovered;
|
|
226
|
+
return modelId === KIMI_CODING_MODEL_ID ? extras : {};
|
|
227
|
+
}
|
|
228
|
+
|
|
165
229
|
export function applyKimiOAuthExtrasToModel(
|
|
166
230
|
model: Model<Api>,
|
|
167
|
-
extras:
|
|
231
|
+
extras: KimiModelMetadata,
|
|
168
232
|
): Model<Api> {
|
|
169
|
-
const next: Model<Api> & {
|
|
170
|
-
|
|
233
|
+
const next: Model<Api> & {
|
|
234
|
+
wireModelId?: string;
|
|
235
|
+
supportsThinkingType?: "only" | "no" | "both";
|
|
236
|
+
wireProtocol?: KimiWireProtocol;
|
|
237
|
+
supportEfforts?: string[];
|
|
238
|
+
defaultEffort?: string;
|
|
239
|
+
} = { ...model };
|
|
171
240
|
if (typeof extras.modelDisplay === "string" && extras.modelDisplay) {
|
|
172
241
|
next.name = extras.modelDisplay;
|
|
173
242
|
next.cost = resolveModelCost(extras.modelDisplay);
|
|
@@ -189,5 +258,8 @@ export function applyKimiOAuthExtrasToModel(
|
|
|
189
258
|
const input = mergeInputModalities(next.input as KimiInputModality[], extras);
|
|
190
259
|
(next as unknown as { input: string[] }).input = input;
|
|
191
260
|
}
|
|
261
|
+
if (extras.protocol) next.wireProtocol = extras.protocol;
|
|
262
|
+
if (extras.supportEfforts) next.supportEfforts = [...extras.supportEfforts];
|
|
263
|
+
if (extras.defaultEffort) next.defaultEffort = extras.defaultEffort;
|
|
192
264
|
return next;
|
|
193
265
|
}
|
package/src/oauth.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// reuse, the login / refresh handlers wired into pi's OAuth interface, and
|
|
3
3
|
// the stream-level auth refresh used by the streaming handler.
|
|
4
4
|
|
|
5
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import os from "node:os";
|
|
7
|
-
import { join } from "node:path";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
8
|
|
|
9
9
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
|
|
10
10
|
import type { OAuthCredential } from "@earendil-works/pi-coding-agent";
|
|
@@ -123,6 +123,16 @@ interface RefreshAccessTokenOptions {
|
|
|
123
123
|
sleep?: (ms: number) => Promise<void>;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
export const KIMI_LOGIN_REQUIRED_MESSAGE =
|
|
127
|
+
"Kimi Code authorization is no longer valid. Sign in again with /login kimi-coding.";
|
|
128
|
+
|
|
129
|
+
export class KimiLoginRequiredError extends Error {
|
|
130
|
+
constructor() {
|
|
131
|
+
super(KIMI_LOGIN_REQUIRED_MESSAGE);
|
|
132
|
+
this.name = "KimiLoginRequiredError";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
126
136
|
function sleep(ms: number): Promise<void> {
|
|
127
137
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
128
138
|
}
|
|
@@ -152,8 +162,15 @@ export async function refreshAccessToken(
|
|
|
152
162
|
|
|
153
163
|
if (!response.ok) {
|
|
154
164
|
const text = await response.text().catch(() => "");
|
|
155
|
-
|
|
156
|
-
|
|
165
|
+
let errorCode = "";
|
|
166
|
+
try {
|
|
167
|
+
const body = JSON.parse(text) as { error?: unknown };
|
|
168
|
+
if (typeof body.error === "string") errorCode = body.error;
|
|
169
|
+
} catch {
|
|
170
|
+
// Non-JSON OAuth errors are classified by status below.
|
|
171
|
+
}
|
|
172
|
+
if (response.status === 401 || response.status === 403 || errorCode === "invalid_grant") {
|
|
173
|
+
throw new KimiLoginRequiredError();
|
|
157
174
|
}
|
|
158
175
|
if (RETRYABLE_REFRESH_STATUSES.has(response.status)) {
|
|
159
176
|
throw new Error(`Token refresh retryable: ${response.status} ${text}`);
|
|
@@ -170,7 +187,7 @@ export async function refreshAccessToken(
|
|
|
170
187
|
} catch (error) {
|
|
171
188
|
lastError = error;
|
|
172
189
|
const message = error instanceof Error ? error.message : String(error);
|
|
173
|
-
if (
|
|
190
|
+
if (error instanceof KimiLoginRequiredError) throw error;
|
|
174
191
|
if (message.startsWith("Token refresh failed:")) throw error;
|
|
175
192
|
if (attempt < maxRetries - 1) {
|
|
176
193
|
await wait(2 ** attempt * 1000);
|
|
@@ -196,15 +213,19 @@ interface KimiCliCredentialsFile {
|
|
|
196
213
|
access_token?: string;
|
|
197
214
|
refresh_token?: string;
|
|
198
215
|
expires_at?: number; // Unix seconds (upstream convention)
|
|
216
|
+
scope?: string;
|
|
217
|
+
token_type?: string;
|
|
218
|
+
expires_in?: number;
|
|
199
219
|
}
|
|
200
220
|
|
|
201
|
-
function
|
|
221
|
+
function getKimiCodeCredentialPath(): string {
|
|
202
222
|
const kimiCodeHome = process.env.KIMI_CODE_HOME || join(os.homedir(), ".kimi-code");
|
|
223
|
+
return join(kimiCodeHome, "credentials", "kimi-code.json");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getKimiCredentialPaths(): string[] {
|
|
203
227
|
const shareDir = process.env.KIMI_SHARE_DIR || join(os.homedir(), ".kimi");
|
|
204
|
-
return [
|
|
205
|
-
join(kimiCodeHome, "credentials", "kimi-code.json"),
|
|
206
|
-
join(shareDir, "credentials", "kimi-code.json"),
|
|
207
|
-
];
|
|
228
|
+
return [getKimiCodeCredentialPath(), join(shareDir, "credentials", "kimi-code.json")];
|
|
208
229
|
}
|
|
209
230
|
|
|
210
231
|
function readKimiCliCredentials(): KimiCliCredentialsFile | null {
|
|
@@ -221,6 +242,25 @@ function readKimiCliCredentials(): KimiCliCredentialsFile | null {
|
|
|
221
242
|
return null;
|
|
222
243
|
}
|
|
223
244
|
|
|
245
|
+
function kimiCodeCredentialExists(): boolean {
|
|
246
|
+
return existsSync(getKimiCodeCredentialPath());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function writeKimiCodeCredentials(access: string, refresh: string, expiresMs: number): void {
|
|
250
|
+
const path = getKimiCodeCredentialPath();
|
|
251
|
+
const dir = dirname(path);
|
|
252
|
+
mkdirSync(dir, { recursive: true });
|
|
253
|
+
const data: KimiCliCredentialsFile = {
|
|
254
|
+
access_token: access,
|
|
255
|
+
refresh_token: refresh,
|
|
256
|
+
expires_at: Math.floor(expiresMs / 1000),
|
|
257
|
+
scope: "",
|
|
258
|
+
token_type: "Bearer",
|
|
259
|
+
expires_in: Math.max(0, Math.floor((expiresMs - Date.now()) / 1000)),
|
|
260
|
+
};
|
|
261
|
+
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
|
|
262
|
+
}
|
|
263
|
+
|
|
224
264
|
async function tryReuseKimiCliCredentials(
|
|
225
265
|
callbacks: OAuthLoginCallbacks,
|
|
226
266
|
): Promise<KimiOAuthCredentials | null> {
|
|
@@ -247,11 +287,13 @@ async function tryReuseKimiCliCredentials(
|
|
|
247
287
|
callbacks.onProgress?.("Kimi access token expired, refreshing.");
|
|
248
288
|
try {
|
|
249
289
|
const token = await refreshAccessToken(data.refresh_token!);
|
|
290
|
+
const expiresMs = Date.now() + token.expires_in * 1000;
|
|
291
|
+
writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
|
|
250
292
|
const extras = await discoverKimiModelMetadata(token.access_token);
|
|
251
293
|
return {
|
|
252
294
|
access: token.access_token,
|
|
253
295
|
refresh: token.refresh_token,
|
|
254
|
-
expires:
|
|
296
|
+
expires: expiresMs,
|
|
255
297
|
...extras,
|
|
256
298
|
};
|
|
257
299
|
} catch {
|
|
@@ -319,11 +361,13 @@ export async function loginKimiCode(callbacks: OAuthLoginCallbacks): Promise<Kim
|
|
|
319
361
|
}
|
|
320
362
|
|
|
321
363
|
if (token) {
|
|
364
|
+
const expiresMs = Date.now() + token.expires_in * 1000;
|
|
365
|
+
writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
|
|
322
366
|
const extras = await discoverKimiModelMetadata(token.access_token);
|
|
323
367
|
return {
|
|
324
368
|
access: token.access_token,
|
|
325
369
|
refresh: token.refresh_token,
|
|
326
|
-
expires:
|
|
370
|
+
expires: expiresMs,
|
|
327
371
|
...extras,
|
|
328
372
|
};
|
|
329
373
|
}
|
|
@@ -335,12 +379,24 @@ export async function loginKimiCode(callbacks: OAuthLoginCallbacks): Promise<Kim
|
|
|
335
379
|
export async function refreshKimiCodeToken(
|
|
336
380
|
credentials: OAuthCredentials,
|
|
337
381
|
): Promise<KimiOAuthCredentials> {
|
|
338
|
-
const
|
|
382
|
+
const kimiCred = kimiCodeCredentialExists() ? readKimiCliCredentials() : null;
|
|
383
|
+
let token: Awaited<ReturnType<typeof refreshAccessToken>>;
|
|
384
|
+
try {
|
|
385
|
+
token = await refreshAccessToken(credentials.refresh);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (kimiCred?.refresh_token && kimiCred.refresh_token !== credentials.refresh) {
|
|
388
|
+
token = await refreshAccessToken(kimiCred.refresh_token);
|
|
389
|
+
} else {
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const expiresMs = Date.now() + token.expires_in * 1000;
|
|
394
|
+
writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
|
|
339
395
|
const extras = await discoverKimiModelMetadata(token.access_token);
|
|
340
396
|
return {
|
|
341
397
|
access: token.access_token,
|
|
342
398
|
refresh: token.refresh_token,
|
|
343
|
-
expires:
|
|
399
|
+
expires: expiresMs,
|
|
344
400
|
...extras,
|
|
345
401
|
};
|
|
346
402
|
}
|
|
@@ -356,6 +412,10 @@ export async function refreshKimiCodeToken(
|
|
|
356
412
|
// new credentials under a file lock), and retry once.
|
|
357
413
|
// =============================================================================
|
|
358
414
|
|
|
415
|
+
export function getKimiApiKey(credentials: OAuthCredentials): string {
|
|
416
|
+
return credentials.access;
|
|
417
|
+
}
|
|
418
|
+
|
|
359
419
|
export function isKimiAuthErrorMessage(message: unknown): boolean {
|
|
360
420
|
const text = String(message ?? "").toLowerCase();
|
|
361
421
|
return (
|
|
@@ -368,37 +428,79 @@ export function isKimiAuthErrorMessage(message: unknown): boolean {
|
|
|
368
428
|
|
|
369
429
|
export async function refreshKimiAuthToken(currentKey: string): Promise<string | null> {
|
|
370
430
|
try {
|
|
431
|
+
const kimiCred = readKimiCliCredentials();
|
|
371
432
|
const storage = AuthStorage.create();
|
|
372
|
-
const
|
|
373
|
-
|
|
433
|
+
const piCred = storage.get(PROVIDER_ID);
|
|
434
|
+
const piOAuth = piCred?.type === "oauth" ? piCred : null;
|
|
435
|
+
|
|
436
|
+
if (piOAuth) {
|
|
437
|
+
if (piOAuth.access !== currentKey && Date.now() < piOAuth.expires) {
|
|
438
|
+
console.error("[kimi-coding] auth refresh: reusing newer on-disk token");
|
|
439
|
+
return piOAuth.access;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
console.error("[kimi-coding] auth refresh: requesting new access token");
|
|
443
|
+
let refreshed: Awaited<ReturnType<typeof refreshAccessToken>>;
|
|
444
|
+
try {
|
|
445
|
+
refreshed = await refreshAccessToken(piOAuth.refresh);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
if (!kimiCred?.refresh_token || kimiCred.refresh_token === piOAuth.refresh) throw error;
|
|
448
|
+
const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
|
|
449
|
+
if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
|
|
450
|
+
const recovered: OAuthCredential = {
|
|
451
|
+
...piOAuth,
|
|
452
|
+
type: "oauth",
|
|
453
|
+
access: kimiCred.access_token!,
|
|
454
|
+
refresh: kimiCred.refresh_token,
|
|
455
|
+
expires: kimiExpiresMs,
|
|
456
|
+
};
|
|
457
|
+
storage.set(PROVIDER_ID, recovered);
|
|
458
|
+
console.error("[kimi-coding] auth refresh: recovered newer kimi-code token");
|
|
459
|
+
return recovered.access;
|
|
460
|
+
}
|
|
461
|
+
console.error("[kimi-coding] auth refresh: pi token rejected, trying kimi-code token");
|
|
462
|
+
refreshed = await refreshAccessToken(kimiCred.refresh_token);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
|
|
466
|
+
const newCred: OAuthCredential = {
|
|
467
|
+
...piOAuth,
|
|
468
|
+
type: "oauth",
|
|
469
|
+
access: refreshed.access_token,
|
|
470
|
+
refresh: refreshed.refresh_token,
|
|
471
|
+
expires: newExpiresMs,
|
|
472
|
+
};
|
|
473
|
+
storage.set(PROVIDER_ID, newCred);
|
|
474
|
+
writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
|
|
475
|
+
console.error("[kimi-coding] auth refresh: new token persisted");
|
|
476
|
+
return newCred.access;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (!kimiCred) {
|
|
374
480
|
console.error(
|
|
375
481
|
`[kimi-coding] auth refresh skipped: no OAuth credentials for ${PROVIDER_ID} on disk`,
|
|
376
482
|
);
|
|
377
483
|
return null;
|
|
378
484
|
}
|
|
379
485
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
console.error("[kimi-coding] auth refresh: reusing newer on-disk token");
|
|
385
|
-
return cred.access;
|
|
486
|
+
const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
|
|
487
|
+
if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
|
|
488
|
+
console.error("[kimi-coding] auth refresh: reusing newer kimi-code token");
|
|
489
|
+
return kimiCred.access_token!;
|
|
386
490
|
}
|
|
387
491
|
|
|
388
|
-
console.error("[kimi-coding] auth refresh: requesting new access token");
|
|
389
|
-
const refreshed = await refreshAccessToken(
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
refresh: refreshed.refresh_token,
|
|
395
|
-
expires: Date.now() + refreshed.expires_in * 1000,
|
|
396
|
-
};
|
|
397
|
-
storage.set(PROVIDER_ID, newCred);
|
|
398
|
-
console.error("[kimi-coding] auth refresh: new token persisted");
|
|
399
|
-
return newCred.access;
|
|
492
|
+
console.error("[kimi-coding] auth refresh: requesting new access token via kimi-code");
|
|
493
|
+
const refreshed = await refreshAccessToken(kimiCred.refresh_token!);
|
|
494
|
+
const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
|
|
495
|
+
writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
|
|
496
|
+
console.error("[kimi-coding] auth refresh: new token persisted to kimi-code");
|
|
497
|
+
return refreshed.access_token;
|
|
400
498
|
} catch (err) {
|
|
401
|
-
|
|
499
|
+
if (err instanceof KimiLoginRequiredError) {
|
|
500
|
+
console.error(`[kimi-coding] ${KIMI_LOGIN_REQUIRED_MESSAGE}`);
|
|
501
|
+
} else {
|
|
502
|
+
console.error("[kimi-coding] auth refresh failed:", err);
|
|
503
|
+
}
|
|
402
504
|
return null;
|
|
403
505
|
}
|
|
404
506
|
}
|
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) {
|
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,
|
package/src/usage.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AuthStorage } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
import { PROVIDER_ID, getBaseUrl } from "./constants.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { getKimiProviderHeaders } from "./device.ts";
|
|
5
5
|
import { refreshKimiAuthToken } from "./oauth.ts";
|
|
6
6
|
|
|
7
7
|
const MEMBERSHIP_LEVEL_NAMES: Record<string, string> = {
|
|
@@ -36,6 +36,16 @@ export interface UsageFormatOptions {
|
|
|
36
36
|
timeZone?: string;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
interface BoosterWalletInfo {
|
|
40
|
+
balanceCents: bigint;
|
|
41
|
+
monthlyChargeLimitEnabled: boolean;
|
|
42
|
+
monthlyChargeLimitCents: bigint;
|
|
43
|
+
monthlyUsedCents: bigint;
|
|
44
|
+
currency: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const FIXED_POINT_CENTS = 1_000_000n;
|
|
48
|
+
|
|
39
49
|
export function getKimiUsageToken(): string | null {
|
|
40
50
|
const credential = AuthStorage.create().get(PROVIDER_ID);
|
|
41
51
|
if (credential?.type === "oauth" && credential.access) return credential.access;
|
|
@@ -62,6 +72,82 @@ function getResetTime(record: Record<string, unknown>): string | undefined {
|
|
|
62
72
|
return undefined;
|
|
63
73
|
}
|
|
64
74
|
|
|
75
|
+
function toBigInt(value: unknown): bigint | null {
|
|
76
|
+
if (typeof value === "bigint") return value;
|
|
77
|
+
if (typeof value === "string" && /^-?\d+$/.test(value)) return BigInt(value);
|
|
78
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) return BigInt(value);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function fixedPointToCents(value: bigint): bigint {
|
|
83
|
+
if (value > 0n && value < FIXED_POINT_CENTS) return 1n;
|
|
84
|
+
return (value + FIXED_POINT_CENTS / 2n) / FIXED_POINT_CENTS;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseMoney(value: unknown): { cents: bigint; currency: string } | null {
|
|
88
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
89
|
+
const record = value as Record<string, unknown>;
|
|
90
|
+
const cents = toBigInt(record.priceInCents);
|
|
91
|
+
if (cents === null) return null;
|
|
92
|
+
return {
|
|
93
|
+
cents,
|
|
94
|
+
currency: typeof record.currency === "string" ? record.currency : "",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseBoosterWallet(value: unknown): BoosterWalletInfo | null {
|
|
99
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
100
|
+
const record = value as Record<string, unknown>;
|
|
101
|
+
const balance = record.balance;
|
|
102
|
+
if (typeof balance !== "object" || balance === null || Array.isArray(balance)) return null;
|
|
103
|
+
const balanceRecord = balance as Record<string, unknown>;
|
|
104
|
+
if (balanceRecord.type !== "BOOSTER") return null;
|
|
105
|
+
const amount = toBigInt(balanceRecord.amount);
|
|
106
|
+
if (amount === null || amount < 0n) return null;
|
|
107
|
+
|
|
108
|
+
const amountLeft = toBigInt(balanceRecord.amountLeft);
|
|
109
|
+
const monthlyLimit = parseMoney(record.monthlyChargeLimit);
|
|
110
|
+
const monthlyUsed = parseMoney(record.monthlyUsed);
|
|
111
|
+
return {
|
|
112
|
+
balanceCents: amountLeft === null ? 0n : fixedPointToCents(amountLeft),
|
|
113
|
+
monthlyChargeLimitEnabled: record.monthlyChargeLimitEnabled === true,
|
|
114
|
+
monthlyChargeLimitCents: monthlyLimit?.cents ?? 0n,
|
|
115
|
+
monthlyUsedCents: monthlyUsed?.cents ?? 0n,
|
|
116
|
+
currency: monthlyLimit?.currency || monthlyUsed?.currency || "USD",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function formatCurrency(cents: bigint, currency: string): string {
|
|
121
|
+
const symbol =
|
|
122
|
+
currency.toUpperCase() === "USD" ? "$" : currency.toUpperCase() === "CNY" ? "¥" : "";
|
|
123
|
+
const sign = cents < 0n ? "-" : "";
|
|
124
|
+
const absolute = cents < 0n ? -cents : cents;
|
|
125
|
+
const amount = `${sign}${absolute / 100n}.${String(absolute % 100n).padStart(2, "0")}`;
|
|
126
|
+
return symbol ? `${symbol}${amount}` : `${amount} ${currency}`.trim();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function formatExtraUsage(info: BoosterWalletInfo): string[] {
|
|
130
|
+
const hasMonthlyLimit = info.monthlyChargeLimitEnabled && info.monthlyChargeLimitCents > 0n;
|
|
131
|
+
const lines = ["Extra Usage"];
|
|
132
|
+
if (hasMonthlyLimit) {
|
|
133
|
+
const used =
|
|
134
|
+
info.monthlyUsedCents < info.monthlyChargeLimitCents
|
|
135
|
+
? info.monthlyUsedCents
|
|
136
|
+
: info.monthlyChargeLimitCents;
|
|
137
|
+
const percent = Number(
|
|
138
|
+
(used * 100n + info.monthlyChargeLimitCents / 2n) / info.monthlyChargeLimitCents,
|
|
139
|
+
);
|
|
140
|
+
const barUnits = Number((used * 400n) / info.monthlyChargeLimitCents);
|
|
141
|
+
lines.push(`${quotaBar(barUnits, 400)} ${percent}% used`);
|
|
142
|
+
}
|
|
143
|
+
lines.push(`Used this month: ${formatCurrency(info.monthlyUsedCents, info.currency)}`);
|
|
144
|
+
lines.push(
|
|
145
|
+
`Monthly limit: ${hasMonthlyLimit ? formatCurrency(info.monthlyChargeLimitCents, info.currency) : "Unlimited"}`,
|
|
146
|
+
);
|
|
147
|
+
lines.push(`Balance: ${formatCurrency(info.balanceCents, info.currency)}`);
|
|
148
|
+
return lines;
|
|
149
|
+
}
|
|
150
|
+
|
|
65
151
|
export function parseUsageRow(value: unknown, fallbackLabel: string): UsageRow | null {
|
|
66
152
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
67
153
|
const record = value as Record<string, unknown>;
|
|
@@ -115,6 +201,12 @@ export function parseUsageSummary(payload: unknown, options: UsageFormatOptions
|
|
|
115
201
|
}
|
|
116
202
|
}
|
|
117
203
|
|
|
204
|
+
const extraUsage = parseBoosterWallet(record.boosterWallet);
|
|
205
|
+
if (extraUsage) {
|
|
206
|
+
if (lines.length > 0) lines.push("");
|
|
207
|
+
lines.push(...formatExtraUsage(extraUsage));
|
|
208
|
+
}
|
|
209
|
+
|
|
118
210
|
while (lines.at(-1) === "") lines.pop();
|
|
119
211
|
return lines.length === 0 ? "Usage: no usage data" : lines.join("\n");
|
|
120
212
|
}
|
|
@@ -244,7 +336,7 @@ function fetchKimiUsage(token: string, signal: AbortSignal): Promise<Response> {
|
|
|
244
336
|
return fetch(buildKimiUsageUrl(), {
|
|
245
337
|
method: "GET",
|
|
246
338
|
headers: {
|
|
247
|
-
...
|
|
339
|
+
...getKimiProviderHeaders(),
|
|
248
340
|
Authorization: `Bearer ${token}`,
|
|
249
341
|
},
|
|
250
342
|
signal,
|