pi-provider-kimi-code 0.2.1 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +13 -13
  2. package/index.ts +146 -71
  3. package/package.json +3 -4
package/README.md CHANGED
@@ -2,22 +2,22 @@
2
2
  [![npm](https://img.shields.io/npm/v/pi-provider-kimi-code)](https://www.npmjs.com/package/pi-provider-kimi-code)
3
3
  [![license](https://img.shields.io/npm/l/pi-provider-kimi-code)](./LICENSE)
4
4
 
5
- **Reuse your [Kimi Code Membership](https://www.kimi.com/code/docs/en/) inside [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent)** — no separate API credits, no second billing dashboard. Every request draws from your Kimi Code membership quota (the 5-hour token bucket) instead of billing per-token on the Moonshot Open Platform.
5
+ **Reuse your [Kimi Code Plan](https://www.kimi.com/code/docs/en/) inside [pi-coding-agent](https://pi.dev/)** — no separate API credits, no second billing dashboard. Every request draws from your Kimi Code membership quota (the 5-hour token bucket) instead of billing per-token on the Moonshot Open Platform.
6
6
 
7
- It's **"Claude Code for Kimi"** — log in with your Kimi account, with `KIMI_API_KEY` still supported as a fallback for CI. You get Kimi K2.5, Kimi K2 Thinking Turbo, and the umbrella `kimi-code` model with a 262K-token context window, automatic prompt caching, automatic large-image and video upload, and compatibility with both Anthropic-style and OpenAI-style clients.
7
+ It's **"Claude Code for Kimi"** — log in with your Kimi account, with `KIMI_API_KEY` still supported as a fallback for CI. You get `kimi-for-coding` (the Kimi-k2.6 latest alias, backed by the same Kimi Code Plan that covers K2.6 and K2.5) with a 262K-token context window, automatic prompt caching, automatic large-image and video upload, and compatibility with both Anthropic-style and OpenAI-style clients.
8
8
 
9
9
  ## Who is this for?
10
10
 
11
- - You already pay for a **[Kimi Code Membership](https://www.kimi.com/code/docs/en/)** and want to use it inside `pi-coding-agent` instead of the official `kimi-cli` — see [MoonshotAI/kimi-cli#757](https://github.com/MoonshotAI/kimi-cli/issues/757) for the canonical feature request this extension answers.
11
+ - You already pay for a **[Kimi Code Plan](https://www.kimi.com/code/docs/en/)** and want to use it inside `pi-coding-agent` instead of the official `kimi-cli` — see [MoonshotAI/kimi-cli#757](https://github.com/MoonshotAI/kimi-cli/issues/757) for the canonical feature request this extension answers.
12
12
  - You want **"Claude Code for Kimi"**: log in with your Kimi account instead of buying separate API credits. (`KIMI_API_KEY` is also supported as a fallback for CI.)
13
- - You're in the [pi / pi-mono](https://github.com/badlogic/pi-mono) ecosystem and want **Kimi K2.5** or **Kimi K2 Thinking Turbo** as a pi provider.
13
+ - You're in the [pi / pi-mono](https://github.com/badlogic/pi-mono) ecosystem and want **Kimi K2.6** (and the K2.5 coverage that comes with the Code Plan) as a pi provider.
14
14
 
15
15
  Pay-per-token via `KIMI_API_KEY` also works if you just want to try Kimi in CI or without a subscription.
16
16
 
17
17
  ## Features
18
18
 
19
19
  - **Browser login with your Kimi account** — reuse your Kimi Code Membership without buying separate API credits. Credentials are stored locally and refreshed automatically. `KIMI_API_KEY` is also accepted for pay-per-token or CI use.
20
- - **262K-token context window** on all three registered models.
20
+ - **262K-token context window** on all registered models.
21
21
  - **Automatic prompt caching** — binds Kimi's prompt cache to your pi session so repeated calls hit the cache cheaply (observed TTL ~5-10 minutes). Honors pi's `PI_CACHE_RETENTION=none` if you want to disable caching entirely.
22
22
  - **Automatic large-image and video upload** — images over 1 MB and all videos are uploaded to Kimi ahead of time, so you don't hit inline payload limits.
23
23
  - **Works with both Anthropic- and OpenAI-compatible modes** — use whichever one your pi setup expects.
@@ -47,7 +47,7 @@ If you prefer not to use npm, download the tarball from the [latest release](htt
47
47
  curl -L https://github.com/Leechael/pi-provider-kimi-code/releases/latest/download/pi-provider-kimi-code.tar.gz | tar -xz -C /tmp
48
48
 
49
49
  # Install from the extracted directory
50
- pi install /tmp/package
50
+ pi install /tmp/pi-provider-kimi-code
51
51
  ```
52
52
 
53
53
  ## Authentication
@@ -72,16 +72,16 @@ KIMI_API_KEY=sk-... pi
72
72
 
73
73
  ## Models
74
74
 
75
- | ID | Name | Reasoning | Input | Context | Max Output |
76
- | ------------------------ | -------------------------------- | --------- | ----------- | ------- | ---------- |
77
- | `kimi-code` | Kimi Code (powered by kimi-k2.5) | yes | text, image | 262 144 | 32 000 |
78
- | `kimi-k2.5` | Kimi K2.5 | yes | text, image | 262 144 | 32 000 |
79
- | `kimi-k2-thinking-turbo` | Kimi K2 Thinking Turbo | yes | text | 262 144 | 32 000 |
75
+ | ID | Name | Reasoning | Input | Context | Max Output |
76
+ | ----------------- | --------------- | --------- | ------------------ | ------- | ---------- |
77
+ | `kimi-for-coding` | Kimi for Coding | yes | text, image, video | 262 144 | 32 000 |
80
78
 
81
- Select a model inside `pi`:
79
+ `kimi-for-coding` is the latest alias on the Kimi Code Plan — today it points at Kimi-k2.6. The Plan itself still covers K2.5; the upstream routes older model IDs to the current alias, but this provider only publishes the canonical `kimi-for-coding` entry.
80
+
81
+ Select it inside `pi`:
82
82
 
83
83
  ```
84
- /model kimi-coding/kimi-code
84
+ /model kimi-coding/kimi-for-coding
85
85
  ```
86
86
 
87
87
  ## Environment variables
package/index.ts CHANGED
@@ -29,7 +29,8 @@ import {
29
29
  streamSimpleOpenAICompletions,
30
30
  AssistantMessageEventStream,
31
31
  } from "@mariozechner/pi-ai";
32
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
32
+ import type { ExtensionAPI, OAuthCredential } from "@mariozechner/pi-coding-agent";
33
+ import { AuthStorage } from "@mariozechner/pi-coding-agent";
33
34
 
34
35
  // =============================================================================
35
36
  // Constants
@@ -709,6 +710,54 @@ async function* filterEmptyResponseStream(
709
710
  }
710
711
  }
711
712
 
713
+ // =============================================================================
714
+ // Auth refresh: recover from server-side token invalidation
715
+ // =============================================================================
716
+ // pi-coding-agent only refreshes an OAuth token when the locally cached
717
+ // `expires` is in the past. If the server rotates/revokes the access token
718
+ // before that (common with short-lived session tokens), every request keeps
719
+ // returning 401. We detect that situation by inspecting the first event of
720
+ // the upstream stream, force a refresh through AuthStorage (which persists
721
+ // the new credentials under a file lock), and retry once.
722
+
723
+ const PROVIDER_ID = "kimi-coding";
724
+
725
+ async function refreshKimiAuthToken(currentKey: string): Promise<string | null> {
726
+ try {
727
+ const storage = AuthStorage.create();
728
+ const cred = storage.get(PROVIDER_ID);
729
+ if (!cred || cred.type !== "oauth") {
730
+ console.error(
731
+ `[kimi-coding] auth refresh skipped: no OAuth credentials for ${PROVIDER_ID} on disk`,
732
+ );
733
+ return null;
734
+ }
735
+
736
+ // If disk already has a different valid token (e.g., another process or a
737
+ // previous retry refreshed it while the caller's in-memory cache went
738
+ // stale), reuse it without hitting the OAuth endpoint.
739
+ if (cred.access !== currentKey && Date.now() < cred.expires) {
740
+ console.error("[kimi-coding] auth refresh: reusing newer on-disk token");
741
+ return cred.access;
742
+ }
743
+
744
+ console.error("[kimi-coding] auth refresh: requesting new access token");
745
+ const refreshed = await refreshAccessToken(cred.refresh);
746
+ const newCred: OAuthCredential = {
747
+ type: "oauth",
748
+ access: refreshed.access_token,
749
+ refresh: refreshed.refresh_token,
750
+ expires: Date.now() + refreshed.expires_in * 1000,
751
+ };
752
+ storage.set(PROVIDER_ID, newCred);
753
+ console.error("[kimi-coding] auth refresh: new token persisted");
754
+ return newCred.access;
755
+ } catch (err) {
756
+ console.error("[kimi-coding] auth refresh failed:", err);
757
+ return null;
758
+ }
759
+ }
760
+
712
761
  // =============================================================================
713
762
  // Stream wrapper: orchestrates payload mutation + event filter
714
763
  // =============================================================================
@@ -723,7 +772,7 @@ function streamSimpleKimi(
723
772
  options?: SimpleStreamOptions,
724
773
  ): AssistantMessageEventStream {
725
774
  const filtered = new AssistantMessageEventStream();
726
- const apiKey = options?.apiKey || process.env.KIMI_API_KEY || "";
775
+ const initialKey = options?.apiKey || process.env.KIMI_API_KEY || "";
727
776
 
728
777
  const cacheKeyOverride = (
729
778
  options as (SimpleStreamOptions & { prompt_cache_key?: unknown }) | undefined
@@ -731,61 +780,105 @@ function streamSimpleKimi(
731
780
  const cacheKey = (typeof cacheKeyOverride === "string" && cacheKeyOverride) || options?.sessionId;
732
781
  const cacheRetention = resolveCacheRetention(options?.cacheRetention);
733
782
  const envOverrides = readEnvOverrides();
734
- const upload: Uploader | undefined = apiKey
735
- ? (mimeType, data) => uploadKimiFile(apiKey, mimeType, data)
736
- : undefined;
737
-
738
783
  const originalOnPayload = options?.onPayload;
739
- const patchedOptions: SimpleStreamOptions = {
740
- ...options,
741
- onPayload: async (payload, modelData) => {
742
- let nextPayload: unknown = payload;
743
-
744
- if (isRecord(nextPayload)) {
745
- await applyKimiPayloadMutations(nextPayload, {
746
- api: model.api,
747
- upload,
748
- cacheKey,
749
- cacheRetention,
750
- reasoning: options?.reasoning,
751
- envOverrides,
752
- });
753
- }
754
784
 
755
- if (originalOnPayload) {
756
- const res = await originalOnPayload(nextPayload, modelData);
757
- if (res !== undefined) nextPayload = res;
758
- }
785
+ const buildPatchedOptions = (apiKey: string): SimpleStreamOptions => {
786
+ const upload: Uploader | undefined = apiKey
787
+ ? (mimeType, data) => uploadKimiFile(apiKey, mimeType, data)
788
+ : undefined;
789
+ return {
790
+ ...options,
791
+ apiKey,
792
+ onPayload: async (payload, modelData) => {
793
+ let nextPayload: unknown = payload;
794
+
795
+ if (isRecord(nextPayload)) {
796
+ await applyKimiPayloadMutations(nextPayload, {
797
+ api: model.api,
798
+ upload,
799
+ cacheKey,
800
+ cacheRetention,
801
+ reasoning: options?.reasoning,
802
+ envOverrides,
803
+ });
804
+ }
759
805
 
760
- return nextPayload;
761
- },
806
+ if (originalOnPayload) {
807
+ const res = await originalOnPayload(nextPayload, modelData);
808
+ if (res !== undefined) nextPayload = res;
809
+ }
810
+
811
+ return nextPayload;
812
+ },
813
+ };
762
814
  };
763
815
 
764
816
  void (async () => {
765
- const upstream =
766
- model.api === "openai-completions"
767
- ? streamSimpleOpenAICompletions(
768
- model as Model<"openai-completions">,
769
- context,
770
- patchedOptions,
771
- )
772
- : streamSimpleAnthropic(model as Model<"anthropic-messages">, context, patchedOptions);
773
-
774
- try {
775
- for await (const event of filterEmptyResponseStream(upstream)) {
776
- filtered.push(event);
817
+ let attempt = 0;
818
+ let currentKey = initialKey;
819
+
820
+ while (true) {
821
+ const patchedOptions = buildPatchedOptions(currentKey);
822
+ const upstream =
823
+ model.api === "openai-completions"
824
+ ? streamSimpleOpenAICompletions(
825
+ model as Model<"openai-completions">,
826
+ context,
827
+ patchedOptions,
828
+ )
829
+ : streamSimpleAnthropic(model as Model<"anthropic-messages">, context, patchedOptions);
830
+
831
+ let pushedAny = false;
832
+ let shouldRetry = false;
833
+
834
+ try {
835
+ for await (const event of filterEmptyResponseStream(upstream)) {
836
+ // If the upstream terminates with an error before we've emitted
837
+ // anything downstream, speculatively try an OAuth refresh and
838
+ // retry once. This handles the common case of a server-side token
839
+ // invalidation before the local `expires` lapses, without having
840
+ // to pattern-match every possible error-message format.
841
+ //
842
+ // Non-auth errors (overflow, network, rate-limit, etc.) still
843
+ // trigger one wasted refresh, but the retried request fails the
844
+ // same way and we forward it — pi-coding-agent's own recovery
845
+ // paths (compaction, retry) take over from there.
846
+ if (!pushedAny && attempt === 0 && event.type === "error") {
847
+ console.error(
848
+ `[kimi-coding] upstream error on first event, attempting refresh: ${event.error?.errorMessage?.slice(0, 200)}`,
849
+ );
850
+ const refreshed = await refreshKimiAuthToken(currentKey);
851
+ if (refreshed && refreshed !== currentKey) {
852
+ console.error("[kimi-coding] retrying stream with refreshed token");
853
+ currentKey = refreshed;
854
+ shouldRetry = true;
855
+ break;
856
+ }
857
+ console.error(
858
+ "[kimi-coding] refresh did not yield a new token, forwarding original error",
859
+ );
860
+ }
861
+ filtered.push(event);
862
+ pushedAny = true;
863
+ }
864
+ } catch (err) {
865
+ console.error("[kimi-coding] stream error:", err);
866
+ filtered.push({
867
+ type: "error",
868
+ reason: "error",
869
+ error: {
870
+ content: [],
871
+ stopReason: "error",
872
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 },
873
+ },
874
+ } as AssistantMessageEvent & { type: "error" });
777
875
  }
778
- } catch (err) {
779
- console.error("[kimi-coding] stream error:", err);
780
- filtered.push({
781
- type: "error",
782
- reason: "error",
783
- error: {
784
- content: [],
785
- stopReason: "error",
786
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 },
787
- },
788
- } as AssistantMessageEvent & { type: "error" });
876
+
877
+ if (shouldRetry) {
878
+ attempt++;
879
+ continue;
880
+ }
881
+ break;
789
882
  }
790
883
  })();
791
884
 
@@ -807,28 +900,10 @@ export default function (pi: ExtensionAPI) {
807
900
 
808
901
  models: [
809
902
  {
810
- id: "kimi-code",
811
- name: "Kimi Code (powered by kimi-k2.5)",
812
- reasoning: true,
813
- input: ["text", "image"],
814
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
815
- contextWindow: 262144,
816
- maxTokens: 32000,
817
- },
818
- {
819
- id: "kimi-k2.5",
820
- name: "Kimi K2.5",
821
- reasoning: true,
822
- input: ["text", "image"],
823
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
824
- contextWindow: 262144,
825
- maxTokens: 32000,
826
- },
827
- {
828
- id: "kimi-k2-thinking-turbo",
829
- name: "Kimi K2 Thinking Turbo",
903
+ id: "kimi-for-coding",
904
+ name: "Kimi for Coding",
830
905
  reasoning: true,
831
- input: ["text"],
906
+ input: ["text", "image", "video"],
832
907
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
833
908
  contextWindow: 262144,
834
909
  maxTokens: 32000,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-provider-kimi-code",
3
- "version": "0.2.1",
4
- "description": "Pi extension for Kimi Code — reuse your kimi.com Membership in pi-coding-agent (K2.5, K2 Thinking Turbo)",
3
+ "version": "0.3.0",
4
+ "description": "Pi extension for Kimi Code — reuse your Kimi Code Plan in pi-coding-agent (K2.6, K2.5)",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi-extension",
@@ -14,12 +14,11 @@
14
14
  "llm",
15
15
  "llm-provider",
16
16
  "kimi",
17
- "kimi-code",
18
17
  "kimi-for-coding",
19
18
  "kimi-coder",
20
19
  "kimi-k2",
20
+ "kimi-k2.6",
21
21
  "kimi-k2.5",
22
- "kimi-k2-thinking",
23
22
  "moonshot",
24
23
  "moonshotai",
25
24
  "oauth",