pi-provider-kimi-code 0.1.9 → 0.2.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 +93 -84
- package/index.ts +377 -191
- package/package.json +29 -5
package/README.md
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# Pi extension for Kimi Code
|
|
3
2
|
[](https://www.npmjs.com/package/pi-provider-kimi-code)
|
|
3
|
+
[](./LICENSE)
|
|
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.
|
|
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.
|
|
8
|
+
|
|
9
|
+
## Who is this for?
|
|
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.
|
|
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.
|
|
14
|
+
|
|
15
|
+
Pay-per-token via `KIMI_API_KEY` also works if you just want to try Kimi in CI or without a subscription.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
4
18
|
|
|
5
|
-
|
|
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.
|
|
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
|
+
- **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
|
+
- **Works with both Anthropic- and OpenAI-compatible modes** — use whichever one your pi setup expects.
|
|
24
|
+
- **Stream cleaning** — Kimi occasionally leaks placeholder blocks into the stream during thinking phases; this extension catches and hides them so your pi UI stays clean.
|
|
25
|
+
- **Zero dependencies, zero build step** — the extension loads directly as TypeScript, nothing to compile.
|
|
6
26
|
|
|
7
27
|
## Install
|
|
8
28
|
|
|
29
|
+
From npm:
|
|
30
|
+
|
|
9
31
|
```bash
|
|
10
32
|
pi install npm:pi-provider-kimi-code
|
|
11
33
|
```
|
|
12
34
|
|
|
13
|
-
Or load without installing:
|
|
35
|
+
Or load a local checkout without installing:
|
|
14
36
|
|
|
15
37
|
```bash
|
|
16
38
|
pi -e /path/to/pi-provider-kimi-code
|
|
@@ -18,19 +40,19 @@ pi -e /path/to/pi-provider-kimi-code
|
|
|
18
40
|
|
|
19
41
|
## Authentication
|
|
20
42
|
|
|
21
|
-
###
|
|
43
|
+
### Browser login (recommended)
|
|
22
44
|
|
|
23
|
-
Inside pi
|
|
45
|
+
Inside `pi`, run:
|
|
24
46
|
|
|
25
47
|
```
|
|
26
48
|
/login kimi-coding
|
|
27
49
|
```
|
|
28
50
|
|
|
29
|
-
|
|
51
|
+
A browser tab opens, you sign into your Kimi account, and credentials are stored at `~/.pi/agent/auth.json`. Tokens refresh automatically.
|
|
30
52
|
|
|
31
|
-
### API
|
|
53
|
+
### API key
|
|
32
54
|
|
|
33
|
-
|
|
55
|
+
For CI or pay-per-token use, set `KIMI_API_KEY`:
|
|
34
56
|
|
|
35
57
|
```bash
|
|
36
58
|
KIMI_API_KEY=sk-... pi
|
|
@@ -44,101 +66,88 @@ KIMI_API_KEY=sk-... pi
|
|
|
44
66
|
| `kimi-k2.5` | Kimi K2.5 | yes | text, image | 262 144 | 32 000 |
|
|
45
67
|
| `kimi-k2-thinking-turbo` | Kimi K2 Thinking Turbo | yes | text | 262 144 | 32 000 |
|
|
46
68
|
|
|
47
|
-
Select a model inside pi
|
|
69
|
+
Select a model inside `pi`:
|
|
48
70
|
|
|
49
71
|
```
|
|
50
72
|
/model kimi-coding/kimi-code
|
|
51
73
|
```
|
|
52
74
|
|
|
53
|
-
## Environment
|
|
75
|
+
## Environment variables
|
|
54
76
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
- `KIMI_MODEL_MAX_TOKENS` — force max tokens on outbound requests
|
|
64
|
-
- `KIMI_CODE_DEBUG=1` — print provider-side debug logs (request metadata, file upload logs)
|
|
77
|
+
| Variable | Description |
|
|
78
|
+
| ---------------------------------- | ------------------------------------------------------ |
|
|
79
|
+
| `KIMI_API_KEY` | Static API key (alternative to browser login) |
|
|
80
|
+
| `KIMI_CODE_BASE_URL` | Override the API base URL |
|
|
81
|
+
| `KIMI_CODE_OAUTH_HOST` | Override the OAuth host |
|
|
82
|
+
| `KIMI_CODE_PROTOCOL` | `anthropic` (default) or `openai` |
|
|
83
|
+
| `KIMI_CODE_UPLOAD_THRESHOLD_BYTES` | Image auto-upload threshold, default `1048576` (1 MB) |
|
|
84
|
+
| `KIMI_CODE_DEBUG` | Set to `1` to print provider-side debug logs |
|
|
65
85
|
|
|
66
|
-
|
|
86
|
+
Full list with defaults and the test-script variables: [docs/ENV.md](docs/ENV.md).
|
|
67
87
|
|
|
68
|
-
|
|
88
|
+
## FAQ
|
|
69
89
|
|
|
70
|
-
|
|
71
|
-
2. **TTL**: Approximately **5-10 minutes**. Consistently hits at 300s, borderline at 600s, misses at 900s. Measure in your environment with `KIMI_E2E_ONLY_CACHE=1` (see [E2E test script](#end-to-end-test-script)).
|
|
72
|
-
3. **Manual override**: Set `prompt_cache_key` explicitly in the payload or options to override the automatic session-based key.
|
|
90
|
+
### How is this different from the official `kimi-cli`?
|
|
73
91
|
|
|
74
|
-
|
|
92
|
+
`kimi-cli` is Moonshot's own terminal agent. It's a full CLI — it replaces pi, you can't use it as a pi provider. This extension is the bridge: it lets you keep pi as your harness and point it at your Kimi Code Membership for inference, reusing the same login flow `kimi-cli` v1.3+ ships.
|
|
75
93
|
|
|
76
|
-
|
|
94
|
+
### Do I need a paid Kimi subscription?
|
|
77
95
|
|
|
78
|
-
|
|
96
|
+
For browser login, yes — whatever your Moonshot account is entitled to is what the provider can access. Without a plan you'll hit rate limits quickly. For pay-per-token usage, set `KIMI_API_KEY` instead.
|
|
79
97
|
|
|
80
|
-
|
|
81
|
-
- Disables `store`
|
|
82
|
-
- Uses `max_tokens`
|
|
83
|
-
- Sends `KimiCLI/*` + `X-Msh-*` agent headers required by the Coding endpoint
|
|
98
|
+
### Where does my data go?
|
|
84
99
|
|
|
85
|
-
|
|
100
|
+
Requests go only to `api.kimi.com` (Moonshot's servers). Login credentials are stored locally at `~/.pi/agent/auth.json`, readable only by your user. Nothing is uploaded to any third party.
|
|
86
101
|
|
|
87
|
-
|
|
102
|
+
### Is this affiliated with Moonshot AI?
|
|
88
103
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
No. This is an independent extension. The login flow is derived from the public implementation in the open-source [`kimi-cli`](https://github.com/MoonshotAI/kimi-cli) repository.
|
|
105
|
+
|
|
106
|
+
### Which pi version does this work with?
|
|
107
|
+
|
|
108
|
+
Any recent [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent).
|
|
109
|
+
|
|
110
|
+
### Why are there two protocol modes?
|
|
111
|
+
|
|
112
|
+
Kimi's coding endpoint speaks both Anthropic and OpenAI dialects. Anthropic mode has better visibility into cache hits, so it's the default. Switch via `KIMI_CODE_PROTOCOL=openai` if something in your pi setup prefers the OpenAI path.
|
|
113
|
+
|
|
114
|
+
## Troubleshooting
|
|
115
|
+
|
|
116
|
+
### `pi` reports `fetch failed` even though `curl` works
|
|
117
|
+
|
|
118
|
+
pi runs on Node's `fetch` / undici stack, which handles `http_proxy` / `https_proxy` / `all_proxy` differently from `curl`. Verify those variables in the pi process's environment. The bundled smoke-test script `scripts/test_e2e.sh` prints the effective proxy-related environment for easier debugging.
|
|
119
|
+
|
|
120
|
+
### `/login kimi-coding` prints a device code but the browser never opens
|
|
121
|
+
|
|
122
|
+
The login flow always prints a verification URL — opening it is the terminal's job. If your terminal or OS blocks auto-open, copy the URL and paste it into a browser manually.
|
|
123
|
+
|
|
124
|
+
### "Access denied" or subscription errors after a successful login
|
|
125
|
+
|
|
126
|
+
Your Moonshot account needs an active Kimi Code subscription for the provider to do anything useful. If the same account works in `kimi-cli`, re-run `/login kimi-coding` to refresh credentials.
|
|
127
|
+
|
|
128
|
+
### Large images fail with a payload error
|
|
129
|
+
|
|
130
|
+
This extension uploads images over `KIMI_CODE_UPLOAD_THRESHOLD_BYTES` (default 1 MB) to Kimi's Files API and references them as `ms://`. Videos always upload. Set `KIMI_CODE_DEBUG=1` to see upload decisions in the provider logs.
|
|
131
|
+
|
|
132
|
+
### Prompt cache never seems to hit
|
|
133
|
+
|
|
134
|
+
The cache is keyed off your pi session, which changes between sessions. Long-lived sessions get cache hits within a few minutes; cold sessions won't. Also check `PI_CACHE_RETENTION` — if it's set to `none`, this extension intentionally skips cache-key injection. For deterministic measurement, run the cache-TTL probe with a fixed key — see [docs/TESTING.md](docs/TESTING.md).
|
|
135
|
+
|
|
136
|
+
### OpenAI-compatible tools complain about a `developer` role
|
|
137
|
+
|
|
138
|
+
In OpenAI mode this extension maps the `developer` role to `system` (Kimi's Coding endpoint does not recognize `developer`). If something in your toolchain expects `developer` to round-trip, use Anthropic mode instead.
|
|
139
|
+
|
|
140
|
+
## References
|
|
92
141
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
2. Runs the same smoke test in OpenAI mode.
|
|
99
|
-
3. Runs a high-thinking smoke test (saves full JSONL, extracts thinking + answer + usage).
|
|
100
|
-
4. Runs a prompt-cache TTL probe and prints a conclusion on observed cache lifetime.
|
|
101
|
-
5. Uploads a watermarked image to `/files`, then asks the model to read back the watermark via `ms://` reference.
|
|
102
|
-
6. Sends a `prompt_cache_key` request directly to verify cache key injection.
|
|
103
|
-
|
|
104
|
-
Useful environment variables for the test script:
|
|
105
|
-
|
|
106
|
-
| Variable | Default | Description |
|
|
107
|
-
| -------------------------- | ----------------------- | --------------------------------------- |
|
|
108
|
-
| `KIMI_API_KEY` | (required) | API key (or pass as positional arg) |
|
|
109
|
-
| `KIMI_CODE_DEBUG` | `1` | Provider debug logs |
|
|
110
|
-
| `KIMI_E2E_VERBOSE` | `1` | Command and environment diagnostics |
|
|
111
|
-
| `KIMI_E2E_MODEL` | `kimi-coding/kimi-code` | Model for `pi` smoke tests |
|
|
112
|
-
| `KIMI_E2E_CACHE_INTERVALS` | `60,300` | Absolute seconds from warmup per probe |
|
|
113
|
-
| `KIMI_E2E_CACHE_KEY` | auto | Override cache key for TTL probe |
|
|
114
|
-
| `KIMI_E2E_CACHE_REPEAT` | `2000` | Long-text repeat count for cache warmup |
|
|
115
|
-
| `KIMI_E2E_SKIP_CACHE` | `0` | Set `1` to skip the cache TTL phase |
|
|
116
|
-
| `KIMI_E2E_ONLY_CACHE` | `0` | Set `1` to run only the cache TTL test |
|
|
117
|
-
|
|
118
|
-
### Proxy / networking note
|
|
119
|
-
|
|
120
|
-
If `curl` can reach Kimi but `pi` reports `fetch failed`, check your `http_proxy` / `https_proxy` / `all_proxy` environment. `pi` runs on Node's `fetch` / undici stack, which may behave differently from `curl`; the test script prints the effective proxy-related environment for easier debugging.
|
|
121
|
-
|
|
122
|
-
## How It Works
|
|
123
|
-
|
|
124
|
-
- Registers provider `kimi-coding` with Kimi's API endpoint
|
|
125
|
-
- Supports two protocol modes:
|
|
126
|
-
- `anthropic-messages` (default)
|
|
127
|
-
- `openai-completions` when `KIMI_CODE_PROTOCOL=openai`
|
|
128
|
-
- Adds a small `streamSimple` wrapper to suppress Kimi's leaked `(Empty response: ...)` placeholder blocks
|
|
129
|
-
- Injects `prompt_cache_key` automatically so Kimi's cache works with pi sessions
|
|
130
|
-
- Maps pi thinking levels to Kimi-native `reasoning_effort` + `extra_body.thinking`
|
|
131
|
-
- Pre-uploads large image / video attachments to Kimi Files API (returns `ms://` references)
|
|
132
|
-
- Maps `developer` role to `system` for OpenAI protocol compatibility
|
|
133
|
-
- Sends the same `KimiCLI/1.30.0` + `X-Msh-*` headers as current `kimi-cli`
|
|
134
|
-
- Persists a stable device ID at `~/.pi/providers/kimi-coding/device_id`
|
|
135
|
-
- OAuth uses [RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628) device authorization grant against `https://auth.kimi.com`
|
|
136
|
-
- Zero dependencies — types from `@mariozechner/pi-ai` and `@mariozechner/pi-coding-agent` are provided by the pi runtime
|
|
137
|
-
- Zero build step — pi loads TypeScript directly via jiti
|
|
142
|
+
- Upstream harness: [badlogic/pi-mono](https://github.com/badlogic/pi-mono) · [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent)
|
|
143
|
+
- Upstream login implementation and feature request: [MoonshotAI/kimi-cli](https://github.com/MoonshotAI/kimi-cli) · [kimi-cli#757](https://github.com/MoonshotAI/kimi-cli/issues/757)
|
|
144
|
+
- Environment variables: [docs/ENV.md](docs/ENV.md)
|
|
145
|
+
- Testing guide: [docs/TESTING.md](docs/TESTING.md)
|
|
146
|
+
- Architecture notes: [docs/architecture.md](docs/architecture.md)
|
|
138
147
|
|
|
139
148
|
## Credits
|
|
140
149
|
|
|
141
|
-
|
|
150
|
+
Based on the login implementation from [`kimi-cli`](https://github.com/MoonshotAI/kimi-cli) by Moonshot AI. Built as an extension for [`pi-coding-agent`](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) by [@badlogic](https://github.com/badlogic).
|
|
142
151
|
|
|
143
152
|
## License
|
|
144
153
|
|
package/index.ts
CHANGED
|
@@ -18,9 +18,11 @@ import type {
|
|
|
18
18
|
OAuthCredentials,
|
|
19
19
|
OAuthLoginCallbacks,
|
|
20
20
|
AssistantMessageEvent,
|
|
21
|
+
CacheRetention,
|
|
21
22
|
Context,
|
|
22
23
|
Model,
|
|
23
24
|
SimpleStreamOptions,
|
|
25
|
+
ThinkingLevel,
|
|
24
26
|
} from "@mariozechner/pi-ai";
|
|
25
27
|
import {
|
|
26
28
|
streamSimpleAnthropic,
|
|
@@ -361,13 +363,39 @@ async function refreshKimiCodeToken(credentials: OAuthCredentials): Promise<OAut
|
|
|
361
363
|
}
|
|
362
364
|
|
|
363
365
|
// =============================================================================
|
|
364
|
-
//
|
|
366
|
+
// Payload / stream helpers: types + pure utilities
|
|
365
367
|
// =============================================================================
|
|
366
|
-
// The Kimi API wraps thinking-only responses (no text content) into a text
|
|
367
|
-
// block like: (Empty response: {'content': [{'type': 'thinking', ...}]})
|
|
368
|
-
// This leaks internal state to the user. We detect and suppress such blocks.
|
|
369
368
|
|
|
370
369
|
const EMPTY_RESPONSE_PREFIX = "(Empty response:";
|
|
370
|
+
const DEFAULT_KIMI_INLINE_UPLOAD_THRESHOLD_BYTES = 1 * 1024 * 1024;
|
|
371
|
+
|
|
372
|
+
type JsonRecord = Record<string, unknown>;
|
|
373
|
+
type Uploader = (mimeType: string, data: string) => Promise<string | null>;
|
|
374
|
+
|
|
375
|
+
interface KimiEnvOverrides {
|
|
376
|
+
temperature?: number;
|
|
377
|
+
topP?: number;
|
|
378
|
+
maxTokens?: number;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function resolveCacheRetention(value?: CacheRetention): CacheRetention {
|
|
382
|
+
if (value === "none" || value === "short" || value === "long") return value;
|
|
383
|
+
if (process.env.PI_CACHE_RETENTION === "long") return "long";
|
|
384
|
+
return "short";
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
interface KimiPayloadContext {
|
|
388
|
+
api: "anthropic-messages" | "openai-completions";
|
|
389
|
+
upload?: Uploader;
|
|
390
|
+
cacheKey?: string;
|
|
391
|
+
cacheRetention: CacheRetention;
|
|
392
|
+
reasoning?: ThinkingLevel;
|
|
393
|
+
envOverrides: KimiEnvOverrides;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function isRecord(value: unknown): value is JsonRecord {
|
|
397
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
398
|
+
}
|
|
371
399
|
|
|
372
400
|
function mapThinkingLevel(level?: string): { effort: string | null; enabled: boolean } | undefined {
|
|
373
401
|
if (!level) return undefined;
|
|
@@ -378,215 +406,373 @@ function mapThinkingLevel(level?: string): { effort: string | null; enabled: boo
|
|
|
378
406
|
return undefined;
|
|
379
407
|
}
|
|
380
408
|
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
409
|
+
function parseInlineUploadThreshold(raw: string | undefined): number {
|
|
410
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
411
|
+
return Number.isFinite(parsed) && parsed >= 0
|
|
412
|
+
? parsed
|
|
413
|
+
: DEFAULT_KIMI_INLINE_UPLOAD_THRESHOLD_BYTES;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function deriveFilesBaseUrl(baseUrl: string): string {
|
|
417
|
+
const trimmed = baseUrl.replace(/\/$/, "");
|
|
418
|
+
return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function parseDataUrl(url: string): { mimeType: string; data: string } | null {
|
|
422
|
+
const match = url.match(/^data:([^;,]+);base64,([A-Za-z0-9+/=]+)$/);
|
|
423
|
+
return match ? { mimeType: match[1], data: match[2] } : null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function getUploadFilename(mimeType: string): string {
|
|
427
|
+
const map: Record<string, string> = {
|
|
428
|
+
"image/jpeg": "upload.jpg",
|
|
429
|
+
"image/png": "upload.png",
|
|
430
|
+
"image/gif": "upload.gif",
|
|
431
|
+
"image/webp": "upload.webp",
|
|
432
|
+
"video/mp4": "upload.mp4",
|
|
433
|
+
"video/quicktime": "upload.mov",
|
|
434
|
+
};
|
|
435
|
+
return map[mimeType] ?? (mimeType.startsWith("video/") ? "upload.mp4" : "upload.bin");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function readEnvOverrides(): KimiEnvOverrides {
|
|
439
|
+
const out: KimiEnvOverrides = {};
|
|
440
|
+
const temp = process.env.KIMI_MODEL_TEMPERATURE;
|
|
441
|
+
if (temp) out.temperature = parseFloat(temp);
|
|
442
|
+
const topP = process.env.KIMI_MODEL_TOP_P;
|
|
443
|
+
if (topP) out.topP = parseFloat(topP);
|
|
444
|
+
const maxTokens = process.env.KIMI_MODEL_MAX_TOKENS;
|
|
445
|
+
if (maxTokens) out.maxTokens = parseInt(maxTokens, 10);
|
|
446
|
+
return out;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// =============================================================================
|
|
450
|
+
// File upload (I/O edge)
|
|
451
|
+
// =============================================================================
|
|
452
|
+
|
|
453
|
+
async function uploadKimiFile(
|
|
454
|
+
apiKey: string,
|
|
455
|
+
mimeType: string,
|
|
456
|
+
data: string,
|
|
457
|
+
): Promise<string | null> {
|
|
458
|
+
const buffer = Buffer.from(data, "base64");
|
|
459
|
+
const isVideo = mimeType.startsWith("video/");
|
|
460
|
+
const threshold = parseInlineUploadThreshold(process.env.KIMI_CODE_UPLOAD_THRESHOLD_BYTES);
|
|
461
|
+
if (!isVideo && buffer.length <= threshold) return null;
|
|
462
|
+
|
|
463
|
+
const filename = getUploadFilename(mimeType);
|
|
464
|
+
const formData = new FormData();
|
|
465
|
+
formData.append("file", new Blob([buffer], { type: mimeType }), filename);
|
|
466
|
+
formData.append("purpose", isVideo ? "video" : "image");
|
|
467
|
+
|
|
468
|
+
const baseUrl = process.env.KIMI_CODE_BASE_URL || DEFAULT_BASE_URL;
|
|
469
|
+
const uploadUrl = `${deriveFilesBaseUrl(baseUrl)}/files`;
|
|
470
|
+
const debug = process.env.KIMI_CODE_DEBUG === "1";
|
|
471
|
+
if (debug) {
|
|
472
|
+
console.log(
|
|
473
|
+
`\n[kimi-coding] Uploading ${filename} to ${uploadUrl} (${(buffer.length / 1024 / 1024).toFixed(2)} MB)`,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
const response = await fetch(uploadUrl, {
|
|
479
|
+
method: "POST",
|
|
480
|
+
headers: { Authorization: `Bearer ${apiKey}`, ...getCommonHeaders() },
|
|
481
|
+
body: formData,
|
|
482
|
+
});
|
|
483
|
+
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
|
|
484
|
+
const fileObj = (await response.json()) as { id?: string };
|
|
485
|
+
if (!fileObj.id) throw new Error("missing file id");
|
|
486
|
+
const fileUrl = `ms://${fileObj.id}`;
|
|
487
|
+
if (debug) console.log(`[kimi-coding] Upload success: ${fileUrl}`);
|
|
488
|
+
return fileUrl;
|
|
489
|
+
} catch (err) {
|
|
490
|
+
console.error("[kimi-coding] Upload failed:", err);
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// =============================================================================
|
|
496
|
+
// Payload file transformers (pure given an Uploader)
|
|
497
|
+
// =============================================================================
|
|
498
|
+
// These walk the provider-specific payload shape and replace inline base64
|
|
499
|
+
// image/video blocks with ms:// references returned by the injected uploader.
|
|
500
|
+
// They take an Uploader rather than an apiKey so they can be unit-tested with
|
|
501
|
+
// a fake uploader; all network I/O stays behind that boundary.
|
|
502
|
+
|
|
503
|
+
async function transformOpenAIPayloadFiles(payload: JsonRecord, upload: Uploader): Promise<void> {
|
|
504
|
+
if (!Array.isArray(payload.messages)) return;
|
|
505
|
+
const cache = new Map<string, string>();
|
|
506
|
+
|
|
507
|
+
for (const message of payload.messages) {
|
|
508
|
+
if (!isRecord(message) || !Array.isArray(message.content)) continue;
|
|
509
|
+
|
|
510
|
+
for (const block of message.content) {
|
|
511
|
+
if (!isRecord(block)) continue;
|
|
512
|
+
const key =
|
|
513
|
+
block.type === "image_url" ? "image_url" : block.type === "video_url" ? "video_url" : null;
|
|
514
|
+
if (!key) continue;
|
|
515
|
+
|
|
516
|
+
const field = block[key];
|
|
517
|
+
const urlValue =
|
|
518
|
+
typeof field === "string"
|
|
519
|
+
? field
|
|
520
|
+
: isRecord(field) && typeof field.url === "string"
|
|
521
|
+
? field.url
|
|
522
|
+
: null;
|
|
523
|
+
if (!urlValue || urlValue.startsWith("ms://")) continue;
|
|
524
|
+
|
|
525
|
+
const parsed = parseDataUrl(urlValue);
|
|
526
|
+
if (!parsed) continue;
|
|
527
|
+
|
|
528
|
+
const uploaded = cache.get(urlValue) ?? (await upload(parsed.mimeType, parsed.data));
|
|
529
|
+
if (!uploaded) continue;
|
|
530
|
+
cache.set(urlValue, uploaded);
|
|
531
|
+
|
|
532
|
+
block[key] =
|
|
533
|
+
typeof field === "string" ? uploaded : { ...(field as JsonRecord), url: uploaded };
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function transformAnthropicPayloadFiles(
|
|
539
|
+
payload: JsonRecord,
|
|
540
|
+
upload: Uploader,
|
|
541
|
+
): Promise<void> {
|
|
542
|
+
if (!Array.isArray(payload.messages)) return;
|
|
543
|
+
const cache = new Map<string, string>();
|
|
544
|
+
|
|
545
|
+
const transformImageBlock = async (block: unknown): Promise<unknown> => {
|
|
546
|
+
if (!isRecord(block) || block.type !== "image") return block;
|
|
547
|
+
const source = block.source;
|
|
548
|
+
if (!isRecord(source) || source.type !== "base64") return block;
|
|
549
|
+
const mediaType = source.media_type;
|
|
550
|
+
const data = source.data;
|
|
551
|
+
if (typeof mediaType !== "string" || typeof data !== "string") return block;
|
|
552
|
+
|
|
553
|
+
const cacheKey = `${mediaType}:${data}`;
|
|
554
|
+
const uploaded = cache.get(cacheKey) ?? (await upload(mediaType, data));
|
|
555
|
+
if (!uploaded) return block;
|
|
556
|
+
cache.set(cacheKey, uploaded);
|
|
557
|
+
|
|
558
|
+
const next: JsonRecord = { type: "image", source: { type: "url", url: uploaded } };
|
|
559
|
+
if (block.cache_control !== undefined) next.cache_control = block.cache_control;
|
|
560
|
+
return next;
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
for (const message of payload.messages) {
|
|
564
|
+
if (!isRecord(message) || !Array.isArray(message.content)) continue;
|
|
565
|
+
|
|
566
|
+
for (let i = 0; i < message.content.length; i++) {
|
|
567
|
+
const block = message.content[i];
|
|
568
|
+
if (isRecord(block) && block.type === "tool_result" && Array.isArray(block.content)) {
|
|
569
|
+
for (let j = 0; j < block.content.length; j++) {
|
|
570
|
+
block.content[j] = await transformImageBlock(block.content[j]);
|
|
439
571
|
}
|
|
440
|
-
|
|
572
|
+
continue;
|
|
441
573
|
}
|
|
442
|
-
|
|
443
|
-
} else {
|
|
444
|
-
transformedMessages.push(message);
|
|
574
|
+
message.content[i] = await transformImageBlock(block);
|
|
445
575
|
}
|
|
446
576
|
}
|
|
447
|
-
return { ...context, messages: transformedMessages };
|
|
448
577
|
}
|
|
449
578
|
|
|
579
|
+
// =============================================================================
|
|
580
|
+
// Payload mutation pipeline
|
|
581
|
+
// =============================================================================
|
|
582
|
+
// Applies all Kimi-specific mutations to a provider payload in place.
|
|
583
|
+
// Pure given its context: no process.env / fs / network access of its own —
|
|
584
|
+
// every side effect enters via ctx.upload or pre-read values in ctx.
|
|
585
|
+
// This makes the five steps below testable with fixture payloads.
|
|
586
|
+
|
|
587
|
+
async function applyKimiPayloadMutations(
|
|
588
|
+
payload: JsonRecord,
|
|
589
|
+
ctx: KimiPayloadContext,
|
|
590
|
+
): Promise<void> {
|
|
591
|
+
// 1. Map unsupported roles: Kimi does not recognize "developer" (OpenAI-specific).
|
|
592
|
+
if (Array.isArray(payload.messages)) {
|
|
593
|
+
payload.messages = payload.messages.map((msg) =>
|
|
594
|
+
isRecord(msg) && msg.role === "developer" ? { ...msg, role: "system" } : msg,
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// 2. File upload dispatch (protocol-specific).
|
|
599
|
+
if (ctx.upload) {
|
|
600
|
+
if (ctx.api === "openai-completions") {
|
|
601
|
+
await transformOpenAIPayloadFiles(payload, ctx.upload);
|
|
602
|
+
} else if (ctx.api === "anthropic-messages") {
|
|
603
|
+
await transformAnthropicPayloadFiles(payload, ctx.upload);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// 3. prompt_cache_key injection. Respect any key already on the payload,
|
|
608
|
+
// otherwise fall back to the caller-provided cacheKey (sessionId or
|
|
609
|
+
// explicit options.prompt_cache_key override). Skipped entirely when
|
|
610
|
+
// cacheRetention is "none" (via options.cacheRetention or
|
|
611
|
+
// PI_CACHE_RETENTION) so callers can truly disable caching — otherwise
|
|
612
|
+
// Kimi's native session cache would still fire even if pi-ai's
|
|
613
|
+
// Anthropic-style cache_control markers are omitted.
|
|
614
|
+
if (ctx.cacheRetention !== "none") {
|
|
615
|
+
const existing = payload.prompt_cache_key;
|
|
616
|
+
const resolved = (typeof existing === "string" && existing) || ctx.cacheKey;
|
|
617
|
+
if (resolved) payload.prompt_cache_key = resolved;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// 4. Env-level hyperparameter overrides (pre-parsed into numbers by caller).
|
|
621
|
+
const { temperature, topP, maxTokens } = ctx.envOverrides;
|
|
622
|
+
if (temperature !== undefined) payload.temperature = temperature;
|
|
623
|
+
if (topP !== undefined) payload.top_p = topP;
|
|
624
|
+
if (maxTokens !== undefined) payload.max_tokens = maxTokens;
|
|
625
|
+
|
|
626
|
+
// 5. Reasoning effort mapping.
|
|
627
|
+
if (ctx.reasoning) {
|
|
628
|
+
const mapped = mapThinkingLevel(ctx.reasoning);
|
|
629
|
+
if (mapped) {
|
|
630
|
+
payload.reasoning_effort = mapped.effort;
|
|
631
|
+
const extraBody = isRecord(payload.extra_body) ? payload.extra_body : {};
|
|
632
|
+
extraBody.thinking = { type: mapped.enabled ? "enabled" : "disabled" };
|
|
633
|
+
payload.extra_body = extraBody;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// =============================================================================
|
|
639
|
+
// Event stream filter: suppress Kimi "(Empty response: ...)" text blocks
|
|
640
|
+
// =============================================================================
|
|
641
|
+
// The Kimi API wraps thinking-only responses (no text content) into a text
|
|
642
|
+
// block like: (Empty response: {'content': [{'type': 'thinking', ...}]}).
|
|
643
|
+
// This leaks internal state to the user. We buffer text_start/text_delta
|
|
644
|
+
// events and drop the whole block if text_end starts with the marker.
|
|
645
|
+
// Pure async generator — no closure dependencies, testable with synthetic
|
|
646
|
+
// event arrays.
|
|
647
|
+
|
|
648
|
+
async function* filterEmptyResponseStream(
|
|
649
|
+
upstream: AsyncIterable<AssistantMessageEvent>,
|
|
650
|
+
): AsyncIterable<AssistantMessageEvent> {
|
|
651
|
+
const suppressedIndices = new Set<number>();
|
|
652
|
+
let textBuffer: AssistantMessageEvent[] = [];
|
|
653
|
+
let bufferingIndex: number | null = null;
|
|
654
|
+
|
|
655
|
+
for await (const event of upstream) {
|
|
656
|
+
// Start buffering when a new text block begins.
|
|
657
|
+
if (event.type === "text_start") {
|
|
658
|
+
bufferingIndex = event.contentIndex;
|
|
659
|
+
textBuffer = [event];
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Accumulate text deltas + detect the empty-response marker on text_end.
|
|
664
|
+
if (
|
|
665
|
+
bufferingIndex !== null &&
|
|
666
|
+
"contentIndex" in event &&
|
|
667
|
+
event.contentIndex === bufferingIndex
|
|
668
|
+
) {
|
|
669
|
+
if (event.type === "text_delta") {
|
|
670
|
+
textBuffer.push(event);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (event.type === "text_end") {
|
|
674
|
+
if (event.content.startsWith(EMPTY_RESPONSE_PREFIX)) {
|
|
675
|
+
// Suppress entire text block. Do NOT splice the message content
|
|
676
|
+
// array: it is a shared reference into session state, and mutating
|
|
677
|
+
// it would shift subsequent contentIndex values, corrupting the
|
|
678
|
+
// stream.
|
|
679
|
+
suppressedIndices.add(bufferingIndex);
|
|
680
|
+
} else {
|
|
681
|
+
// Legitimate text block — flush buffered events + end event.
|
|
682
|
+
for (const buffered of textBuffer) yield buffered;
|
|
683
|
+
yield event;
|
|
684
|
+
}
|
|
685
|
+
textBuffer = [];
|
|
686
|
+
bufferingIndex = null;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Skip any event targeting an already-suppressed content index.
|
|
692
|
+
if ("contentIndex" in event && suppressedIndices.has(event.contentIndex)) {
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Clean suppressed blocks out of the final message.
|
|
697
|
+
if (event.type === "done" && suppressedIndices.size > 0) {
|
|
698
|
+
event.message.content = event.message.content.filter(
|
|
699
|
+
(block) =>
|
|
700
|
+
!(
|
|
701
|
+
block.type === "text" &&
|
|
702
|
+
typeof block.text === "string" &&
|
|
703
|
+
block.text.startsWith(EMPTY_RESPONSE_PREFIX)
|
|
704
|
+
),
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
yield event;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// =============================================================================
|
|
713
|
+
// Stream wrapper: orchestrates payload mutation + event filter
|
|
714
|
+
// =============================================================================
|
|
715
|
+
// Reads every side-effect source (process.env, options, apiKey) at the top
|
|
716
|
+
// and hands a plain KimiPayloadContext to applyKimiPayloadMutations. The only
|
|
717
|
+
// thing this function itself "does" is wire SDK streaming + filter + error
|
|
718
|
+
// fallback; the actual logic lives in the pure units above.
|
|
719
|
+
|
|
450
720
|
function streamSimpleKimi(
|
|
451
721
|
model: Model<"anthropic-messages" | "openai-completions">,
|
|
452
722
|
context: Context,
|
|
453
723
|
options?: SimpleStreamOptions,
|
|
454
724
|
): AssistantMessageEventStream {
|
|
455
|
-
// Pre-process context files: upload large images/videos to Kimi API
|
|
456
725
|
const filtered = new AssistantMessageEventStream();
|
|
457
726
|
const apiKey = options?.apiKey || process.env.KIMI_API_KEY || "";
|
|
458
727
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
728
|
+
const cacheKeyOverride = (
|
|
729
|
+
options as (SimpleStreamOptions & { prompt_cache_key?: unknown }) | undefined
|
|
730
|
+
)?.prompt_cache_key;
|
|
731
|
+
const cacheKey = (typeof cacheKeyOverride === "string" && cacheKeyOverride) || options?.sessionId;
|
|
732
|
+
const cacheRetention = resolveCacheRetention(options?.cacheRetention);
|
|
733
|
+
const envOverrides = readEnvOverrides();
|
|
734
|
+
const upload: Uploader | undefined = apiKey
|
|
735
|
+
? (mimeType, data) => uploadKimiFile(apiKey, mimeType, data)
|
|
736
|
+
: undefined;
|
|
737
|
+
|
|
738
|
+
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
|
+
});
|
|
464
753
|
}
|
|
465
|
-
} catch (e) {
|
|
466
|
-
console.error("Error transforming context files", e);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// Intercept the payload to inject Kimi's proprietary prompt_cache_key
|
|
470
|
-
// because Kimi's Anthropic compatibility endpoint requires it alongside cache_control.
|
|
471
|
-
const originalOnPayload = options?.onPayload;
|
|
472
|
-
const patchedOptions: SimpleStreamOptions = {
|
|
473
|
-
...options,
|
|
474
|
-
onPayload: async (payload: any, modelData) => {
|
|
475
|
-
let nextPayload = payload;
|
|
476
|
-
if (originalOnPayload) {
|
|
477
|
-
const res = await originalOnPayload(payload, modelData);
|
|
478
|
-
if (res !== undefined) nextPayload = res;
|
|
479
|
-
}
|
|
480
754
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
msg.role === "developer" ? { ...msg, role: "system" } : msg,
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Inject prompt_cache_key: allow explicit override, fallback to stable sessionId.
|
|
490
|
-
const cacheKey =
|
|
491
|
-
nextPayload.prompt_cache_key ||
|
|
492
|
-
(options as any)?.prompt_cache_key ||
|
|
493
|
-
options?.sessionId;
|
|
494
|
-
if (cacheKey) {
|
|
495
|
-
nextPayload = { ...nextPayload, prompt_cache_key: cacheKey };
|
|
496
|
-
}
|
|
755
|
+
if (originalOnPayload) {
|
|
756
|
+
const res = await originalOnPayload(nextPayload, modelData);
|
|
757
|
+
if (res !== undefined) nextPayload = res;
|
|
758
|
+
}
|
|
497
759
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const envTopP = process.env.KIMI_MODEL_TOP_P;
|
|
503
|
-
if (envTopP) nextPayload.top_p = parseFloat(envTopP);
|
|
504
|
-
|
|
505
|
-
const envMaxTokens = process.env.KIMI_MODEL_MAX_TOKENS;
|
|
506
|
-
if (envMaxTokens) nextPayload.max_tokens = parseInt(envMaxTokens, 10);
|
|
507
|
-
|
|
508
|
-
// Reasoning effort mapping
|
|
509
|
-
const reasoningLevel = (options as any)?.reasoning;
|
|
510
|
-
if (reasoningLevel) {
|
|
511
|
-
const mapped = mapThinkingLevel(reasoningLevel);
|
|
512
|
-
if (mapped) {
|
|
513
|
-
nextPayload.reasoning_effort = mapped.effort;
|
|
514
|
-
nextPayload.extra_body = nextPayload.extra_body || {};
|
|
515
|
-
nextPayload.extra_body.thinking = {
|
|
516
|
-
type: mapped.enabled ? "enabled" : "disabled",
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
return nextPayload;
|
|
522
|
-
},
|
|
523
|
-
};
|
|
760
|
+
return nextPayload;
|
|
761
|
+
},
|
|
762
|
+
};
|
|
524
763
|
|
|
764
|
+
void (async () => {
|
|
525
765
|
const upstream =
|
|
526
766
|
model.api === "openai-completions"
|
|
527
|
-
? streamSimpleOpenAICompletions(
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
let textBuffer: AssistantMessageEvent[] = [];
|
|
534
|
-
let bufferingIndex: number | null = null;
|
|
767
|
+
? streamSimpleOpenAICompletions(
|
|
768
|
+
model as Model<"openai-completions">,
|
|
769
|
+
context,
|
|
770
|
+
patchedOptions,
|
|
771
|
+
)
|
|
772
|
+
: streamSimpleAnthropic(model as Model<"anthropic-messages">, context, patchedOptions);
|
|
535
773
|
|
|
536
774
|
try {
|
|
537
|
-
for await (const event of upstream) {
|
|
538
|
-
// Start buffering when a new text block begins
|
|
539
|
-
if (event.type === "text_start") {
|
|
540
|
-
bufferingIndex = event.contentIndex;
|
|
541
|
-
textBuffer = [event];
|
|
542
|
-
continue;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Accumulate text deltas while buffering
|
|
546
|
-
if (
|
|
547
|
-
bufferingIndex !== null &&
|
|
548
|
-
"contentIndex" in event &&
|
|
549
|
-
event.contentIndex === bufferingIndex
|
|
550
|
-
) {
|
|
551
|
-
if (event.type === "text_delta") {
|
|
552
|
-
textBuffer.push(event);
|
|
553
|
-
continue;
|
|
554
|
-
}
|
|
555
|
-
if (event.type === "text_end") {
|
|
556
|
-
if (event.content.startsWith(EMPTY_RESPONSE_PREFIX)) {
|
|
557
|
-
// Suppress entire text block — discard buffered events.
|
|
558
|
-
// Do NOT splice content array: it is a shared reference
|
|
559
|
-
// into session state, and mutating it shifts subsequent
|
|
560
|
-
// contentIndex values, corrupting the stream.
|
|
561
|
-
suppressedIndices.add(bufferingIndex);
|
|
562
|
-
} else {
|
|
563
|
-
// Legitimate text block — flush buffer + end event
|
|
564
|
-
for (const buffered of textBuffer) filtered.push(buffered);
|
|
565
|
-
filtered.push(event);
|
|
566
|
-
}
|
|
567
|
-
textBuffer = [];
|
|
568
|
-
bufferingIndex = null;
|
|
569
|
-
continue;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Skip events for already-suppressed indices
|
|
574
|
-
if ("contentIndex" in event && suppressedIndices.has(event.contentIndex)) {
|
|
575
|
-
continue;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// Clean suppressed blocks from the final message
|
|
579
|
-
if (event.type === "done" && suppressedIndices.size > 0) {
|
|
580
|
-
event.message.content = event.message.content.filter(
|
|
581
|
-
(block) =>
|
|
582
|
-
!(
|
|
583
|
-
block.type === "text" &&
|
|
584
|
-
typeof block.text === "string" &&
|
|
585
|
-
block.text.startsWith(EMPTY_RESPONSE_PREFIX)
|
|
586
|
-
),
|
|
587
|
-
);
|
|
588
|
-
}
|
|
589
|
-
|
|
775
|
+
for await (const event of filterEmptyResponseStream(upstream)) {
|
|
590
776
|
filtered.push(event);
|
|
591
777
|
}
|
|
592
778
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-provider-kimi-code",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Pi extension for Kimi Code — reuse your kimi.com Membership in pi-coding-agent (K2.5, K2 Thinking Turbo)",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"pi-provider",
|
|
9
|
+
"pi",
|
|
10
|
+
"pi-coding-agent",
|
|
11
|
+
"pi-mono",
|
|
6
12
|
"coding-agent",
|
|
13
|
+
"ai-provider",
|
|
14
|
+
"llm",
|
|
15
|
+
"llm-provider",
|
|
7
16
|
"kimi",
|
|
8
17
|
"kimi-code",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
18
|
+
"kimi-for-coding",
|
|
19
|
+
"kimi-coder",
|
|
20
|
+
"kimi-k2",
|
|
21
|
+
"kimi-k2.5",
|
|
22
|
+
"kimi-k2-thinking",
|
|
23
|
+
"moonshot",
|
|
24
|
+
"moonshotai",
|
|
25
|
+
"oauth",
|
|
26
|
+
"device-code",
|
|
27
|
+
"subscription"
|
|
12
28
|
],
|
|
13
29
|
"license": "MIT",
|
|
14
30
|
"repository": {
|
|
15
31
|
"type": "git",
|
|
16
32
|
"url": "https://github.com/Leechael/pi-provider-kimi-code"
|
|
17
33
|
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/Leechael/pi-provider-kimi-code/issues"
|
|
36
|
+
},
|
|
37
|
+
"author": "Leechael",
|
|
18
38
|
"files": [
|
|
19
39
|
"index.ts",
|
|
20
40
|
"package.json",
|
|
@@ -30,6 +50,10 @@
|
|
|
30
50
|
"format:check": "oxfmt . --check",
|
|
31
51
|
"pre-commit": "prek run"
|
|
32
52
|
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
55
|
+
"@mariozechner/pi-ai": "*"
|
|
56
|
+
},
|
|
33
57
|
"devDependencies": {
|
|
34
58
|
"@j178/prek": "^0.3.8",
|
|
35
59
|
"oxfmt": "^0.44.0",
|