pi-provider-kimi-code 0.6.7 → 0.6.9
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 +46 -109
- package/index.ts +231 -87
- package/package.json +5 -5
- package/src/constants.ts +6 -2
- package/src/device.ts +59 -17
- package/src/models.ts +147 -62
- package/src/oauth.ts +232 -68
- package/src/payload.ts +154 -40
- package/src/stream.ts +32 -4
- package/src/usage.ts +4 -42
package/index.ts
CHANGED
|
@@ -27,6 +27,9 @@ import {
|
|
|
27
27
|
import { Input, SettingsList, type SettingItem, truncateToWidth } from "@earendil-works/pi-tui";
|
|
28
28
|
import os from "node:os";
|
|
29
29
|
|
|
30
|
+
// pi-ai <=0.79 has no "./compat"
|
|
31
|
+
const piAiCompatModule = "@earendil-works/pi-ai/compat";
|
|
32
|
+
|
|
30
33
|
import {
|
|
31
34
|
type KimiCodeConfig,
|
|
32
35
|
type KimiCodeConfigPatch,
|
|
@@ -38,11 +41,16 @@ import {
|
|
|
38
41
|
saveProjectKimiCodeConfig,
|
|
39
42
|
type KimiToolName,
|
|
40
43
|
} from "./src/config.ts";
|
|
41
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
KIMI_GLOBAL_FALLBACK_SOURCE_ID,
|
|
46
|
+
PROVIDER_ID,
|
|
47
|
+
PROVIDER_VERSION,
|
|
48
|
+
getBaseUrl,
|
|
49
|
+
getKimiApiType,
|
|
50
|
+
} from "./src/constants.ts";
|
|
42
51
|
import {
|
|
43
52
|
type KimiOAuthCredentials,
|
|
44
53
|
type KimiOAuthExtras,
|
|
45
|
-
applyKimiMembershipLimitsToModel,
|
|
46
54
|
buildKimiModelFromConfig,
|
|
47
55
|
applyKimiOAuthExtrasToModel,
|
|
48
56
|
KIMI_CODING_HIGHSPEED_MODEL_ID,
|
|
@@ -51,10 +59,15 @@ import {
|
|
|
51
59
|
KIMI_MODEL_CATALOG_VERSION,
|
|
52
60
|
discoverKimiModelMetadata,
|
|
53
61
|
getKimiModelMetadata,
|
|
54
|
-
|
|
62
|
+
hasKimiModelMetadata,
|
|
55
63
|
resolveKimiModelConfig,
|
|
56
64
|
} from "./src/models.ts";
|
|
57
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
getKimiApiKey,
|
|
67
|
+
loginKimiCode,
|
|
68
|
+
refreshKimiAuthToken,
|
|
69
|
+
refreshKimiCodeToken,
|
|
70
|
+
} from "./src/oauth.ts";
|
|
58
71
|
import { isKimiProjectConfigApproved } from "./src/project-trust.ts";
|
|
59
72
|
import {
|
|
60
73
|
type KimiConfigScope,
|
|
@@ -70,8 +83,8 @@ import { buildKimiDatasourceTool } from "./src/tools/datasource.ts";
|
|
|
70
83
|
interface KimiRuntimeState {
|
|
71
84
|
cwd: string;
|
|
72
85
|
config: KimiCodeConfig;
|
|
86
|
+
fallbackSourceId: string;
|
|
73
87
|
modelExtras: KimiOAuthExtras;
|
|
74
|
-
membershipLevel: string | null;
|
|
75
88
|
projectTrusted: boolean;
|
|
76
89
|
overrides?: KimiCodeConfigPatch;
|
|
77
90
|
}
|
|
@@ -143,15 +156,30 @@ function applyEffectiveKimiRuntimeConfig(
|
|
|
143
156
|
return config;
|
|
144
157
|
}
|
|
145
158
|
|
|
146
|
-
async function refreshModelExtras(
|
|
147
|
-
|
|
159
|
+
async function refreshModelExtras(
|
|
160
|
+
state: KimiRuntimeState,
|
|
161
|
+
token = getKimiUsageToken(),
|
|
162
|
+
): Promise<boolean> {
|
|
148
163
|
if (!token) return false;
|
|
149
|
-
const extras = await discoverKimiModelMetadata(token, state.config.protocol
|
|
164
|
+
const extras = await discoverKimiModelMetadata(token, state.config.protocol, {
|
|
165
|
+
refreshAccessToken: refreshKimiAuthToken,
|
|
166
|
+
});
|
|
150
167
|
if (Object.keys(extras).length === 0) return false;
|
|
151
168
|
Object.assign(state.modelExtras, extras);
|
|
152
169
|
return true;
|
|
153
170
|
}
|
|
154
171
|
|
|
172
|
+
// Discovery can lose a race with a concurrent OAuth refresh: the token it
|
|
173
|
+
// started with may already be dead by the time the request goes out. Retry
|
|
174
|
+
// once, but only when the stored token actually changed underneath us.
|
|
175
|
+
async function discoverModelExtras(state: KimiRuntimeState): Promise<boolean> {
|
|
176
|
+
const token = getKimiUsageToken();
|
|
177
|
+
if (await refreshModelExtras(state, token)) return true;
|
|
178
|
+
const refreshedToken = getKimiUsageToken();
|
|
179
|
+
if (!refreshedToken || refreshedToken === token) return false;
|
|
180
|
+
return refreshModelExtras(state, refreshedToken);
|
|
181
|
+
}
|
|
182
|
+
|
|
155
183
|
async function openSettingsMenu(
|
|
156
184
|
pi: ExtensionAPI,
|
|
157
185
|
ctx: ExtensionCommandContext,
|
|
@@ -167,10 +195,7 @@ async function openSettingsMenu(
|
|
|
167
195
|
if (!modelsRefreshed && refreshedToken && refreshedToken !== modelDiscoveryToken) {
|
|
168
196
|
modelsRefreshed = await refreshModelExtras(state);
|
|
169
197
|
}
|
|
170
|
-
|
|
171
|
-
const membershipChanged = state.membershipLevel !== refreshedMembershipLevel;
|
|
172
|
-
state.membershipLevel = refreshedMembershipLevel;
|
|
173
|
-
if (modelsRefreshed || membershipChanged) registerKimiProvider(pi, state);
|
|
198
|
+
if (modelsRefreshed) await registerKimiProvider(pi, state);
|
|
174
199
|
const usage = usageSnapshot.summary;
|
|
175
200
|
|
|
176
201
|
const projectTrusted = await isKimiProjectConfigApproved(ctx, ctx.cwd);
|
|
@@ -382,74 +407,125 @@ function saveScopeKimiCodeConfig(
|
|
|
382
407
|
function filterAvailableKimiModels<T extends { id: string }>(
|
|
383
408
|
models: T[],
|
|
384
409
|
extras: KimiOAuthExtras,
|
|
385
|
-
membershipLevel: string | null,
|
|
386
410
|
): T[] {
|
|
387
411
|
const available =
|
|
388
412
|
extras.modelCatalogVersion === KIMI_MODEL_CATALOG_VERSION && extras.modelCatalog
|
|
389
413
|
? new Set(Object.keys(extras.modelCatalog))
|
|
390
414
|
: null;
|
|
391
|
-
return models.filter(
|
|
392
|
-
(model) =>
|
|
393
|
-
(!available || available.has(model.id)) &&
|
|
394
|
-
isKimiModelAvailableForMembership(model.id, membershipLevel) !== false,
|
|
395
|
-
);
|
|
415
|
+
return models.filter((model) => !available || available.has(model.id));
|
|
396
416
|
}
|
|
397
417
|
|
|
398
|
-
function
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
applyKimiOAuthExtrasToModel(
|
|
408
|
-
buildKimiModelFromConfig(state.config.model, KIMI_CODING_HIGHSPEED_MODEL_ID),
|
|
409
|
-
getKimiModelMetadata(state.modelExtras, KIMI_CODING_HIGHSPEED_MODEL_ID),
|
|
410
|
-
),
|
|
411
|
-
state.membershipLevel,
|
|
412
|
-
);
|
|
413
|
-
const k3Model = applyKimiMembershipLimitsToModel(
|
|
418
|
+
function catalogModelIds(extras: KimiOAuthExtras): string[] {
|
|
419
|
+
if (extras.modelCatalogVersion === KIMI_MODEL_CATALOG_VERSION && extras.modelCatalog) {
|
|
420
|
+
return Object.keys(extras.modelCatalog);
|
|
421
|
+
}
|
|
422
|
+
return [KIMI_CODING_MODEL_ID, KIMI_CODING_HIGHSPEED_MODEL_ID, KIMI_K3_MODEL_ID];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function buildKimiCatalogModels(state: KimiRuntimeState) {
|
|
426
|
+
return catalogModelIds(state.modelExtras).map((modelId) =>
|
|
414
427
|
applyKimiOAuthExtrasToModel(
|
|
415
|
-
buildKimiModelFromConfig(state.config.model,
|
|
416
|
-
getKimiModelMetadata(state.modelExtras,
|
|
428
|
+
buildKimiModelFromConfig(state.config.model, modelId),
|
|
429
|
+
getKimiModelMetadata(state.modelExtras, modelId),
|
|
430
|
+
state.config.model.reasoningMap,
|
|
417
431
|
),
|
|
418
|
-
state.membershipLevel,
|
|
419
432
|
);
|
|
433
|
+
}
|
|
420
434
|
|
|
435
|
+
// Node reports a missing subpath export (pi-ai <=0.79) and a missing package
|
|
436
|
+
// with these codes. Both mean "this pi-ai has no compat layer", which is an
|
|
437
|
+
// expected configuration rather than a failure worth reporting: the fallback
|
|
438
|
+
// simply does not exist there.
|
|
439
|
+
const MODULE_UNAVAILABLE_CODES = new Set(["ERR_MODULE_NOT_FOUND", "ERR_PACKAGE_PATH_NOT_EXPORTED"]);
|
|
440
|
+
|
|
441
|
+
// Every api id this extension can hand out, not only the one for the currently
|
|
442
|
+
// configured protocol: sessions persist the api id that was current when the
|
|
443
|
+
// model was resolved, so a session created before a protocol switch still
|
|
444
|
+
// carries the other id and would keep crashing. streamSimpleKimi routes on the
|
|
445
|
+
// model's wireProtocol / the resolved runtime config and never on model.api, so
|
|
446
|
+
// both ids dispatch identically.
|
|
447
|
+
const KIMI_FALLBACK_APIS = [getKimiApiType("openai"), getKimiApiType("anthropic")];
|
|
448
|
+
|
|
449
|
+
type PiAiApiRegistry = {
|
|
450
|
+
registerApiProvider: (
|
|
451
|
+
provider: {
|
|
452
|
+
api: string;
|
|
453
|
+
stream: typeof streamSimpleKimi;
|
|
454
|
+
streamSimple: typeof streamSimpleKimi;
|
|
455
|
+
},
|
|
456
|
+
sourceId: string,
|
|
457
|
+
) => void;
|
|
458
|
+
unregisterApiProviders: (sourceId: string) => void;
|
|
459
|
+
getApiProvider: (api: string) => unknown;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
async function loadPiAiApiRegistry(action: string): Promise<PiAiApiRegistry | null> {
|
|
463
|
+
try {
|
|
464
|
+
return (await import(piAiCompatModule)) as PiAiApiRegistry;
|
|
465
|
+
} catch (error: unknown) {
|
|
466
|
+
const code = (error as { code?: unknown } | undefined)?.code;
|
|
467
|
+
if (typeof code === "string" && MODULE_UNAVAILABLE_CODES.has(code)) return null;
|
|
468
|
+
console.error(
|
|
469
|
+
`[pi-provider-kimi-code] failed to ${action} the global api fallback:`,
|
|
470
|
+
error instanceof Error ? error.message : error,
|
|
471
|
+
);
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
let nextFallbackSourceId = 0;
|
|
477
|
+
const liveFallbackSources = new Set<string>();
|
|
478
|
+
|
|
479
|
+
function createKimiGlobalFallbackSourceId(): string {
|
|
480
|
+
nextFallbackSourceId += 1;
|
|
481
|
+
return `${KIMI_GLOBAL_FALLBACK_SOURCE_ID}:${nextFallbackSourceId}`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function registerKimiGlobalApiFallback(sourceId: string): Promise<void> {
|
|
485
|
+
if (!liveFallbackSources.has(sourceId)) return;
|
|
486
|
+
const registry = await loadPiAiApiRegistry("register");
|
|
487
|
+
if (!registry) return;
|
|
488
|
+
for (const api of KIMI_FALLBACK_APIS) {
|
|
489
|
+
// Never displace an entry someone else owns: this is a shared,
|
|
490
|
+
// process-wide table and the fallback only exists to fill a hole, not to
|
|
491
|
+
// win a conflict. pi-ai installs its own builtins under the same rule.
|
|
492
|
+
if (registry.getApiProvider(api)) continue;
|
|
493
|
+
registry.registerApiProvider(
|
|
494
|
+
{ api, stream: streamSimpleKimi, streamSimple: streamSimpleKimi },
|
|
495
|
+
sourceId,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// The registry outlives each session. Remove only the entries owned by the
|
|
501
|
+
// retiring runtime, then hand any holes to another live runtime.
|
|
502
|
+
async function unregisterKimiGlobalApiFallback(sourceId: string): Promise<void> {
|
|
503
|
+
if (!liveFallbackSources.delete(sourceId)) return;
|
|
504
|
+
const registry = await loadPiAiApiRegistry("unregister");
|
|
505
|
+
if (!registry) return;
|
|
506
|
+
registry.unregisterApiProviders(sourceId);
|
|
507
|
+
const successor = liveFallbackSources.values().next().value;
|
|
508
|
+
if (successor) await registerKimiGlobalApiFallback(successor);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function registerKimiProvider(pi: ExtensionAPI, state: KimiRuntimeState): Promise<void> {
|
|
512
|
+
await registerKimiGlobalApiFallback(state.fallbackSourceId);
|
|
421
513
|
pi.registerProvider(PROVIDER_ID, {
|
|
422
514
|
baseUrl: getBaseUrl(state.config.protocol),
|
|
423
515
|
apiKey: "$KIMI_API_KEY",
|
|
424
516
|
api: getKimiApiType(state.config.protocol),
|
|
425
517
|
streamSimple: streamSimpleKimi,
|
|
426
518
|
|
|
427
|
-
models: filterAvailableKimiModels(
|
|
428
|
-
[standardModel, highSpeedModel, k3Model],
|
|
429
|
-
state.modelExtras,
|
|
430
|
-
state.membershipLevel,
|
|
431
|
-
),
|
|
519
|
+
models: filterAvailableKimiModels(buildKimiCatalogModels(state), state.modelExtras),
|
|
432
520
|
|
|
433
521
|
oauth: {
|
|
434
522
|
name: "Kimi Code (OAuth)",
|
|
435
523
|
login: async (callbacks) => {
|
|
436
524
|
const credentials = await loginKimiCode(callbacks);
|
|
437
|
-
const usage = await fetchKimiUsageSnapshot({
|
|
438
|
-
timeoutMs: 2500,
|
|
439
|
-
token: credentials.access,
|
|
440
|
-
refreshOnUnauthorized: false,
|
|
441
|
-
});
|
|
442
|
-
state.membershipLevel = usage.membershipLevel;
|
|
443
525
|
return credentials;
|
|
444
526
|
},
|
|
445
527
|
refreshToken: async (credentials) => {
|
|
446
528
|
const refreshed = await refreshKimiCodeToken(credentials);
|
|
447
|
-
const usage = await fetchKimiUsageSnapshot({
|
|
448
|
-
timeoutMs: 2500,
|
|
449
|
-
token: refreshed.access,
|
|
450
|
-
refreshOnUnauthorized: false,
|
|
451
|
-
});
|
|
452
|
-
state.membershipLevel = usage.membershipLevel;
|
|
453
529
|
return refreshed;
|
|
454
530
|
},
|
|
455
531
|
getApiKey: getKimiApiKey,
|
|
@@ -460,32 +536,102 @@ function registerKimiProvider(pi: ExtensionAPI, state: KimiRuntimeState): void {
|
|
|
460
536
|
// request payload by streamSimpleKimi.
|
|
461
537
|
modifyModels: (models, cred) => {
|
|
462
538
|
const extras = cred as KimiOAuthCredentials;
|
|
463
|
-
|
|
539
|
+
// Retain previously discovered extras when this credential carries
|
|
540
|
+
// none (legacy format or failed discovery): applying empty metadata
|
|
541
|
+
// would strip thinking levels the startup discovery already found.
|
|
542
|
+
if (hasKimiModelMetadata(extras)) state.modelExtras = extras;
|
|
464
543
|
reloadEffectiveKimiRuntimeConfig(state, state.cwd, state.projectTrusted);
|
|
465
544
|
const available =
|
|
466
545
|
extras.modelCatalogVersion === KIMI_MODEL_CATALOG_VERSION && extras.modelCatalog
|
|
467
546
|
? new Set(Object.keys(extras.modelCatalog))
|
|
468
547
|
: null;
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
(model) =>
|
|
472
|
-
|
|
473
|
-
((
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
state.
|
|
481
|
-
)
|
|
482
|
-
|
|
548
|
+
if (!available) {
|
|
549
|
+
return [
|
|
550
|
+
...models.filter((model) => model.provider !== PROVIDER_ID),
|
|
551
|
+
...models
|
|
552
|
+
.filter((model) => model.provider === PROVIDER_ID)
|
|
553
|
+
.map((model) => {
|
|
554
|
+
const metadata = getKimiModelMetadata(extras, model.id);
|
|
555
|
+
if (!hasKimiModelMetadata(metadata)) return model;
|
|
556
|
+
return applyKimiOAuthExtrasToModel(
|
|
557
|
+
model,
|
|
558
|
+
metadata,
|
|
559
|
+
state.config.model.reasoningMap,
|
|
560
|
+
);
|
|
561
|
+
}),
|
|
562
|
+
];
|
|
563
|
+
}
|
|
564
|
+
const catalogModels = catalogModelIds(extras).map((modelId) => {
|
|
565
|
+
const existing = models.find(
|
|
566
|
+
(model) => model.provider === PROVIDER_ID && model.id === modelId,
|
|
483
567
|
);
|
|
568
|
+
return applyKimiOAuthExtrasToModel(
|
|
569
|
+
existing ?? {
|
|
570
|
+
...buildKimiModelFromConfig(state.config.model, modelId),
|
|
571
|
+
api: getKimiApiType(state.config.protocol),
|
|
572
|
+
provider: PROVIDER_ID,
|
|
573
|
+
baseUrl: getBaseUrl(state.config.protocol),
|
|
574
|
+
},
|
|
575
|
+
getKimiModelMetadata(extras, modelId),
|
|
576
|
+
state.config.model.reasoningMap,
|
|
577
|
+
);
|
|
578
|
+
});
|
|
579
|
+
let insertedCatalog = false;
|
|
580
|
+
return models.flatMap((model) => {
|
|
581
|
+
if (model.provider !== PROVIDER_ID) return [model];
|
|
582
|
+
if (insertedCatalog) return [];
|
|
583
|
+
insertedCatalog = true;
|
|
584
|
+
return catalogModels;
|
|
585
|
+
});
|
|
484
586
|
},
|
|
485
587
|
},
|
|
486
588
|
});
|
|
487
589
|
}
|
|
488
590
|
|
|
591
|
+
// What pi throws from any `pi` method once the extension runtime behind it has
|
|
592
|
+
// been retired, which is what a /reload does. Same wording on every version
|
|
593
|
+
// this extension supports (0.78.1 through 0.82.0); if it ever changes, the
|
|
594
|
+
// worst case is that a reload race gets logged instead of ignored.
|
|
595
|
+
const STALE_EXTENSION_RUNTIME_PREFIX = "This extension ctx is stale";
|
|
596
|
+
|
|
597
|
+
let pendingModelDiscovery: Promise<void> = Promise.resolve();
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Resolves once the discovery started by the most recent extension load has
|
|
601
|
+
* settled. Nothing in the runtime waits for it; it exists so tests can observe
|
|
602
|
+
* the deferred registration without polling.
|
|
603
|
+
*/
|
|
604
|
+
export function kimiModelDiscoverySettled(): Promise<void> {
|
|
605
|
+
return pendingModelDiscovery;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// pi's extension loader awaits this factory before it builds the session, and
|
|
609
|
+
// the TUI only draws its first frame after that returns, so a network round
|
|
610
|
+
// trip here is a stall the user sees on every launch and every /reload. The
|
|
611
|
+
// stored OAuth credential already carries the last known catalog (pi replays it
|
|
612
|
+
// through oauth.modifyModels), so register what the config describes right away
|
|
613
|
+
// and fold the fresh metadata in when it lands.
|
|
614
|
+
function startModelDiscovery(pi: ExtensionAPI, state: KimiRuntimeState): void {
|
|
615
|
+
pendingModelDiscovery = discoverModelExtras(state)
|
|
616
|
+
.then((discovered) => {
|
|
617
|
+
if (!discovered) return;
|
|
618
|
+
reloadEffectiveKimiRuntimeConfig(state, state.cwd, state.projectTrusted);
|
|
619
|
+
return registerKimiProvider(pi, state);
|
|
620
|
+
})
|
|
621
|
+
.catch((error: unknown) => {
|
|
622
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
623
|
+
// A /reload retires this extension runtime while discovery may still be
|
|
624
|
+
// in flight, and registering against a retired runtime throws. The
|
|
625
|
+
// reloaded factory runs its own discovery, so there is nothing to redo.
|
|
626
|
+
if (message.startsWith(STALE_EXTENSION_RUNTIME_PREFIX)) return;
|
|
627
|
+
// Anything else used to surface as an extension load failure, back when
|
|
628
|
+
// this ran inside the factory. Deferring it must not make it invisible:
|
|
629
|
+
// the provider stays registered with the metadata it already had, and
|
|
630
|
+
// the user needs to know why it stopped refreshing.
|
|
631
|
+
console.error("[kimi-coding] deferred model discovery failed:", message);
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
489
635
|
export function KimiCode(overrides?: KimiCodeConfigPatch): ExtensionFactory {
|
|
490
636
|
return async (pi: ExtensionAPI) => {
|
|
491
637
|
const cwd = process.cwd();
|
|
@@ -493,30 +639,22 @@ export function KimiCode(overrides?: KimiCodeConfigPatch): ExtensionFactory {
|
|
|
493
639
|
{ cwd, home: os.homedir(), includeProject: false },
|
|
494
640
|
overrides,
|
|
495
641
|
);
|
|
496
|
-
const discoveryToken = getKimiUsageToken();
|
|
497
|
-
const [initialDiscovery, usageSnapshot] = await Promise.all([
|
|
498
|
-
discoveryToken ? discoverKimiModelMetadata(discoveryToken, config.protocol) : {},
|
|
499
|
-
fetchKimiUsageSnapshot({ timeoutMs: 2500 }),
|
|
500
|
-
]);
|
|
501
|
-
let discovered = initialDiscovery;
|
|
502
|
-
const refreshedToken = getKimiUsageToken();
|
|
503
|
-
if (
|
|
504
|
-
Object.keys(discovered).length === 0 &&
|
|
505
|
-
refreshedToken &&
|
|
506
|
-
refreshedToken !== discoveryToken
|
|
507
|
-
) {
|
|
508
|
-
discovered = await discoverKimiModelMetadata(refreshedToken, config.protocol);
|
|
509
|
-
}
|
|
510
642
|
const state: KimiRuntimeState = {
|
|
511
643
|
cwd,
|
|
512
644
|
config,
|
|
513
|
-
|
|
514
|
-
|
|
645
|
+
fallbackSourceId: createKimiGlobalFallbackSourceId(),
|
|
646
|
+
modelExtras: {},
|
|
515
647
|
projectTrusted: false,
|
|
516
648
|
overrides,
|
|
517
649
|
};
|
|
518
650
|
reloadEffectiveKimiRuntimeConfig(state, cwd, false);
|
|
519
|
-
|
|
651
|
+
liveFallbackSources.add(state.fallbackSourceId);
|
|
652
|
+
try {
|
|
653
|
+
await registerKimiProvider(pi, state);
|
|
654
|
+
} catch (error) {
|
|
655
|
+
liveFallbackSources.delete(state.fallbackSourceId);
|
|
656
|
+
throw error;
|
|
657
|
+
}
|
|
520
658
|
|
|
521
659
|
registerConfiguredMoonshotTools(pi, state.config, { updateActiveTools: false });
|
|
522
660
|
|
|
@@ -526,7 +664,11 @@ export function KimiCode(overrides?: KimiCodeConfigPatch): ExtensionFactory {
|
|
|
526
664
|
updateActiveTools: true,
|
|
527
665
|
projectTrusted,
|
|
528
666
|
});
|
|
529
|
-
registerKimiProvider(pi, state);
|
|
667
|
+
await registerKimiProvider(pi, state);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
pi.on("session_shutdown", async () => {
|
|
671
|
+
await unregisterKimiGlobalApiFallback(state.fallbackSourceId);
|
|
530
672
|
});
|
|
531
673
|
|
|
532
674
|
pi.registerCommand("kimi-settings", {
|
|
@@ -539,6 +681,8 @@ export function KimiCode(overrides?: KimiCodeConfigPatch): ExtensionFactory {
|
|
|
539
681
|
await openSettingsMenu(pi, ctx, state);
|
|
540
682
|
},
|
|
541
683
|
});
|
|
684
|
+
|
|
685
|
+
startModelDiscovery(pi, state);
|
|
542
686
|
};
|
|
543
687
|
}
|
|
544
688
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-provider-kimi-code",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.9",
|
|
4
|
+
"description": "Kimi Code harness extension for Pi: shared login state, file uploads, and Kimi-native tools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-provider",
|
|
7
7
|
"coding-agent",
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"proper-lockfile": "^4.1.2"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@earendil-works/pi-ai": "^0.
|
|
57
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
58
|
-
"@earendil-works/pi-tui": "^0.
|
|
56
|
+
"@earendil-works/pi-ai": "^0.82.0",
|
|
57
|
+
"@earendil-works/pi-coding-agent": "^0.82.0",
|
|
58
|
+
"@earendil-works/pi-tui": "^0.82.0",
|
|
59
59
|
"@j178/prek": "^0.4.4",
|
|
60
60
|
"@types/node": "^24.0.0",
|
|
61
61
|
"@types/proper-lockfile": "^4.1.4",
|
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.9";
|
|
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/MoonshotAI/kimi-code.
|
|
49
|
-
export const KIMI_UPSTREAM_VERSION = "0.
|
|
49
|
+
export const KIMI_UPSTREAM_VERSION = "0.29.1";
|
|
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
|
|
|
@@ -63,6 +63,10 @@ export const RETRYABLE_REFRESH_STATUSES = new Set([429, 500, 502, 503, 504]);
|
|
|
63
63
|
|
|
64
64
|
export const PROVIDER_ID = "kimi-coding";
|
|
65
65
|
|
|
66
|
+
// Owner tag for the entries this extension adds to pi-ai's global api-provider
|
|
67
|
+
// registry, so they can be identified and removed as a group.
|
|
68
|
+
export const KIMI_GLOBAL_FALLBACK_SOURCE_ID = "pi-provider-kimi-code-global-fallback";
|
|
69
|
+
|
|
66
70
|
export function getOAuthHost(): string {
|
|
67
71
|
const value =
|
|
68
72
|
process.env.KIMI_CODE_OAUTH_HOST || process.env.KIMI_OAUTH_HOST || DEFAULT_OAUTH_HOST;
|
package/src/device.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Device identification: device-id persistence, Kimi Code-compatible request
|
|
2
2
|
// headers, and the cross-platform device-model string. Pure helpers live near
|
|
3
|
-
// the side-effecting one-shots (file IO
|
|
3
|
+
// the side-effecting one-shots (file IO) they support.
|
|
4
4
|
|
|
5
|
-
import { execFileSync } from "node:child_process";
|
|
6
5
|
import { randomBytes } from "node:crypto";
|
|
7
6
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
8
|
import os from "node:os";
|
|
9
9
|
import { dirname } from "node:path";
|
|
10
10
|
|
|
@@ -97,25 +97,68 @@ export function computeDeviceModel(input: DeviceModelInput): string {
|
|
|
97
97
|
return `${system} ${arch}`.trim();
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
100
|
+
// Where macOS keeps its product version. Upstream Kimi Code reads it through
|
|
101
|
+
// Python's platform.mac_ver(), which parses this same file, so it is the same
|
|
102
|
+
// source of truth as `sw_vers -productVersion` without the process spawn: a
|
|
103
|
+
// spawn costs ~10ms of blocked event loop, and pi pays it again on every
|
|
104
|
+
// /reload because it re-evaluates extension modules.
|
|
105
|
+
const MAC_PRODUCT_VERSION_PATH = "/System/Library/CoreServices/SystemVersion.plist";
|
|
106
|
+
|
|
107
|
+
// Exposed for tests. The plist is a flat XML property list, so the version sits
|
|
108
|
+
// in the <string> immediately after the ProductVersion key.
|
|
109
|
+
export function parseMacProductVersion(plist: string): string | undefined {
|
|
110
|
+
const match = /<key>ProductVersion<\/key>\s*<string>([^<]*)<\/string>/.exec(plist);
|
|
111
|
+
return match?.[1].trim() || undefined;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
let DEVICE_MODEL: string | undefined;
|
|
115
|
+
|
|
116
|
+
function setDeviceModel(macVersion: string | undefined): string {
|
|
117
|
+
DEVICE_MODEL ??= computeDeviceModel({
|
|
114
118
|
platform: process.platform,
|
|
115
119
|
release: os.release(),
|
|
116
120
|
arch: os.machine() || process.arch,
|
|
117
|
-
macVersion
|
|
121
|
+
macVersion,
|
|
118
122
|
});
|
|
123
|
+
return DEVICE_MODEL;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function loadMacDeviceModel(): Promise<void> {
|
|
127
|
+
try {
|
|
128
|
+
setDeviceModel(parseMacProductVersion(await readFile(MAC_PRODUCT_VERSION_PATH, "utf-8")));
|
|
129
|
+
} catch {
|
|
130
|
+
setDeviceModel(undefined);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Settles once the module-load lookup has produced a device model. Nothing in
|
|
136
|
+
* the provider waits for it — headers stay synchronous — but tests use it to
|
|
137
|
+
* exercise the async path without racing the fallback below.
|
|
138
|
+
*/
|
|
139
|
+
export const deviceModelReady: Promise<void> = (() => {
|
|
140
|
+
if (process.platform !== "darwin") {
|
|
141
|
+
// Nothing to read: every other platform derives the model from os.release().
|
|
142
|
+
setDeviceModel(undefined);
|
|
143
|
+
return Promise.resolve();
|
|
144
|
+
}
|
|
145
|
+
// Read the version off the event loop at module load so the value is usually
|
|
146
|
+
// in place before anything asks for a header.
|
|
147
|
+
return loadMacDeviceModel();
|
|
148
|
+
})();
|
|
149
|
+
|
|
150
|
+
function getDeviceModel(): string {
|
|
151
|
+
if (DEVICE_MODEL !== undefined) return DEVICE_MODEL;
|
|
152
|
+
// A header was needed before the async read landed. Read the same file
|
|
153
|
+
// synchronously rather than reporting the kernel release, which would put a
|
|
154
|
+
// different device model on the wire than every later request. This costs
|
|
155
|
+
// ~0.02ms against the ~10ms a `sw_vers` subprocess used to cost, and both
|
|
156
|
+
// paths memoize into the same slot.
|
|
157
|
+
try {
|
|
158
|
+
return setDeviceModel(parseMacProductVersion(readFileSync(MAC_PRODUCT_VERSION_PATH, "utf-8")));
|
|
159
|
+
} catch {
|
|
160
|
+
return setDeviceModel(undefined);
|
|
161
|
+
}
|
|
119
162
|
}
|
|
120
163
|
|
|
121
164
|
export function getOsVersion(): string {
|
|
@@ -133,7 +176,6 @@ export function asciiHeaderValue(value: string, fallback = "unknown"): string {
|
|
|
133
176
|
return sanitized || fallback;
|
|
134
177
|
}
|
|
135
178
|
|
|
136
|
-
const DEVICE_MODEL = getDeviceModel();
|
|
137
179
|
let DEVICE_ID: string | null = null;
|
|
138
180
|
|
|
139
181
|
function getStableDeviceId(): string {
|
|
@@ -174,7 +216,7 @@ export function getCommonHeaders(): Record<string, string> {
|
|
|
174
216
|
"X-Msh-Platform": KIMI_PLATFORM,
|
|
175
217
|
"X-Msh-Version": KIMI_UPSTREAM_VERSION,
|
|
176
218
|
"X-Msh-Device-Name": os.hostname(),
|
|
177
|
-
"X-Msh-Device-Model":
|
|
219
|
+
"X-Msh-Device-Model": getDeviceModel(),
|
|
178
220
|
"X-Msh-Os-Version": getOsVersion(),
|
|
179
221
|
"X-Msh-Device-Id": getStableDeviceId(),
|
|
180
222
|
};
|