pi-provider-kimi-code 0.3.0 → 0.3.1
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/index.ts +90 -28
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -40,6 +40,9 @@ const CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
|
|
|
40
40
|
const DEFAULT_OAUTH_HOST = "https://auth.kimi.com";
|
|
41
41
|
const PROTOCOL =
|
|
42
42
|
process.env.KIMI_CODE_PROTOCOL === "openai" ? "openai-completions" : "anthropic-messages";
|
|
43
|
+
// Use a custom api identifier so this provider never conflicts with the
|
|
44
|
+
// built-in "anthropic-messages" or "openai-completions" stream handlers.
|
|
45
|
+
const KIMI_API_TYPE = PROTOCOL === "openai-completions" ? "kimi-openai-completions" : "kimi-anthropic-messages";
|
|
43
46
|
const DEFAULT_BASE_URL =
|
|
44
47
|
PROTOCOL === "openai-completions"
|
|
45
48
|
? "https://api.kimi.com/coding/v1"
|
|
@@ -112,11 +115,32 @@ function getDeviceModel(): string {
|
|
|
112
115
|
return version && arch ? `macOS ${version} ${arch}` : `macOS ${arch}`;
|
|
113
116
|
}
|
|
114
117
|
if (platform === "win32") {
|
|
115
|
-
|
|
118
|
+
// Only show the major release (e.g. "Windows 10", "Windows 11") to match
|
|
119
|
+
// the upstream kimi-cli behavior in `def _device_model() -> str:`.
|
|
120
|
+
// See: https://github.com/MoonshotAI/kimi-cli/blob/main/src/kimi_cli/auth/oauth.py
|
|
121
|
+
const parts = os.release().split(".");
|
|
122
|
+
let release = parts[0];
|
|
123
|
+
if (release === "10" && parts.length >= 3) {
|
|
124
|
+
const build = parseInt(parts[2], 10);
|
|
125
|
+
if (!isNaN(build) && build >= 22000) {
|
|
126
|
+
release = "11";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
116
129
|
return release && arch ? `Windows ${release} ${arch}` : `Windows ${arch}`;
|
|
117
130
|
}
|
|
131
|
+
// Normalize platform name to match Python `platform.system()` casing used by
|
|
132
|
+
// upstream kimi-cli (e.g. "Linux", "FreeBSD", "SunOS"), since Node's
|
|
133
|
+
// `process.platform` is always lower-case.
|
|
134
|
+
const SYSTEM_NAME: Record<string, string> = {
|
|
135
|
+
aix: "AIX",
|
|
136
|
+
freebsd: "FreeBSD",
|
|
137
|
+
linux: "Linux",
|
|
138
|
+
openbsd: "OpenBSD",
|
|
139
|
+
sunos: "SunOS",
|
|
140
|
+
};
|
|
141
|
+
const system = SYSTEM_NAME[platform] ?? platform;
|
|
118
142
|
const release = os.release();
|
|
119
|
-
return release && arch ? `${
|
|
143
|
+
return release && arch ? `${system} ${release} ${arch}` : `${system} ${arch}`;
|
|
120
144
|
}
|
|
121
145
|
|
|
122
146
|
function asciiHeaderValue(value: string, fallback = "unknown"): string {
|
|
@@ -786,9 +810,12 @@ function streamSimpleKimi(
|
|
|
786
810
|
const upload: Uploader | undefined = apiKey
|
|
787
811
|
? (mimeType, data) => uploadKimiFile(apiKey, mimeType, data)
|
|
788
812
|
: undefined;
|
|
813
|
+
// Only forward apiKey if we actually have one — never override the
|
|
814
|
+
// caller's credential (e.g. Claude Code OAuth token) with an empty string.
|
|
815
|
+
const apiKeyOverride = apiKey ? { apiKey } : {};
|
|
789
816
|
return {
|
|
790
817
|
...options,
|
|
791
|
-
|
|
818
|
+
...apiKeyOverride,
|
|
792
819
|
onPayload: async (payload, modelData) => {
|
|
793
820
|
let nextPayload: unknown = payload;
|
|
794
821
|
|
|
@@ -819,8 +846,11 @@ function streamSimpleKimi(
|
|
|
819
846
|
|
|
820
847
|
while (true) {
|
|
821
848
|
const patchedOptions = buildPatchedOptions(currentKey);
|
|
849
|
+
// Route by the module-level PROTOCOL, not model.api, since we register
|
|
850
|
+
// with a custom api type (kimi-openai-completions / kimi-anthropic-messages)
|
|
851
|
+
// to avoid overriding the built-in Anthropic/OpenAI stream handlers.
|
|
822
852
|
const upstream =
|
|
823
|
-
|
|
853
|
+
PROTOCOL === "openai-completions"
|
|
824
854
|
? streamSimpleOpenAICompletions(
|
|
825
855
|
model as Model<"openai-completions">,
|
|
826
856
|
context,
|
|
@@ -828,22 +858,30 @@ function streamSimpleKimi(
|
|
|
828
858
|
)
|
|
829
859
|
: streamSimpleAnthropic(model as Model<"anthropic-messages">, context, patchedOptions);
|
|
830
860
|
|
|
831
|
-
let pushedAny = false;
|
|
832
861
|
let shouldRetry = false;
|
|
862
|
+
let prefixBuffer: AssistantMessageEvent[] = [];
|
|
833
863
|
|
|
834
864
|
try {
|
|
835
865
|
for await (const event of filterEmptyResponseStream(upstream)) {
|
|
836
|
-
//
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
//
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
866
|
+
// streamAnthropic emits a synthetic "start" event synchronously,
|
|
867
|
+
// before the for-await loop begins iterating and therefore before
|
|
868
|
+
// the HTTP request is actually made. If the request 401s, the loop
|
|
869
|
+
// throws and the catch block emits "error". Without buffering, the
|
|
870
|
+
// "start" event (which carries an empty AssistantMessage) leaks into
|
|
871
|
+
// the session history and the TUI, leaving a phantom empty assistant
|
|
872
|
+
// bubble. We buffer "start" events and only flush them once we see
|
|
873
|
+
// a non-error event that proves the stream is alive.
|
|
874
|
+
if (event.type === "start") {
|
|
875
|
+
prefixBuffer.push(event);
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Speculative OAuth refresh on the first error. We retry once so
|
|
880
|
+
// that short-lived Kimi tokens invalidated by the server before the
|
|
881
|
+
// local expires timestamp lapses don't surface as raw 401s to the
|
|
882
|
+
// user. Non-auth errors still trigger one wasted refresh, but the
|
|
883
|
+
// retried request fails the same way and we forward it.
|
|
884
|
+
if (attempt === 0 && event.type === "error") {
|
|
847
885
|
console.error(
|
|
848
886
|
`[kimi-coding] upstream error on first event, attempting refresh: ${event.error?.errorMessage?.slice(0, 200)}`,
|
|
849
887
|
);
|
|
@@ -852,26 +890,50 @@ function streamSimpleKimi(
|
|
|
852
890
|
console.error("[kimi-coding] retrying stream with refreshed token");
|
|
853
891
|
currentKey = refreshed;
|
|
854
892
|
shouldRetry = true;
|
|
855
|
-
break;
|
|
893
|
+
break; // discard prefixBuffer — don't leak the stale start
|
|
856
894
|
}
|
|
857
895
|
console.error(
|
|
858
896
|
"[kimi-coding] refresh did not yield a new token, forwarding original error",
|
|
859
897
|
);
|
|
860
898
|
}
|
|
899
|
+
|
|
900
|
+
// First non-start, non-retry event: flush buffered prefix, then
|
|
901
|
+
// stream normally.
|
|
902
|
+
for (const e of prefixBuffer) filtered.push(e);
|
|
903
|
+
prefixBuffer = [];
|
|
861
904
|
filtered.push(event);
|
|
862
|
-
pushedAny = true;
|
|
863
905
|
}
|
|
906
|
+
|
|
907
|
+
// Stream ended normally: flush any remaining buffered starts.
|
|
908
|
+
for (const e of prefixBuffer) filtered.push(e);
|
|
864
909
|
} catch (err) {
|
|
910
|
+
// Upstream threw rather than emitting a stream `error` event. This can
|
|
911
|
+
// be the same stale-token 401 surfaced as an exception (depending on
|
|
912
|
+
// the SDK path / network layer), so mirror the in-stream refresh
|
|
913
|
+
// branch: on attempt 0, try one OAuth refresh + retry. Either way,
|
|
914
|
+
// discard `prefixBuffer` — we never confirmed the stream actually
|
|
915
|
+
// started, and flushing the buffered `start` would resurrect the
|
|
916
|
+
// phantom empty assistant message this PR set out to fix.
|
|
865
917
|
console.error("[kimi-coding] stream error:", err);
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
918
|
+
if (attempt === 0) {
|
|
919
|
+
const refreshed = await refreshKimiAuthToken(currentKey);
|
|
920
|
+
if (refreshed && refreshed !== currentKey) {
|
|
921
|
+
console.error("[kimi-coding] retrying stream after thrown error with refreshed token");
|
|
922
|
+
currentKey = refreshed;
|
|
923
|
+
shouldRetry = true;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (!shouldRetry) {
|
|
927
|
+
filtered.push({
|
|
928
|
+
type: "error",
|
|
929
|
+
reason: "error",
|
|
930
|
+
error: {
|
|
931
|
+
content: [],
|
|
932
|
+
stopReason: "error",
|
|
933
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 },
|
|
934
|
+
},
|
|
935
|
+
} as AssistantMessageEvent & { type: "error" });
|
|
936
|
+
}
|
|
875
937
|
}
|
|
876
938
|
|
|
877
939
|
if (shouldRetry) {
|
|
@@ -893,7 +955,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
893
955
|
pi.registerProvider("kimi-coding", {
|
|
894
956
|
baseUrl: getBaseUrl(),
|
|
895
957
|
apiKey: "KIMI_API_KEY",
|
|
896
|
-
api:
|
|
958
|
+
api: KIMI_API_TYPE,
|
|
897
959
|
streamSimple: streamSimpleKimi,
|
|
898
960
|
|
|
899
961
|
headers: getCommonHeaders(),
|