pi-provider-kimi-code 0.3.1 → 0.4.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 +149 -62
- package/index.ts +325 -879
- package/package.json +10 -6
- package/src/config.ts +151 -0
- package/src/constants.ts +48 -0
- package/src/device.ts +164 -0
- package/src/models.ts +129 -0
- package/src/oauth.ts +397 -0
- package/src/payload.ts +446 -0
- package/src/stream.ts +299 -0
- package/src/tools/moonshot.ts +316 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-provider-kimi-code",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Use Kimi Code in Pi without losing the Kimi-specific bits",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"pi-extension",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"author": "Leechael",
|
|
37
37
|
"files": [
|
|
38
38
|
"index.ts",
|
|
39
|
+
"src",
|
|
39
40
|
"package.json",
|
|
40
41
|
"README.md"
|
|
41
42
|
],
|
|
@@ -43,20 +44,23 @@
|
|
|
43
44
|
"scripts": {
|
|
44
45
|
"clean": "echo 'nothing to clean'",
|
|
45
46
|
"build": "echo 'nothing to build'",
|
|
46
|
-
"check": "
|
|
47
|
+
"check": "tsc --noEmit",
|
|
48
|
+
"test": "node --test tests/*.test.ts",
|
|
47
49
|
"lint": "oxlint .",
|
|
48
50
|
"format": "oxfmt . --write",
|
|
49
51
|
"format:check": "oxfmt . --check",
|
|
50
52
|
"pre-commit": "prek run"
|
|
51
53
|
},
|
|
52
54
|
"peerDependencies": {
|
|
53
|
-
"@
|
|
54
|
-
"@
|
|
55
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
56
|
+
"@earendil-works/pi-ai": "*"
|
|
55
57
|
},
|
|
56
58
|
"devDependencies": {
|
|
57
59
|
"@j178/prek": "^0.3.8",
|
|
60
|
+
"@types/node": "^24.0.0",
|
|
58
61
|
"oxfmt": "^0.44.0",
|
|
59
|
-
"oxlint": "^1.59.0"
|
|
62
|
+
"oxlint": "^1.59.0",
|
|
63
|
+
"typescript": "^5.9.0"
|
|
60
64
|
},
|
|
61
65
|
"pi": {
|
|
62
66
|
"extensions": [
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface KimiCodeConfig {
|
|
5
|
+
tools: {
|
|
6
|
+
moonshot_search: { enabled: boolean; default_collapsed: boolean };
|
|
7
|
+
moonshot_fetch: { enabled: boolean; default_collapsed: boolean };
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface LoadKimiCodeConfigOptions {
|
|
12
|
+
cwd: string;
|
|
13
|
+
home: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
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
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function getGlobalKimiCodeConfigPath(home: string): string {
|
|
24
|
+
return join(home, ".pi", "pi-provider-kimi-code.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getProjectKimiCodeConfigPath(cwd: string): string {
|
|
28
|
+
return join(cwd, ".pi", "pi-provider-kimi-code.json");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readConfigFile(path: string): Record<string, unknown> {
|
|
36
|
+
try {
|
|
37
|
+
if (!existsSync(path)) return {};
|
|
38
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
39
|
+
return isRecord(parsed) ? parsed : {};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(`[kimi-coding] failed to read config file ${path}:`, error);
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readConfigFileQuiet(path: string): Record<string, unknown> {
|
|
47
|
+
try {
|
|
48
|
+
if (!existsSync(path)) return {};
|
|
49
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
50
|
+
return isRecord(parsed) ? parsed : {};
|
|
51
|
+
} catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mergeConfig(
|
|
57
|
+
base: Record<string, unknown>,
|
|
58
|
+
override: Record<string, unknown>,
|
|
59
|
+
): Record<string, unknown> {
|
|
60
|
+
const result: Record<string, unknown> = { ...base };
|
|
61
|
+
for (const [key, value] of Object.entries(override)) {
|
|
62
|
+
const current = result[key];
|
|
63
|
+
if (isRecord(current) && isRecord(value)) {
|
|
64
|
+
result[key] = mergeConfig(current, value);
|
|
65
|
+
} else {
|
|
66
|
+
result[key] = value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readEnabled(config: Record<string, unknown>, toolName: string): boolean {
|
|
73
|
+
const tools = config.tools;
|
|
74
|
+
if (!isRecord(tools)) return false;
|
|
75
|
+
const tool = tools[toolName];
|
|
76
|
+
if (!isRecord(tool)) return false;
|
|
77
|
+
return tool.enabled === true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readDefaultCollapsed(config: Record<string, unknown>, toolName: string): boolean {
|
|
81
|
+
const tools = config.tools;
|
|
82
|
+
if (!isRecord(tools)) return true;
|
|
83
|
+
const tool = tools[toolName];
|
|
84
|
+
if (!isRecord(tool)) return true;
|
|
85
|
+
return tool.default_collapsed !== false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function loadKimiCodeConfig(options: LoadKimiCodeConfigOptions): KimiCodeConfig {
|
|
89
|
+
const globalConfig = readConfigFile(getGlobalKimiCodeConfigPath(options.home));
|
|
90
|
+
const projectConfig = readConfigFile(getProjectKimiCodeConfigPath(options.cwd));
|
|
91
|
+
const merged = mergeConfig(globalConfig, projectConfig);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
tools: {
|
|
95
|
+
moonshot_search: {
|
|
96
|
+
enabled: readEnabled(merged, "moonshot_search"),
|
|
97
|
+
default_collapsed: readDefaultCollapsed(merged, "moonshot_search"),
|
|
98
|
+
},
|
|
99
|
+
moonshot_fetch: {
|
|
100
|
+
enabled: readEnabled(merged, "moonshot_fetch"),
|
|
101
|
+
default_collapsed: readDefaultCollapsed(merged, "moonshot_fetch"),
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function loadProjectKimiCodeConfig(cwd: string): KimiCodeConfig {
|
|
108
|
+
return loadKimiCodeConfigFile(getProjectKimiCodeConfigPath(cwd));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function loadHomeKimiCodeConfig(home: string): KimiCodeConfig {
|
|
112
|
+
return loadKimiCodeConfigFile(getGlobalKimiCodeConfigPath(home));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function loadKimiCodeConfigFile(path: string): KimiCodeConfig {
|
|
116
|
+
const projectConfig = readConfigFileQuiet(path);
|
|
117
|
+
return {
|
|
118
|
+
tools: {
|
|
119
|
+
moonshot_search: {
|
|
120
|
+
enabled: readEnabled(projectConfig, "moonshot_search"),
|
|
121
|
+
default_collapsed: readDefaultCollapsed(projectConfig, "moonshot_search"),
|
|
122
|
+
},
|
|
123
|
+
moonshot_fetch: {
|
|
124
|
+
enabled: readEnabled(projectConfig, "moonshot_fetch"),
|
|
125
|
+
default_collapsed: readDefaultCollapsed(projectConfig, "moonshot_fetch"),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function saveProjectKimiCodeConfig(cwd: string, config: KimiCodeConfig): void {
|
|
132
|
+
saveKimiCodeConfigFile(getProjectKimiCodeConfigPath(cwd), config);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function saveHomeKimiCodeConfig(home: string, config: KimiCodeConfig): void {
|
|
136
|
+
saveKimiCodeConfigFile(getGlobalKimiCodeConfigPath(home), config);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function saveKimiCodeConfigFile(path: string, config: KimiCodeConfig): void {
|
|
140
|
+
const raw = readConfigFileQuiet(path);
|
|
141
|
+
const next = {
|
|
142
|
+
...raw,
|
|
143
|
+
tools: {
|
|
144
|
+
...(isRecord(raw.tools) ? raw.tools : {}),
|
|
145
|
+
moonshot_search: { ...config.tools.moonshot_search },
|
|
146
|
+
moonshot_fetch: { ...config.tools.moonshot_fetch },
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
150
|
+
writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
151
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Module-level constants and env-driven configuration shared across the
|
|
2
|
+
// provider's modules. Anything env-dependent that we read once at module load
|
|
3
|
+
// time lives here; helpers that need to be evaluated per request go in env.ts
|
|
4
|
+
// (not yet split).
|
|
5
|
+
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
export const CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
|
|
10
|
+
export const DEFAULT_OAUTH_HOST = "https://auth.kimi.com";
|
|
11
|
+
|
|
12
|
+
// KIMI_CODE_PROTOCOL supports two values: "openai" (default) and "anthropic".
|
|
13
|
+
export const IS_OPENAI_PROTOCOL = process.env.KIMI_CODE_PROTOCOL !== "anthropic";
|
|
14
|
+
|
|
15
|
+
export const PROTOCOL = IS_OPENAI_PROTOCOL ? "openai-completions" : "anthropic-messages";
|
|
16
|
+
|
|
17
|
+
// Use a custom api identifier so this provider never conflicts with the
|
|
18
|
+
// built-in "anthropic-messages" or "openai-completions" stream handlers.
|
|
19
|
+
export const KIMI_API_TYPE = IS_OPENAI_PROTOCOL
|
|
20
|
+
? "kimi-openai-completions"
|
|
21
|
+
: "kimi-anthropic-messages";
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_BASE_URL = IS_OPENAI_PROTOCOL
|
|
24
|
+
? "https://api.kimi.com/coding/v1"
|
|
25
|
+
: "https://api.kimi.com/coding";
|
|
26
|
+
|
|
27
|
+
export const KIMI_CLI_VERSION = "1.44.0";
|
|
28
|
+
export const KIMI_CLI_USER_AGENT = `KimiCLI/${KIMI_CLI_VERSION}`;
|
|
29
|
+
export const KIMI_PLATFORM = "kimi_cli";
|
|
30
|
+
|
|
31
|
+
export const DEVICE_ID_PATH = join(os.homedir(), ".pi", "providers", "kimi-coding", "device_id");
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_KIMI_MODEL_INPUT = ["text", "image"] as const;
|
|
34
|
+
|
|
35
|
+
export const RETRYABLE_REFRESH_STATUSES = new Set([429, 500, 502, 503, 504]);
|
|
36
|
+
|
|
37
|
+
export const PROVIDER_ID = "kimi-coding";
|
|
38
|
+
|
|
39
|
+
export function getOAuthHost(): string {
|
|
40
|
+
const value =
|
|
41
|
+
process.env.KIMI_CODE_OAUTH_HOST || process.env.KIMI_OAUTH_HOST || DEFAULT_OAUTH_HOST;
|
|
42
|
+
return value.trim() || DEFAULT_OAUTH_HOST;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getBaseUrl(): string {
|
|
46
|
+
const value = process.env.KIMI_CODE_BASE_URL || process.env.KIMI_BASE_URL || DEFAULT_BASE_URL;
|
|
47
|
+
return value.trim() || DEFAULT_BASE_URL;
|
|
48
|
+
}
|
package/src/device.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Device identification: device-id persistence, kimi-cli-compatible request
|
|
2
|
+
// headers, and the cross-platform device-model string. Pure helpers live near
|
|
3
|
+
// the side-effecting one-shots (file IO, exec) they support.
|
|
4
|
+
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import { dirname } from "node:path";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
DEVICE_ID_PATH,
|
|
13
|
+
KIMI_CLI_USER_AGENT,
|
|
14
|
+
KIMI_CLI_VERSION,
|
|
15
|
+
KIMI_PLATFORM,
|
|
16
|
+
} from "./constants.ts";
|
|
17
|
+
|
|
18
|
+
function createDeviceId(): string {
|
|
19
|
+
return randomBytes(16).toString("hex");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ensurePrivateFile(path: string): void {
|
|
23
|
+
try {
|
|
24
|
+
chmodSync(path, 0o600);
|
|
25
|
+
} catch {
|
|
26
|
+
// Ignore chmod failures on platforms/filesystems that do not support it.
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readPersistedDeviceId(): string | null {
|
|
31
|
+
try {
|
|
32
|
+
if (!existsSync(DEVICE_ID_PATH)) return null;
|
|
33
|
+
const deviceId = readFileSync(DEVICE_ID_PATH, "utf8").trim();
|
|
34
|
+
return deviceId || null;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function persistDeviceId(deviceId: string): void {
|
|
41
|
+
try {
|
|
42
|
+
mkdirSync(dirname(DEVICE_ID_PATH), { recursive: true });
|
|
43
|
+
writeFileSync(DEVICE_ID_PATH, deviceId, "utf8");
|
|
44
|
+
ensurePrivateFile(DEVICE_ID_PATH);
|
|
45
|
+
} catch {
|
|
46
|
+
// Ignore persistence failures and fall back to the in-memory device id.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getMacOSVersion(): string {
|
|
51
|
+
try {
|
|
52
|
+
return execSync("sw_vers -productVersion", { encoding: "utf8" }).trim();
|
|
53
|
+
} catch {
|
|
54
|
+
return os.release();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Normalize Node's lower-case `process.platform` (`linux`, `freebsd`,
|
|
59
|
+
// `sunos`...) to Python `platform.system()` casing used by upstream kimi-cli.
|
|
60
|
+
const SYSTEM_NAME: Record<string, string> = {
|
|
61
|
+
aix: "AIX",
|
|
62
|
+
freebsd: "FreeBSD",
|
|
63
|
+
linux: "Linux",
|
|
64
|
+
openbsd: "OpenBSD",
|
|
65
|
+
sunos: "SunOS",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export interface DeviceModelInput {
|
|
69
|
+
platform: NodeJS.Platform;
|
|
70
|
+
release: string;
|
|
71
|
+
arch: string;
|
|
72
|
+
/** macOS productVersion (e.g. "15.2"). Required only on darwin. */
|
|
73
|
+
macVersion?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Pure function exposed for tests. Mirror of upstream kimi-cli's
|
|
77
|
+
// `_device_model()` in `src/kimi_cli/auth/oauth.py`.
|
|
78
|
+
export function computeDeviceModel(input: DeviceModelInput): string {
|
|
79
|
+
const { platform, release, arch, macVersion } = input;
|
|
80
|
+
if (platform === "darwin") {
|
|
81
|
+
const version = macVersion || release;
|
|
82
|
+
if (version && arch) return `macOS ${version} ${arch}`;
|
|
83
|
+
if (version) return `macOS ${version}`;
|
|
84
|
+
return `macOS ${arch}`.trim();
|
|
85
|
+
}
|
|
86
|
+
if (platform === "win32") {
|
|
87
|
+
// Only show the major release (e.g. "Windows 10", "Windows 11") to match
|
|
88
|
+
// the upstream behavior. Windows 11 still reports kernel version
|
|
89
|
+
// "10.0.xxxxx"; treat build ≥ 22000 as Windows 11.
|
|
90
|
+
const parts = release.split(".");
|
|
91
|
+
let label = parts[0];
|
|
92
|
+
if (label === "10" && parts.length >= 3) {
|
|
93
|
+
const build = parseInt(parts[2], 10);
|
|
94
|
+
if (!isNaN(build) && build >= 22000) {
|
|
95
|
+
label = "11";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (label && arch) return `Windows ${label} ${arch}`;
|
|
99
|
+
if (label) return `Windows ${label}`;
|
|
100
|
+
return `Windows ${arch}`.trim();
|
|
101
|
+
}
|
|
102
|
+
const system = SYSTEM_NAME[platform] ?? platform;
|
|
103
|
+
if (release && arch) return `${system} ${release} ${arch}`;
|
|
104
|
+
if (release) return `${system} ${release}`;
|
|
105
|
+
return `${system} ${arch}`.trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getDeviceModel(): string {
|
|
109
|
+
return computeDeviceModel({
|
|
110
|
+
platform: process.platform,
|
|
111
|
+
release: os.release(),
|
|
112
|
+
arch: os.machine() || process.arch,
|
|
113
|
+
macVersion: process.platform === "darwin" ? getMacOSVersion() : undefined,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function getOsVersion(): string {
|
|
118
|
+
return os.version();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function asciiHeaderValue(value: string, fallback = "unknown"): string {
|
|
122
|
+
const trimmed = value.trim();
|
|
123
|
+
/* oxlint-disable-next-line no-control-regex */
|
|
124
|
+
if (/^[\x00-\x7F]*$/.test(trimmed)) {
|
|
125
|
+
return trimmed;
|
|
126
|
+
}
|
|
127
|
+
/* oxlint-disable-next-line no-control-regex */
|
|
128
|
+
const sanitized = trimmed.replace(/[^\x00-\x7F]/g, "").trim();
|
|
129
|
+
return sanitized || fallback;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const DEVICE_MODEL = getDeviceModel();
|
|
133
|
+
let DEVICE_ID: string | null = null;
|
|
134
|
+
|
|
135
|
+
function getStableDeviceId(): string {
|
|
136
|
+
if (DEVICE_ID) {
|
|
137
|
+
return DEVICE_ID;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const persisted = readPersistedDeviceId();
|
|
141
|
+
if (persisted) {
|
|
142
|
+
DEVICE_ID = persisted;
|
|
143
|
+
return DEVICE_ID;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
DEVICE_ID = createDeviceId();
|
|
147
|
+
persistDeviceId(DEVICE_ID);
|
|
148
|
+
return DEVICE_ID;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function getCommonHeaders(): Record<string, string> {
|
|
152
|
+
const headers = {
|
|
153
|
+
"User-Agent": KIMI_CLI_USER_AGENT,
|
|
154
|
+
"X-Msh-Platform": KIMI_PLATFORM,
|
|
155
|
+
"X-Msh-Version": KIMI_CLI_VERSION,
|
|
156
|
+
"X-Msh-Device-Name": os.hostname(),
|
|
157
|
+
"X-Msh-Device-Model": DEVICE_MODEL,
|
|
158
|
+
"X-Msh-Os-Version": getOsVersion(),
|
|
159
|
+
"X-Msh-Device-Id": getStableDeviceId(),
|
|
160
|
+
};
|
|
161
|
+
return Object.fromEntries(
|
|
162
|
+
Object.entries(headers).map(([key, value]) => [key, asciiHeaderValue(value)]),
|
|
163
|
+
) as Record<string, string>;
|
|
164
|
+
}
|
package/src/models.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Model identity: discovery against the server's /v1/models endpoint, plus the
|
|
2
|
+
// extras-merging helpers used by both the OAuth modifyModels hook and the
|
|
3
|
+
// `KIMI_MODEL_*` env-override path.
|
|
4
|
+
|
|
5
|
+
import type { Api, Model, OAuthCredentials } from "@earendil-works/pi-ai";
|
|
6
|
+
|
|
7
|
+
import { getBaseUrl } from "./constants.ts";
|
|
8
|
+
import { getCommonHeaders } from "./device.ts";
|
|
9
|
+
|
|
10
|
+
export interface KimiOAuthExtras {
|
|
11
|
+
wireModelId?: string;
|
|
12
|
+
modelDisplay?: string;
|
|
13
|
+
contextLength?: number;
|
|
14
|
+
supportsReasoning?: boolean;
|
|
15
|
+
supportsImageIn?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type KimiOAuthCredentials = OAuthCredentials & KimiOAuthExtras;
|
|
19
|
+
|
|
20
|
+
interface KimiServerModel {
|
|
21
|
+
id?: unknown;
|
|
22
|
+
display_name?: unknown;
|
|
23
|
+
context_length?: unknown;
|
|
24
|
+
supports_reasoning?: unknown;
|
|
25
|
+
supports_image_in?: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildModelsUrl(baseUrl: string): string {
|
|
29
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
30
|
+
return base.endsWith("/v1") ? `${base}/models` : `${base}/v1/models`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getModelsUrl(): string {
|
|
34
|
+
return buildModelsUrl(getBaseUrl());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function discoverKimiModelMetadata(accessToken: string): Promise<KimiOAuthExtras> {
|
|
38
|
+
if (!accessToken) return {};
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(getModelsUrl(), {
|
|
41
|
+
headers: {
|
|
42
|
+
...getCommonHeaders(),
|
|
43
|
+
Authorization: `Bearer ${accessToken}`,
|
|
44
|
+
Accept: "application/json",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) return {};
|
|
48
|
+
const json = (await response.json()) as { data?: unknown };
|
|
49
|
+
const list = Array.isArray(json.data) ? (json.data as KimiServerModel[]) : [];
|
|
50
|
+
const preferred =
|
|
51
|
+
list.find((m) => typeof m.id === "string" && m.id === "kimi-for-coding") ?? list[0];
|
|
52
|
+
if (!preferred || typeof preferred.id !== "string") return {};
|
|
53
|
+
const extras: KimiOAuthExtras = { wireModelId: preferred.id };
|
|
54
|
+
if (typeof preferred.display_name === "string") extras.modelDisplay = preferred.display_name;
|
|
55
|
+
if (typeof preferred.context_length === "number" && preferred.context_length > 0) {
|
|
56
|
+
extras.contextLength = preferred.context_length;
|
|
57
|
+
}
|
|
58
|
+
if (typeof preferred.supports_reasoning === "boolean") {
|
|
59
|
+
extras.supportsReasoning = preferred.supports_reasoning;
|
|
60
|
+
}
|
|
61
|
+
if (typeof preferred.supports_image_in === "boolean") {
|
|
62
|
+
extras.supportsImageIn = preferred.supports_image_in;
|
|
63
|
+
}
|
|
64
|
+
return extras;
|
|
65
|
+
} catch {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function applyKimiOAuthExtrasToModel(
|
|
71
|
+
model: Model<Api>,
|
|
72
|
+
extras: KimiOAuthExtras,
|
|
73
|
+
): Model<Api> {
|
|
74
|
+
const next: Model<Api> & { wireModelId?: string } = { ...model };
|
|
75
|
+
if (typeof extras.modelDisplay === "string" && extras.modelDisplay) {
|
|
76
|
+
next.name = extras.modelDisplay;
|
|
77
|
+
}
|
|
78
|
+
if (typeof extras.contextLength === "number" && extras.contextLength > 0) {
|
|
79
|
+
next.contextWindow = extras.contextLength;
|
|
80
|
+
}
|
|
81
|
+
if (typeof extras.wireModelId === "string" && extras.wireModelId) {
|
|
82
|
+
next.wireModelId = extras.wireModelId;
|
|
83
|
+
}
|
|
84
|
+
if (typeof extras.supportsReasoning === "boolean") {
|
|
85
|
+
next.reasoning = extras.supportsReasoning;
|
|
86
|
+
}
|
|
87
|
+
if (typeof extras.supportsImageIn === "boolean") {
|
|
88
|
+
const input = ["text"];
|
|
89
|
+
if (extras.supportsImageIn) input.push("image");
|
|
90
|
+
(next as unknown as { input: string[] }).input = input;
|
|
91
|
+
}
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseKimiModelCapabilities(value: string | undefined): KimiOAuthExtras | null {
|
|
96
|
+
if (!value) return null;
|
|
97
|
+
const caps = new Set(
|
|
98
|
+
value
|
|
99
|
+
.split(",")
|
|
100
|
+
.map((cap) => cap.trim().toLowerCase())
|
|
101
|
+
.filter(Boolean),
|
|
102
|
+
);
|
|
103
|
+
return {
|
|
104
|
+
supportsReasoning: caps.has("thinking") || caps.has("always_thinking"),
|
|
105
|
+
supportsImageIn: caps.has("image_in"),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function applyKimiEnvOverridesToModel(model: Model<Api>): Model<Api> {
|
|
110
|
+
const extras: KimiOAuthExtras = {};
|
|
111
|
+
const modelName = process.env.KIMI_MODEL_NAME?.trim();
|
|
112
|
+
if (modelName) {
|
|
113
|
+
extras.wireModelId = modelName;
|
|
114
|
+
extras.modelDisplay = modelName;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const maxContextSize = process.env.KIMI_MODEL_MAX_CONTEXT_SIZE?.trim();
|
|
118
|
+
if (maxContextSize) {
|
|
119
|
+
const parsed = parseInt(maxContextSize, 10);
|
|
120
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
121
|
+
extras.contextLength = parsed;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const capabilities = parseKimiModelCapabilities(process.env.KIMI_MODEL_CAPABILITIES);
|
|
126
|
+
if (capabilities) Object.assign(extras, capabilities);
|
|
127
|
+
|
|
128
|
+
return applyKimiOAuthExtrasToModel(model, extras);
|
|
129
|
+
}
|