mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
feat(kimi): add Kimi K3 support (#109202)
* feat(moonshot): add Kimi K3 support * feat(kimi): add K3 subscription models --------- Co-authored-by: NianJiuZst <180004567+NianJiuZst@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
co-authored by
NianJiuZst
Peter Steinberger
parent
e9b974e18b
commit
42ff5ec754
@@ -368,14 +368,15 @@ Install `@openclaw/moonshot-provider` before onboarding. Add an explicit `models
|
||||
|
||||
- Provider: `moonshot`
|
||||
- Auth: `MOONSHOT_API_KEY`
|
||||
- Example model: `moonshot/kimi-k2.6`
|
||||
- Example model: `moonshot/kimi-k3`
|
||||
- CLI: `openclaw onboard --auth-choice moonshot-api-key` or `openclaw onboard --auth-choice moonshot-api-key-cn`
|
||||
|
||||
Kimi K2 model IDs:
|
||||
Kimi model IDs:
|
||||
|
||||
[//]: # "moonshot-kimi-k2-model-refs:start"
|
||||
|
||||
- `moonshot/kimi-k2.6`
|
||||
- `moonshot/kimi-k3`
|
||||
- `moonshot/kimi-k2.7-code`
|
||||
- `moonshot/kimi-k2.5`
|
||||
- `moonshot/kimi-k2-thinking`
|
||||
@@ -411,7 +412,8 @@ Kimi Coding uses Moonshot AI's Anthropic-compatible endpoint:
|
||||
|
||||
- Provider: `kimi`
|
||||
- Auth: `KIMI_API_KEY`
|
||||
- Example model: `kimi/kimi-for-coding`
|
||||
- Kimi K3: `kimi/k3` (256K) or `kimi/k3[1m]` (1M, Allegretto and above)
|
||||
- Kimi K2.7 Code: `kimi/kimi-for-coding`
|
||||
|
||||
```json5
|
||||
{
|
||||
|
||||
@@ -437,7 +437,7 @@ Live is opt-in, so there is no fixed "CI model list." `OPENCLAW_LIVE_MODELS=mode
|
||||
| `google/gemini-3.1-pro-preview` | Gemini API |
|
||||
| `google/gemini-3.5-flash` | Gemini API |
|
||||
| `cohere/command-a-plus-05-2026` | |
|
||||
| `moonshot/kimi-k2.7-code` | |
|
||||
| `moonshot/kimi-k3` | |
|
||||
| `anthropic/claude-opus-4-6` | |
|
||||
| `deepseek/deepseek-v4-flash` | |
|
||||
| `deepseek/deepseek-v4-pro` | |
|
||||
|
||||
+75
-37
@@ -1,15 +1,15 @@
|
||||
---
|
||||
summary: "Configure Moonshot K2 vs Kimi Coding (separate providers + keys)"
|
||||
summary: "Configure Moonshot Kimi models vs Kimi Coding (separate providers + keys)"
|
||||
read_when:
|
||||
- You want Moonshot K2 (Moonshot Open Platform) vs Kimi Coding setup
|
||||
- You want Moonshot Kimi K3/K2 (Moonshot Open Platform) vs Kimi Coding setup
|
||||
- You need to understand separate endpoints, keys, and model refs
|
||||
- You want copy/paste config for either provider
|
||||
title: "Moonshot AI"
|
||||
---
|
||||
|
||||
Moonshot provides the Kimi API with OpenAI-compatible endpoints. Set the
|
||||
default model to `moonshot/kimi-k2.6` for the Moonshot Open Platform, or
|
||||
`kimi/kimi-for-coding` for Kimi Coding.
|
||||
Moonshot provides the Kimi API with OpenAI-compatible endpoints. Select
|
||||
`moonshot/kimi-k3` for Kimi K3, keep the onboarding default
|
||||
`moonshot/kimi-k2.6`, or use `kimi/kimi-for-coding` for Kimi Coding.
|
||||
|
||||
<Warning>
|
||||
Moonshot and Kimi Coding are **separate providers**, each shipped as a separate external plugin. Keys are not interchangeable, endpoints differ, and model refs differ (`moonshot/...` vs `kimi/...`).
|
||||
@@ -19,29 +19,33 @@ Moonshot and Kimi Coding are **separate providers**, each shipped as a separate
|
||||
|
||||
[//]: # "moonshot-kimi-k2-ids:start"
|
||||
|
||||
| Model ref | Name | Reasoning | Input | Context | Max output |
|
||||
| --------------------------------- | ---------------------- | --------- | ----------- | ------- | ---------- |
|
||||
| `moonshot/kimi-k2.6` | Kimi K2.6 | No | text, image | 262,144 | 262,144 |
|
||||
| `moonshot/kimi-k2.7-code` | Kimi K2.7 Code | Always on | text, image | 262,144 | 262,144 |
|
||||
| `moonshot/kimi-k2.5` | Kimi K2.5 | No | text, image | 262,144 | 262,144 |
|
||||
| `moonshot/kimi-k2-thinking` | Kimi K2 Thinking | Yes | text | 262,144 | 262,144 |
|
||||
| `moonshot/kimi-k2-thinking-turbo` | Kimi K2 Thinking Turbo | Yes | text | 262,144 | 262,144 |
|
||||
| `moonshot/kimi-k2-turbo` | Kimi K2 Turbo | No | text | 256,000 | 16,384 |
|
||||
| Model ref | Name | Reasoning | Input | Context | Max output |
|
||||
| --------------------------------- | ---------------------- | ---------- | ----------- | --------- | ---------- |
|
||||
| `moonshot/kimi-k2.6` | Kimi K2.6 | No | text, image | 262,144 | 262,144 |
|
||||
| `moonshot/kimi-k3` | Kimi K3 | Always max | text, image | 1,048,576 | 1,048,576 |
|
||||
| `moonshot/kimi-k2.7-code` | Kimi K2.7 Code | Always on | text, image | 262,144 | 262,144 |
|
||||
| `moonshot/kimi-k2.5` | Kimi K2.5 | No | text, image | 262,144 | 262,144 |
|
||||
| `moonshot/kimi-k2-thinking` | Kimi K2 Thinking | Yes | text | 262,144 | 262,144 |
|
||||
| `moonshot/kimi-k2-thinking-turbo` | Kimi K2 Thinking Turbo | Yes | text | 262,144 | 262,144 |
|
||||
| `moonshot/kimi-k2-turbo` | Kimi K2 Turbo | No | text | 256,000 | 16,384 |
|
||||
|
||||
[//]: # "moonshot-kimi-k2-ids:end"
|
||||
|
||||
Catalog cost estimates use Moonshot's published pay-as-you-go rates: Kimi
|
||||
K2.7 Code is $0.19/MTok cache hit, $0.95/MTok input, $4.00/MTok output; Kimi
|
||||
K2.6 is $0.16/MTok cache hit, $0.95/MTok input, $4.00/MTok output; Kimi K2.5
|
||||
is $0.10/MTok cache hit, $0.60/MTok input, $3.00/MTok output. Other catalog
|
||||
Catalog cost estimates use Moonshot's published pay-as-you-go rates. The
|
||||
[Kimi K3 pricing](https://platform.kimi.ai/docs/pricing/chat-k3) is $0.30/MTok
|
||||
cache hit, $3.00/MTok input, and $15.00/MTok output; Kimi K2.7 Code is
|
||||
$0.19/MTok cache hit, $0.95/MTok input, $4.00/MTok output; Kimi K2.6 is
|
||||
$0.16/MTok cache hit, $0.95/MTok input, $4.00/MTok output; Kimi K2.5 is
|
||||
$0.10/MTok cache hit, $0.60/MTok input, $3.00/MTok output. Other catalog
|
||||
entries keep zero-cost placeholders unless you override them in config.
|
||||
|
||||
Kimi K2.7 Code always uses native thinking. OpenClaw exposes only the `on`
|
||||
thinking state for this model and omits outbound `thinking` and
|
||||
`reasoning_effort` fields, as required by Moonshot. It also omits sampling
|
||||
overrides (`temperature`, `top_p`, `n`, `presence_penalty`,
|
||||
`frequency_penalty`), which K2.7 fixes to provider defaults. Kimi K2.6 remains
|
||||
the onboarding default.
|
||||
Kimi K3 always reasons at `reasoning_effort: "max"`. OpenClaw exposes only
|
||||
`/think max`, omits the K2-only `thinking` field, and removes sampling
|
||||
overrides (`temperature`, `top_p`, `n`, `presence_penalty`, and
|
||||
`frequency_penalty`) that K3 fixes to provider defaults. Kimi K2.7 Code also
|
||||
always uses native thinking but requires both `thinking` and
|
||||
`reasoning_effort` to be omitted. Kimi K2.6 remains the onboarding default.
|
||||
See Moonshot's [Kimi K3 quickstart](https://platform.kimi.ai/docs/guide/kimi-k3-quickstart).
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -50,7 +54,7 @@ onboarding.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Moonshot API">
|
||||
**Best for:** Kimi K2 models via the Moonshot Open Platform.
|
||||
**Best for:** Kimi K3 and K2 models via the Moonshot Open Platform.
|
||||
|
||||
<Steps>
|
||||
<Step title="Install the plugin">
|
||||
@@ -76,15 +80,12 @@ onboarding.
|
||||
openclaw onboard --auth-choice moonshot-api-key-cn
|
||||
```
|
||||
</Step>
|
||||
<Step title="Set a default model">
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "moonshot/kimi-k2.6" },
|
||||
},
|
||||
},
|
||||
}
|
||||
<Step title="Set Kimi K3 as the default model">
|
||||
Onboarding keeps Kimi K2.6 as the initial default. Switch explicitly
|
||||
when you want Kimi K3:
|
||||
|
||||
```bash
|
||||
openclaw models set moonshot/kimi-k3
|
||||
```
|
||||
</Step>
|
||||
<Step title="Verify models are available">
|
||||
@@ -102,12 +103,12 @@ onboarding.
|
||||
openclaw agent --local \
|
||||
--session-id live-kimi-cost \
|
||||
--message 'Reply exactly: KIMI_LIVE_OK' \
|
||||
--thinking off \
|
||||
--thinking max \
|
||||
--json
|
||||
```
|
||||
|
||||
The JSON response should report `provider: "moonshot"` and
|
||||
`model: "kimi-k2.6"`. The assistant transcript entry stores normalized
|
||||
`model: "kimi-k3"`. The assistant transcript entry stores normalized
|
||||
token usage plus estimated cost under `usage.cost` when Moonshot returns
|
||||
usage metadata.
|
||||
</Step>
|
||||
@@ -124,6 +125,7 @@ onboarding.
|
||||
models: {
|
||||
// moonshot-kimi-k2-aliases:start
|
||||
"moonshot/kimi-k2.6": { alias: "Kimi K2.6" },
|
||||
"moonshot/kimi-k3": { alias: "Kimi K3" },
|
||||
"moonshot/kimi-k2.7-code": { alias: "Kimi K2.7 Code" },
|
||||
"moonshot/kimi-k2.5": { alias: "Kimi K2.5" },
|
||||
"moonshot/kimi-k2-thinking": { alias: "Kimi K2 Thinking" },
|
||||
@@ -151,6 +153,28 @@ onboarding.
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262144,
|
||||
},
|
||||
{
|
||||
id: "kimi-k3",
|
||||
name: "Kimi K3",
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {
|
||||
off: null,
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
high: null,
|
||||
xhigh: "max",
|
||||
max: "max",
|
||||
},
|
||||
input: ["text", "image"],
|
||||
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 1048576,
|
||||
compat: {
|
||||
supportsReasoningEffort: true,
|
||||
supportedReasoningEfforts: ["max"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "kimi-k2.7-code",
|
||||
name: "Kimi K2.7 Code",
|
||||
@@ -210,7 +234,7 @@ onboarding.
|
||||
**Best for:** code-focused tasks via the Kimi Coding endpoint.
|
||||
|
||||
<Note>
|
||||
Kimi Coding uses a different API key and provider prefix (`kimi/...`) than Moonshot (`moonshot/...`). The stable model ref is `kimi/kimi-for-coding`; legacy refs `kimi/kimi-code` and `kimi/k2p5` remain accepted and normalize to that model id.
|
||||
Kimi Coding uses a different API key and provider prefix (`kimi/...`) than Moonshot (`moonshot/...`). Kimi K3 uses `kimi/k3` for a 256K context or `kimi/k3[1m]` for the 1M tier. Kimi K2.7 Code keeps the stable ref `kimi/kimi-for-coding`; legacy refs `kimi/kimi-code` and `kimi/k2p5` remain accepted and normalize to that model id.
|
||||
</Note>
|
||||
|
||||
<Steps>
|
||||
@@ -243,6 +267,13 @@ onboarding.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
Kimi K3 always uses deep thinking at `max`. OpenClaw ignores stale K2
|
||||
thinking toggles for `kimi/k3` and `kimi/k3[1m]` and sends the K3
|
||||
adaptive/max request contract. The 1M model requires an Allegretto or
|
||||
higher Kimi membership; use `kimi/k3` on Moderato.
|
||||
|
||||
See the official [Kimi Code model table](https://www.kimi.com/code/docs/en/kimi-code/models.html) for current plan availability.
|
||||
|
||||
### Config example
|
||||
|
||||
```json5
|
||||
@@ -318,6 +349,13 @@ Config lives under `plugins.entries.moonshot.config.webSearch`:
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Native thinking mode">
|
||||
Kimi K3 always reasons at maximum effort. OpenClaw exposes only `/think max`,
|
||||
sends `reasoning_effort: "max"`, and ignores stale lower or `off` settings.
|
||||
K3 supports `auto`, `none`, `required`, and pinned tool choices, so OpenClaw
|
||||
preserves the requested `tool_choice`. For multi-turn tool use, OpenClaw
|
||||
preserves the assistant reasoning content required by Moonshot's replay
|
||||
contract.
|
||||
|
||||
Kimi K2.7 Code always uses native thinking. Moonshot requires clients to
|
||||
omit the `thinking` field for this model, so OpenClaw exposes only `on` and
|
||||
ignores stale `off` settings. K2.7 also fixes `temperature`, `top_p`, `n`,
|
||||
@@ -355,7 +393,7 @@ Config lives under `plugins.entries.moonshot.config.webSearch`:
|
||||
| Any non-off level | `thinking.type=enabled` |
|
||||
|
||||
<Warning>
|
||||
When Moonshot thinking is enabled, `tool_choice` must be `auto` or `none`. A pinned tool choice (`type: "tool"` or `type: "function"`) forces thinking back to `disabled` instead, so the requested tool still runs; `tool_choice: "required"` is normalized to `auto` instead. This applies to every Moonshot model except Kimi K2.7 Code, whose thinking mode cannot be disabled - its `tool_choice` is normalized to `auto` when incompatible.
|
||||
When Moonshot K2 thinking is enabled, `tool_choice` must be `auto` or `none`. A pinned tool choice (`type: "tool"` or `type: "function"`) forces thinking back to `disabled` instead, so the requested tool still runs; `tool_choice: "required"` is normalized to `auto` instead. Kimi K2.7 Code cannot disable thinking, so its incompatible `tool_choice` is normalized to `auto`. Kimi K3 uses its separate reasoning-effort contract and preserves supported tool choices.
|
||||
</Warning>
|
||||
|
||||
Kimi K2.6 also accepts an optional `thinking.keep` field that controls
|
||||
|
||||
@@ -38,7 +38,7 @@ title: "Thinking levels"
|
||||
- Google Gemini maps `/think adaptive` to Gemini's provider-owned dynamic thinking. Gemini 3 requests omit a fixed `thinkingLevel`, while Gemini 2.5 requests send `thinkingBudget: -1`; fixed levels still map to the closest Gemini `thinkingLevel` or budget for that model family.
|
||||
- MiniMax M2.x (`minimax/MiniMax-M2*`) on the Anthropic-compatible streaming path defaults to `thinking: { type: "disabled" }` unless you explicitly set thinking in model params or request params. This avoids leaked `reasoning_content` deltas from M2.x's non-native Anthropic stream format. MiniMax-M3 (and M3.x) is exempt: M3 emits proper Anthropic thinking blocks and returns empty content when thinking is disabled, so OpenClaw keeps M3 on the provider's omitted/adaptive thinking path.
|
||||
- Z.AI (`zai/*`) is binary (`on`/`off`) for most GLM models. GLM-5.2 is the exception: it exposes `/think off|low|high|max`, maps `low` and `high` to Z.AI `reasoning_effort: "high"`, and maps `max` to `reasoning_effort: "max"`.
|
||||
- Moonshot Kimi K2.7 Code (`moonshot/kimi-k2.7-code`) always thinks. Its profile exposes only `on`, and OpenClaw omits the outbound `thinking` field as required by Moonshot. Other `moonshot/*` models map `/think off` to `thinking: { type: "disabled" }` and any non-`off` level to `thinking: { type: "enabled" }`. When thinking is enabled, Moonshot only accepts `tool_choice` `auto|none`; OpenClaw normalizes incompatible values to `auto`.
|
||||
- Kimi K3 always thinks at `max`. Moonshot API (`moonshot/kimi-k3`) sends `reasoning_effort: "max"`, omits the K2 `thinking` field and fixed sampling overrides, and preserves K3-supported tool choices. Kimi Code (`kimi/k3` and `kimi/k3[1m]`) sends adaptive thinking with max effort and ignores stale K2 thinking toggles. Kimi K2.7 Code (`moonshot/kimi-k2.7-code`) also always thinks, exposes only `on`, and omits both outbound `thinking` and `reasoning_effort`. Other `moonshot/*` models map `/think off` to `thinking: { type: "disabled" }` and any non-`off` level to `thinking: { type: "enabled" }`. When K2 thinking is enabled, Moonshot only accepts `tool_choice` `auto|none`; OpenClaw normalizes incompatible values to `auto`.
|
||||
|
||||
## Resolution order
|
||||
|
||||
|
||||
@@ -64,6 +64,42 @@ describe("Kimi implicit provider (#22409)", () => {
|
||||
contextWindow: 262144,
|
||||
maxTokens: 32768,
|
||||
},
|
||||
{
|
||||
id: "k3",
|
||||
name: "Kimi K3",
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {
|
||||
off: null,
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
high: null,
|
||||
xhigh: "max",
|
||||
max: "max",
|
||||
},
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 262144,
|
||||
maxTokens: 32768,
|
||||
},
|
||||
{
|
||||
id: "k3[1m]",
|
||||
name: "Kimi K3 (1M)",
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {
|
||||
off: null,
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
high: null,
|
||||
xhigh: "max",
|
||||
max: "max",
|
||||
},
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 32768,
|
||||
},
|
||||
{
|
||||
id: "kimi-code",
|
||||
name: "Kimi Code (legacy kimi-code)",
|
||||
|
||||
@@ -43,4 +43,40 @@ describe("kimi provider plugin", () => {
|
||||
defaultLevel: "off",
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["k3", "k3[1m]"])("forces %s to max thinking", async (modelId) => {
|
||||
const provider = await registerSingleProviderPlugin(plugin);
|
||||
|
||||
expect(
|
||||
provider.resolveThinkingProfile?.({
|
||||
provider: "kimi",
|
||||
modelId,
|
||||
reasoning: true,
|
||||
} as never),
|
||||
).toEqual({
|
||||
levels: [{ id: "max", label: "max" }],
|
||||
defaultLevel: "max",
|
||||
preserveWhenCatalogReasoningFalse: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("wraps K3 simple completions without changing K2 simple completions", async () => {
|
||||
const provider = await registerSingleProviderPlugin(plugin);
|
||||
const streamFn = (() => undefined) as never;
|
||||
|
||||
expect(
|
||||
provider.wrapSimpleCompletionStreamFn?.({
|
||||
provider: "kimi",
|
||||
modelId: "k3",
|
||||
streamFn,
|
||||
} as never),
|
||||
).not.toBe(streamFn);
|
||||
expect(
|
||||
provider.wrapSimpleCompletionStreamFn?.({
|
||||
provider: "kimi",
|
||||
modelId: "kimi-for-coding",
|
||||
streamFn,
|
||||
} as never),
|
||||
).toBe(streamFn);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,11 @@ import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import type { SecretInput } from "openclaw/plugin-sdk/secret-input";
|
||||
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { applyKimiCodeConfig, KIMI_CODING_MODEL_REF } from "./onboard.js";
|
||||
import { buildKimiCodingProvider, normalizeKimiCodingModelId } from "./provider-catalog.js";
|
||||
import {
|
||||
buildKimiCodingProvider,
|
||||
isKimiK3ModelId,
|
||||
normalizeKimiCodingModelId,
|
||||
} from "./provider-catalog.js";
|
||||
import { KIMI_REPLAY_POLICY } from "./replay-policy.js";
|
||||
import { wrapKimiProviderStream } from "./stream.js";
|
||||
|
||||
@@ -101,13 +105,22 @@ export default definePluginEntry({
|
||||
const normalizedId = normalizeKimiCodingModelId(model.id);
|
||||
return normalizedId === model.id ? undefined : { ...model, id: normalizedId };
|
||||
},
|
||||
resolveThinkingProfile: () => ({
|
||||
levels: [
|
||||
{ id: "off", label: "off" },
|
||||
{ id: "low", label: "on" },
|
||||
],
|
||||
defaultLevel: "off",
|
||||
}),
|
||||
resolveThinkingProfile: ({ modelId }) =>
|
||||
isKimiK3ModelId(modelId)
|
||||
? {
|
||||
levels: [{ id: "max", label: "max" }],
|
||||
defaultLevel: "max",
|
||||
preserveWhenCatalogReasoningFalse: true,
|
||||
}
|
||||
: {
|
||||
levels: [
|
||||
{ id: "off", label: "off" },
|
||||
{ id: "low", label: "on" },
|
||||
],
|
||||
defaultLevel: "off",
|
||||
},
|
||||
wrapSimpleCompletionStreamFn: (ctx) =>
|
||||
isKimiK3ModelId(ctx.modelId) ? wrapKimiProviderStream(ctx) : ctx.streamFn,
|
||||
wrapStreamFn: wrapKimiProviderStream,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// Kimi Coding tests cover provider catalog plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildKimiCodingProvider, normalizeKimiCodingModelId } from "./provider-catalog.js";
|
||||
import {
|
||||
buildKimiCodingProvider,
|
||||
isKimiK3ModelId,
|
||||
normalizeKimiCodingModelId,
|
||||
} from "./provider-catalog.js";
|
||||
|
||||
describe("kimi provider catalog", () => {
|
||||
it("builds the bundled Kimi coding defaults", () => {
|
||||
@@ -11,14 +15,42 @@ describe("kimi provider catalog", () => {
|
||||
expect(provider.headers).toEqual({ "User-Agent": "claude-code/0.1.0" });
|
||||
expect(provider.models.map((model) => model.id)).toEqual([
|
||||
"kimi-for-coding",
|
||||
"k3",
|
||||
"k3[1m]",
|
||||
"kimi-code",
|
||||
"k2p5",
|
||||
]);
|
||||
expect(provider.models.find((model) => model.id === "k3")).toMatchObject({
|
||||
name: "Kimi K3",
|
||||
reasoning: true,
|
||||
contextWindow: 262_144,
|
||||
maxTokens: 32_768,
|
||||
thinkingLevelMap: {
|
||||
off: null,
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
high: null,
|
||||
xhigh: "max",
|
||||
max: "max",
|
||||
},
|
||||
});
|
||||
expect(provider.models.find((model) => model.id === "k3[1m]")).toMatchObject({
|
||||
name: "Kimi K3 (1M)",
|
||||
reasoning: true,
|
||||
contextWindow: 1_048_576,
|
||||
maxTokens: 32_768,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes legacy Kimi coding model ids to the stable API model id", () => {
|
||||
expect(normalizeKimiCodingModelId("kimi-code")).toBe("kimi-for-coding");
|
||||
expect(normalizeKimiCodingModelId("k2p5")).toBe("kimi-for-coding");
|
||||
expect(normalizeKimiCodingModelId("kimi-for-coding")).toBe("kimi-for-coding");
|
||||
expect(normalizeKimiCodingModelId("k3")).toBe("k3");
|
||||
expect(normalizeKimiCodingModelId("k3[1m]")).toBe("k3[1m]");
|
||||
expect(isKimiK3ModelId("k3")).toBe(true);
|
||||
expect(isKimiK3ModelId("k3[1m]")).toBe(true);
|
||||
expect(isKimiK3ModelId("kimi-for-coding")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,9 @@ import type {
|
||||
const KIMI_BASE_URL = "https://api.kimi.com/coding/";
|
||||
const KIMI_CODING_USER_AGENT = "claude-code/0.1.0";
|
||||
const KIMI_DEFAULT_MODEL_ID = "kimi-for-coding";
|
||||
// Kimi's Claude Code endpoint uses k3[1m] as the wire id for the 1M plan;
|
||||
// normalizing it to k3 would lose the server-side context entitlement signal.
|
||||
const KIMI_K3_MODEL_IDS = ["k3", "k3[1m]"] as const;
|
||||
const KIMI_LEGACY_MODEL_IDS = ["kimi-code", "k2p5"] as const;
|
||||
const KIMI_CODING_DEFAULT_CONTEXT_WINDOW = 262144;
|
||||
const KIMI_CODING_DEFAULT_MAX_TOKENS = 32768;
|
||||
@@ -35,6 +38,24 @@ export function buildKimiCodingProvider(): ModelProviderConfig {
|
||||
contextWindow: KIMI_CODING_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: KIMI_CODING_DEFAULT_MAX_TOKENS,
|
||||
},
|
||||
...KIMI_K3_MODEL_IDS.map((id) => ({
|
||||
id,
|
||||
name: id === "k3" ? "Kimi K3" : "Kimi K3 (1M)",
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {
|
||||
off: null,
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
high: null,
|
||||
xhigh: "max" as const,
|
||||
max: "max" as const,
|
||||
},
|
||||
input: [...KIMI_CODING_INPUT],
|
||||
cost: KIMI_CODING_DEFAULT_COST,
|
||||
contextWindow: id === "k3" ? KIMI_CODING_DEFAULT_CONTEXT_WINDOW : 1_048_576,
|
||||
maxTokens: KIMI_CODING_DEFAULT_MAX_TOKENS,
|
||||
})),
|
||||
...KIMI_LEGACY_MODEL_IDS.map((id) => ({
|
||||
id,
|
||||
name: `Kimi Code (legacy ${id})`,
|
||||
@@ -54,6 +75,10 @@ export function normalizeKimiCodingModelId(modelId: string): string {
|
||||
: modelId;
|
||||
}
|
||||
|
||||
export function isKimiK3ModelId(modelId: string): boolean {
|
||||
return KIMI_K3_MODEL_IDS.includes(modelId as (typeof KIMI_K3_MODEL_IDS)[number]);
|
||||
}
|
||||
|
||||
export const KIMI_CODING_BASE_URL = KIMI_BASE_URL;
|
||||
export const KIMI_CODING_DEFAULT_MODEL_ID = KIMI_DEFAULT_MODEL_ID;
|
||||
export const KIMI_CODING_LEGACY_MODEL_IDS = KIMI_LEGACY_MODEL_IDS;
|
||||
|
||||
@@ -268,6 +268,39 @@ describe("kimi tool-call markup wrapper", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["k3", "k3[1m]"])("forces %s adaptive max thinking", (modelId) => {
|
||||
const { streamFn: baseStreamFn, getCapturedPayload } = createPayloadCapturingStream({
|
||||
thinking: { type: "disabled", budget_tokens: 8192 },
|
||||
output_config: { effort: "low", format: { type: "json_schema" } },
|
||||
reasoning: { effort: "low" },
|
||||
reasoning_effort: "low",
|
||||
reasoningEffort: "low",
|
||||
});
|
||||
|
||||
const wrapped = wrapKimiProviderStream({
|
||||
provider: "kimi",
|
||||
modelId,
|
||||
extraParams: { thinking: "off" },
|
||||
thinkingLevel: "off",
|
||||
streamFn: baseStreamFn,
|
||||
} as never);
|
||||
|
||||
void wrapped(
|
||||
{
|
||||
api: "anthropic-messages",
|
||||
provider: "kimi",
|
||||
id: modelId,
|
||||
} as Model<"anthropic-messages">,
|
||||
KIMI_CONTEXT,
|
||||
{},
|
||||
);
|
||||
|
||||
expect(getCapturedPayload()).toEqual({
|
||||
thinking: { type: "adaptive" },
|
||||
output_config: { effort: "max", format: { type: "json_schema" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("strips Anthropic cache_control markers before Kimi requests are sent", () => {
|
||||
const { streamFn: baseStreamFn, getCapturedPayload } = createPayloadCapturingStream({
|
||||
system: [{ type: "text", text: "stable", cache_control: { type: "ephemeral", ttl: "1h" } }],
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { streamWithPayloadPatch } from "openclaw/plugin-sdk/provider-stream-shared";
|
||||
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { isKimiK3ModelId } from "./provider-catalog.js";
|
||||
|
||||
const TOOL_CALLS_SECTION_BEGIN = "<|tool_calls_section_begin|>";
|
||||
const TOOL_CALLS_SECTION_END = "<|tool_calls_section_end|>";
|
||||
@@ -374,6 +375,22 @@ function createKimiThinkingWrapper(
|
||||
const underlying = baseStreamFn ?? streamSimple;
|
||||
return (model, context, options) =>
|
||||
streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => {
|
||||
if (model.api === "anthropic-messages" && isKimiK3ModelId(model.id)) {
|
||||
// Kimi Code K3 uses Claude's adaptive/max contract, not K2's budget toggle.
|
||||
// Forcing this after onPayload prevents stale K2 settings from disabling K3 thinking.
|
||||
payloadObj.thinking = { type: "adaptive" };
|
||||
const outputConfig = payloadObj.output_config;
|
||||
payloadObj.output_config =
|
||||
outputConfig && typeof outputConfig === "object" && !Array.isArray(outputConfig)
|
||||
? { ...outputConfig, effort: "max" }
|
||||
: { effort: "max" };
|
||||
delete payloadObj.reasoning;
|
||||
delete payloadObj.reasoning_effort;
|
||||
delete payloadObj.reasoningEffort;
|
||||
stripAnthropicCacheControlMarkers(payloadObj);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized =
|
||||
typeof thinkingConfig === "string" ? { type: thinkingConfig } : thinkingConfig;
|
||||
payloadObj.thinking =
|
||||
|
||||
@@ -163,4 +163,43 @@ describe("moonshot provider plugin", () => {
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("exposes Kimi K3 as an always-max-thinking modern model", async () => {
|
||||
const provider = await registerSingleProviderPlugin(plugin);
|
||||
const capturedStream = createCapturedThinkingConfigStream();
|
||||
|
||||
const wrapped = provider.wrapSimpleCompletionStreamFn?.({
|
||||
provider: "moonshot",
|
||||
modelId: "kimi-k3",
|
||||
thinkingLevel: "off",
|
||||
streamFn: capturedStream.streamFn,
|
||||
} as never);
|
||||
|
||||
void wrapped?.(
|
||||
{
|
||||
api: "openai-completions",
|
||||
provider: "moonshot",
|
||||
id: "kimi-k3",
|
||||
} as Model<"openai-completions">,
|
||||
{ messages: [] } as Context,
|
||||
{},
|
||||
);
|
||||
|
||||
expect(capturedStream.getCapturedPayload()).toEqual({
|
||||
config: { thinkingConfig: { thinkingBudget: -1 } },
|
||||
reasoning_effort: "max",
|
||||
});
|
||||
expect(
|
||||
provider.resolveThinkingProfile?.({
|
||||
provider: "moonshot",
|
||||
modelId: "kimi-k3",
|
||||
reasoning: true,
|
||||
} as never),
|
||||
).toEqual({
|
||||
levels: [{ id: "max", label: "max" }],
|
||||
defaultLevel: "max",
|
||||
preserveWhenCatalogReasoningFalse: true,
|
||||
});
|
||||
expect(provider.isModernModelRef?.({ provider: "moonshot", modelId: "kimi-k3" })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
MOONSHOT_DEFAULT_MODEL_REF,
|
||||
} from "./onboard.js";
|
||||
import { buildMoonshotProvider } from "./provider-catalog.js";
|
||||
import { KIMI_K2_7_CODE_MODEL_ID, resolveThinkingProfile } from "./provider-policy-api.js";
|
||||
import { isMoonshotAlwaysThinkingModelId, resolveThinkingProfile } from "./provider-policy-api.js";
|
||||
import { createKimiWebSearchProvider } from "./src/kimi-web-search-provider.js";
|
||||
|
||||
const PROVIDER_ID = "moonshot";
|
||||
@@ -70,11 +70,11 @@ export default defineSingleProviderPluginEntry({
|
||||
}),
|
||||
...moonshotThinkingStreamHooks,
|
||||
wrapSimpleCompletionStreamFn: (ctx) =>
|
||||
ctx.modelId.trim().toLowerCase() === KIMI_K2_7_CODE_MODEL_ID
|
||||
isMoonshotAlwaysThinkingModelId(ctx.modelId)
|
||||
? moonshotThinkingStreamHooks.wrapStreamFn?.(ctx)
|
||||
: ctx.streamFn,
|
||||
resolveThinkingProfile,
|
||||
isModernModelRef: ({ modelId }) => modelId.trim().toLowerCase() === KIMI_K2_7_CODE_MODEL_ID,
|
||||
isModernModelRef: ({ modelId }) => isMoonshotAlwaysThinkingModelId(modelId),
|
||||
},
|
||||
register(api) {
|
||||
api.registerMediaUnderstandingProvider(moonshotMediaUnderstandingProvider);
|
||||
|
||||
@@ -349,6 +349,96 @@ describeModelLive("moonshot K2.7 Code live", () => {
|
||||
}, 180_000);
|
||||
});
|
||||
|
||||
describeModelLive("moonshot K3 live", () => {
|
||||
it("forces max reasoning and completes a required tool call", async () => {
|
||||
const provider = await registerSingleProviderPlugin(plugin);
|
||||
const wrappedStream = provider.wrapStreamFn?.({
|
||||
provider: "moonshot",
|
||||
modelId: "kimi-k3",
|
||||
thinkingLevel: "off",
|
||||
extraParams: { thinking: { type: "disabled", keep: "all" } },
|
||||
streamFn: streamSimple,
|
||||
} as never);
|
||||
if (!wrappedStream) {
|
||||
throw new Error("Moonshot provider did not register a stream wrapper");
|
||||
}
|
||||
|
||||
const runScenario = async (model: Model<"openai-completions">, liveValue: string) => {
|
||||
let capturedPayload: Record<string, unknown> | undefined;
|
||||
const response = await collectDoneMessage(
|
||||
wrappedStream(
|
||||
model,
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "Call the noop tool with {}. Do not answer directly.",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
tools: [createNoopTool()],
|
||||
},
|
||||
{
|
||||
apiKey: liveValue,
|
||||
maxTokens: 4096,
|
||||
onPayload: (value) => {
|
||||
const payload = value as Record<string, unknown>;
|
||||
capturedPayload = payload;
|
||||
payload.thinking = { type: "disabled", keep: "all" };
|
||||
payload.reasoning_effort = "low";
|
||||
payload.reasoningEffort = "low";
|
||||
payload.temperature = 0;
|
||||
payload.top_p = 0.5;
|
||||
payload.n = 2;
|
||||
payload.presence_penalty = 1;
|
||||
payload.frequency_penalty = 1;
|
||||
payload.tool_choice = "required";
|
||||
},
|
||||
},
|
||||
) as AsyncIterable<{
|
||||
type: string;
|
||||
message?: AssistantMessage;
|
||||
error?: AssistantMessage;
|
||||
}>,
|
||||
);
|
||||
|
||||
expect(capturedPayload).toBeDefined();
|
||||
expect(capturedPayload).not.toHaveProperty("thinking");
|
||||
expect(capturedPayload).not.toHaveProperty("reasoningEffort");
|
||||
expect(capturedPayload?.reasoning_effort).toBe("max");
|
||||
expect(capturedPayload?.tool_choice).toBe("required");
|
||||
expect(capturedPayload).not.toHaveProperty("temperature");
|
||||
expect(capturedPayload).not.toHaveProperty("top_p");
|
||||
expect(capturedPayload).not.toHaveProperty("n");
|
||||
expect(capturedPayload).not.toHaveProperty("presence_penalty");
|
||||
expect(capturedPayload).not.toHaveProperty("frequency_penalty");
|
||||
const reasoning = response.content.find((block) => block.type === "thinking");
|
||||
if (!reasoning || reasoning.type !== "thinking" || reasoning.thinking.length === 0) {
|
||||
throw new Error("Moonshot K3 did not return captured reasoning");
|
||||
}
|
||||
const toolCall = response.content.find((block) => block.type === "toolCall");
|
||||
if (!toolCall || toolCall.type !== "toolCall") {
|
||||
throw new Error(`Moonshot K3 did not call noop: ${response.stopReason}`);
|
||||
}
|
||||
expect(toolCall.name).toBe("noop");
|
||||
};
|
||||
|
||||
let lastAuthError: unknown;
|
||||
for (const model of resolveMoonshotModels("kimi-k3")) {
|
||||
try {
|
||||
await runScenario(model, MOONSHOT_API_KEY);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (!isMoonshotAuthDrift(error)) {
|
||||
throw error;
|
||||
}
|
||||
lastAuthError = error;
|
||||
}
|
||||
}
|
||||
throw toLintErrorObject(lastAuthError, "Moonshot K3 rejected the API key in both regions");
|
||||
}, 180_000);
|
||||
});
|
||||
|
||||
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
|
||||
if (value instanceof Error) {
|
||||
return value;
|
||||
|
||||
@@ -63,6 +63,33 @@
|
||||
"cacheWrite": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "kimi-k3",
|
||||
"name": "Kimi K3",
|
||||
"reasoning": true,
|
||||
"thinkingLevelMap": {
|
||||
"off": null,
|
||||
"minimal": null,
|
||||
"low": null,
|
||||
"medium": null,
|
||||
"high": null,
|
||||
"xhigh": "max",
|
||||
"max": "max"
|
||||
},
|
||||
"input": ["text", "image"],
|
||||
"contextWindow": 1048576,
|
||||
"maxTokens": 1048576,
|
||||
"cost": {
|
||||
"input": 3,
|
||||
"output": 15,
|
||||
"cacheRead": 0.3,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"compat": {
|
||||
"supportsReasoningEffort": true,
|
||||
"supportedReasoningEfforts": ["max"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "kimi-k2.7-code",
|
||||
"name": "Kimi K2.7 Code",
|
||||
|
||||
@@ -41,6 +41,7 @@ describe("moonshot provider catalog", () => {
|
||||
expect(provider.api).toBe("openai-completions");
|
||||
expect(provider.models.map((model) => model.id)).toEqual([
|
||||
"kimi-k2.6",
|
||||
"kimi-k3",
|
||||
"kimi-k2.7-code",
|
||||
"kimi-k2.5",
|
||||
"kimi-k2-thinking",
|
||||
@@ -53,6 +54,31 @@ describe("moonshot provider catalog", () => {
|
||||
cacheRead: 0.16,
|
||||
cacheWrite: 0,
|
||||
});
|
||||
expect(requireMoonshotModel(provider, "kimi-k3")).toMatchObject({
|
||||
reasoning: true,
|
||||
thinkingLevelMap: {
|
||||
off: null,
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
high: null,
|
||||
xhigh: "max",
|
||||
max: "max",
|
||||
},
|
||||
input: ["text", "image"],
|
||||
contextWindow: 1_048_576,
|
||||
maxTokens: 1_048_576,
|
||||
cost: {
|
||||
input: 3,
|
||||
output: 15,
|
||||
cacheRead: 0.3,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
compat: {
|
||||
supportsReasoningEffort: true,
|
||||
supportedReasoningEfforts: ["max"],
|
||||
},
|
||||
});
|
||||
expect(requireMoonshotModel(provider, "kimi-k2.7-code")).toMatchObject({
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
|
||||
@@ -2,9 +2,23 @@
|
||||
import type { ProviderDefaultThinkingPolicyContext } from "openclaw/plugin-sdk/core";
|
||||
|
||||
export const KIMI_K2_7_CODE_MODEL_ID = "kimi-k2.7-code";
|
||||
export const KIMI_K3_MODEL_ID = "kimi-k3";
|
||||
|
||||
export function isMoonshotAlwaysThinkingModelId(modelId: string): boolean {
|
||||
const normalized = modelId.trim().toLowerCase();
|
||||
return normalized === KIMI_K2_7_CODE_MODEL_ID || normalized === KIMI_K3_MODEL_ID;
|
||||
}
|
||||
|
||||
export function resolveThinkingProfile(context: ProviderDefaultThinkingPolicyContext) {
|
||||
if (context.modelId.trim().toLowerCase() === KIMI_K2_7_CODE_MODEL_ID) {
|
||||
const modelId = context.modelId.trim().toLowerCase();
|
||||
if (modelId === KIMI_K3_MODEL_ID) {
|
||||
return {
|
||||
levels: [{ id: "max" as const, label: "max" }],
|
||||
defaultLevel: "max" as const,
|
||||
preserveWhenCatalogReasoningFalse: true,
|
||||
};
|
||||
}
|
||||
if (modelId === KIMI_K2_7_CODE_MODEL_ID) {
|
||||
return {
|
||||
levels: [{ id: "low" as const, label: "on" }],
|
||||
defaultLevel: "low" as const,
|
||||
|
||||
@@ -233,6 +233,62 @@ describe("applyExtraParamsToAgent Moonshot", () => {
|
||||
expect(expectDefined(messages[0], "messages[0] test invariant").reasoning_content).toBe("");
|
||||
});
|
||||
|
||||
it("forces Kimi K3 max reasoning while preserving pinned tool choice", () => {
|
||||
const pinnedToolChoice = { type: "function", function: { name: "read" } };
|
||||
const payload = runExtraParamsPayloadCase({
|
||||
provider: "moonshot",
|
||||
modelId: "kimi-k3",
|
||||
thinkingLevel: "off",
|
||||
payload: {
|
||||
model: "kimi-k3",
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
tool_calls: [
|
||||
{ id: "call_1", type: "function", function: { name: "read", arguments: "{}" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"moonshot/kimi-k3": {
|
||||
params: {
|
||||
thinking: { type: "disabled", keep: "all" },
|
||||
extra_body: {
|
||||
thinking: { type: "disabled", keep: "all" },
|
||||
reasoningEffort: "low",
|
||||
reasoning_effort: "low",
|
||||
tool_choice: pinnedToolChoice,
|
||||
temperature: 0,
|
||||
top_p: 0.5,
|
||||
n: 2,
|
||||
presence_penalty: 1,
|
||||
frequency_penalty: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload).not.toHaveProperty("thinking");
|
||||
expect(payload).not.toHaveProperty("reasoningEffort");
|
||||
expect(payload.reasoning_effort).toBe("max");
|
||||
expect(payload.tool_choice).toEqual(pinnedToolChoice);
|
||||
expect(payload).not.toHaveProperty("temperature");
|
||||
expect(payload).not.toHaveProperty("top_p");
|
||||
expect(payload).not.toHaveProperty("n");
|
||||
expect(payload).not.toHaveProperty("presence_penalty");
|
||||
expect(payload).not.toHaveProperty("frequency_penalty");
|
||||
const messages = payload.messages as Array<Record<string, unknown>>;
|
||||
expect(expectDefined(messages[0], "messages[0] test invariant").reasoning_content).toBe("");
|
||||
});
|
||||
|
||||
it("repairs only missing assistant tool-call reasoning_content when thinking is enabled", () => {
|
||||
const payload = runExtraParamsPayloadCase({
|
||||
provider: "moonshot",
|
||||
|
||||
@@ -113,10 +113,10 @@ describe("resolveHighSignalLiveModelLimit", () => {
|
||||
});
|
||||
|
||||
describe("live model priorities", () => {
|
||||
it("includes the always-thinking Moonshot K2.7 Code route", () => {
|
||||
it("includes the always-thinking Moonshot K3 route", () => {
|
||||
expect(listPrioritizedHighSignalLiveModelRefs()).toContainEqual({
|
||||
provider: "moonshot",
|
||||
id: "kimi-k2.7-code",
|
||||
id: "kimi-k3",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ const HIGH_SIGNAL_LIVE_MODEL_PRIORITY = [
|
||||
"google/gemini-3.1-pro-preview",
|
||||
"google/gemini-3.5-flash",
|
||||
"cohere/command-a-plus-05-2026",
|
||||
"moonshot/kimi-k2.7-code",
|
||||
"moonshot/kimi-k3",
|
||||
"anthropic/claude-opus-4-6",
|
||||
"deepseek/deepseek-v4-flash",
|
||||
"deepseek/deepseek-v4-pro",
|
||||
|
||||
@@ -689,7 +689,7 @@ describe("isPrioritizedHighSignalLiveModelRef", () => {
|
||||
{ provider: "google", id: "gemini-3.1-pro-preview" },
|
||||
{ provider: "google", id: "gemini-3.5-flash" },
|
||||
{ provider: "cohere", id: "command-a-plus-05-2026" },
|
||||
{ provider: "moonshot", id: "kimi-k2.7-code" },
|
||||
{ provider: "moonshot", id: "kimi-k3" },
|
||||
{ provider: "anthropic", id: "claude-opus-4-6" },
|
||||
{ provider: "deepseek", id: "deepseek-v4-flash" },
|
||||
{ provider: "deepseek", id: "deepseek-v4-pro" },
|
||||
|
||||
@@ -1555,6 +1555,7 @@ const REASONING_CONTENT_REPLAY_MODEL_IDS = new Set([
|
||||
"kimi-k2.5",
|
||||
"kimi-k2.6",
|
||||
"kimi-k2.7-code",
|
||||
"kimi-k3",
|
||||
"kimi-k2-thinking",
|
||||
"kimi-k2-thinking-turbo",
|
||||
"mimo-v2-pro",
|
||||
|
||||
@@ -181,6 +181,11 @@ describe("buildOpenAICompletionsParams sanitizes reasoning replay fields", () =>
|
||||
model: staleKimiK27Model,
|
||||
assertSanitizedFields: true,
|
||||
},
|
||||
{
|
||||
label: "preserves Kimi K3 reasoning_content replay with stale reasoning metadata",
|
||||
model: { ...staleKimiK27Model, id: "kimi-k3", name: "Kimi K3" },
|
||||
assertSanitizedFields: true,
|
||||
},
|
||||
{
|
||||
label: "preserves reasoning_content replay for Kimi Coding OpenAI-compatible routes",
|
||||
model: kimiCodingProxyModel,
|
||||
|
||||
@@ -395,6 +395,7 @@ describe("resolveTranscriptPolicy", () => {
|
||||
"kimi-for-coding",
|
||||
"moonshotai/kimi-k2.6",
|
||||
"moonshot/kimi-k2.7-code",
|
||||
"moonshot/kimi-k3",
|
||||
"kimi-k2-thinking",
|
||||
"hf:moonshotai/kimi-k2-thinking",
|
||||
"xiaomi/mimo-v2.6-pro",
|
||||
|
||||
@@ -187,6 +187,7 @@ const REASONING_CONTENT_REPLAY_MODEL_IDS = new Set([
|
||||
"kimi-k2.5",
|
||||
"kimi-k2.6",
|
||||
"kimi-k2.7-code",
|
||||
"kimi-k3",
|
||||
"kimi-k2-thinking",
|
||||
"kimi-k2-thinking-turbo",
|
||||
"mimo-v2-pro",
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("loadProviderIndexCatalogRowsForList", () => {
|
||||
cfg: baseConfig,
|
||||
providerFilter: "moonshot",
|
||||
}).map((row) => row.ref),
|
||||
).toEqual(["moonshot/kimi-k2.6", "moonshot/kimi-k2.7-code"]);
|
||||
).toEqual(["moonshot/kimi-k2.6", "moonshot/kimi-k2.7-code", "moonshot/kimi-k3"]);
|
||||
});
|
||||
|
||||
it("returns all enabled provider-index preview rows without a provider filter", () => {
|
||||
@@ -26,6 +26,7 @@ describe("loadProviderIndexCatalogRowsForList", () => {
|
||||
"deepseek/deepseek-v4-pro",
|
||||
"moonshot/kimi-k2.6",
|
||||
"moonshot/kimi-k2.7-code",
|
||||
"moonshot/kimi-k3",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ import { createLazyImportLoader } from "../../../shared/lazy-promise.js";
|
||||
type MoonshotThinkingType = "enabled" | "disabled";
|
||||
type MoonshotThinkingKeep = "all";
|
||||
const MOONSHOT_THINKING_KEEP_MODEL_ID = "kimi-k2.6";
|
||||
const MOONSHOT_ALWAYS_THINKING_MODEL_ID = "kimi-k2.7-code";
|
||||
const MOONSHOT_PROVIDER_ID = "moonshot";
|
||||
const MOONSHOT_K2_7_CODE_MODEL_ID = "kimi-k2.7-code";
|
||||
const MOONSHOT_K3_MODEL_ID = "kimi-k3";
|
||||
const MOONSHOT_FIXED_SAMPLING_FIELDS = [
|
||||
"temperature",
|
||||
"top_p",
|
||||
@@ -16,6 +18,7 @@ const MOONSHOT_FIXED_SAMPLING_FIELDS = [
|
||||
"frequency_penalty",
|
||||
] as const;
|
||||
const llmRuntimeLoader = createLazyImportLoader(() => import("openclaw/plugin-sdk/llm"));
|
||||
type MoonshotAlwaysThinkingModel = "kimi-k2.7-code" | "kimi-k3";
|
||||
|
||||
async function loadDefaultStreamFn(): Promise<StreamFn> {
|
||||
const runtime = await llmRuntimeLoader.load();
|
||||
@@ -110,12 +113,33 @@ function sanitizeKimiK27Payload(payloadObj: Record<string, unknown>): void {
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeKimiK27AfterCaller(
|
||||
function sanitizeKimiK3Payload(payloadObj: Record<string, unknown>): void {
|
||||
delete payloadObj.thinking;
|
||||
delete payloadObj.reasoningEffort;
|
||||
payloadObj.reasoning_effort = "max";
|
||||
for (const field of MOONSHOT_FIXED_SAMPLING_FIELDS) {
|
||||
delete payloadObj[field];
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeAlwaysThinkingPayload(
|
||||
payloadObj: Record<string, unknown>,
|
||||
modelId: MoonshotAlwaysThinkingModel,
|
||||
): void {
|
||||
if (modelId === MOONSHOT_K3_MODEL_ID) {
|
||||
sanitizeKimiK3Payload(payloadObj);
|
||||
} else {
|
||||
sanitizeKimiK27Payload(payloadObj);
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeAlwaysThinkingAfterCaller(
|
||||
value: unknown,
|
||||
fallbackPayload: Record<string, unknown>,
|
||||
modelId: MoonshotAlwaysThinkingModel,
|
||||
): unknown {
|
||||
const finalPayload = asPayloadRecord(value) ?? fallbackPayload;
|
||||
sanitizeKimiK27Payload(finalPayload);
|
||||
sanitizeAlwaysThinkingPayload(finalPayload, modelId);
|
||||
ensureMoonshotToolCallReasoningContent(finalPayload);
|
||||
return value;
|
||||
}
|
||||
@@ -163,9 +187,22 @@ export function createMoonshotThinkingWrapper(
|
||||
(underlying: StreamFn): StreamFn =>
|
||||
(model, context, options) => {
|
||||
const modelId = model.id.trim().toLowerCase();
|
||||
const isKimiK27 = modelId === MOONSHOT_ALWAYS_THINKING_MODEL_ID;
|
||||
const streamModel = isKimiK27 ? { ...model, reasoning: true } : model;
|
||||
const streamOptions = isKimiK27 ? { ...options, reasoning: "low" as const } : options;
|
||||
const directMoonshotModel =
|
||||
normalizeOptionalLowercaseString(model.provider) === MOONSHOT_PROVIDER_ID;
|
||||
const alwaysThinkingModel =
|
||||
modelId === MOONSHOT_K2_7_CODE_MODEL_ID
|
||||
? MOONSHOT_K2_7_CODE_MODEL_ID
|
||||
: directMoonshotModel && modelId === MOONSHOT_K3_MODEL_ID
|
||||
? MOONSHOT_K3_MODEL_ID
|
||||
: undefined;
|
||||
const streamModel = alwaysThinkingModel ? { ...model, reasoning: true } : model;
|
||||
const streamOptions = alwaysThinkingModel
|
||||
? {
|
||||
...options,
|
||||
reasoning:
|
||||
alwaysThinkingModel === MOONSHOT_K3_MODEL_ID ? ("max" as const) : ("low" as const),
|
||||
}
|
||||
: options;
|
||||
const originalOnPayload = streamOptions?.onPayload;
|
||||
return underlying(streamModel, context, {
|
||||
...streamOptions,
|
||||
@@ -183,17 +220,27 @@ export function createMoonshotThinkingWrapper(
|
||||
effectiveThinkingType = thinkingType;
|
||||
}
|
||||
|
||||
if (payloadModelId === MOONSHOT_ALWAYS_THINKING_MODEL_ID) {
|
||||
// K2.7 Code always reasons, preserves reasoning, and fixes sampling.
|
||||
// Reapply constraints after caller hooks so extra_body cannot restore them.
|
||||
sanitizeKimiK27Payload(payloadObj);
|
||||
const payloadAlwaysThinkingModel =
|
||||
payloadModelId === MOONSHOT_K2_7_CODE_MODEL_ID
|
||||
? MOONSHOT_K2_7_CODE_MODEL_ID
|
||||
: directMoonshotModel && payloadModelId === MOONSHOT_K3_MODEL_ID
|
||||
? MOONSHOT_K3_MODEL_ID
|
||||
: undefined;
|
||||
if (payloadAlwaysThinkingModel) {
|
||||
// These models fix their reasoning and sampling contract. Reapply it
|
||||
// after caller hooks so extra_body cannot restore rejected fields.
|
||||
sanitizeAlwaysThinkingPayload(payloadObj, payloadAlwaysThinkingModel);
|
||||
const result = originalOnPayload?.(payload, payloadModel);
|
||||
if (result && typeof (result as Promise<unknown>).then === "function") {
|
||||
return Promise.resolve(result).then((resolved) =>
|
||||
sanitizeKimiK27AfterCaller(resolved, payloadObj),
|
||||
sanitizeAlwaysThinkingAfterCaller(resolved, payloadObj, payloadAlwaysThinkingModel),
|
||||
);
|
||||
}
|
||||
return sanitizeKimiK27AfterCaller(result, payloadObj);
|
||||
return sanitizeAlwaysThinkingAfterCaller(
|
||||
result,
|
||||
payloadObj,
|
||||
payloadAlwaysThinkingModel,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -150,6 +150,15 @@ describe("OpenClaw provider index", () => {
|
||||
(model) => model.id === "kimi-k2.6",
|
||||
);
|
||||
expect(kimi?.status).toBe("preview");
|
||||
const kimiK3 = index.providers.moonshot?.previewCatalog?.models.find(
|
||||
(model) => model.id === "kimi-k3",
|
||||
);
|
||||
expect(kimiK3).toMatchObject({
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 1048576,
|
||||
status: "preview",
|
||||
});
|
||||
const kimiCode = index.providers.moonshot?.previewCatalog?.models.find(
|
||||
(model) => model.id === "kimi-k2.7-code",
|
||||
);
|
||||
|
||||
@@ -29,6 +29,13 @@ export const OPENCLAW_PROVIDER_INDEX = {
|
||||
input: ["text", "image"],
|
||||
contextWindow: 262144,
|
||||
},
|
||||
{
|
||||
id: "kimi-k3",
|
||||
name: "Kimi K3",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
contextWindow: 1048576,
|
||||
},
|
||||
{
|
||||
id: "kimi-k2.7-code",
|
||||
name: "Kimi K2.7 Code",
|
||||
|
||||
@@ -91,6 +91,65 @@ describe("createMoonshotThinkingWrapper", () => {
|
||||
expect(payload).not.toHaveProperty("top_p");
|
||||
expect(payload.tool_choice).toBe("auto");
|
||||
});
|
||||
|
||||
it("forces the direct Moonshot K3 payload contract after async caller replacement", async () => {
|
||||
let finalPayload: Record<string, unknown> | undefined;
|
||||
const pinnedToolChoice = { type: "function", function: { name: "read" } };
|
||||
const baseStreamFn: StreamFn = async (model, _context, options) => {
|
||||
const payload = { model: model.id };
|
||||
const replacement = await options?.onPayload?.(payload, model);
|
||||
finalPayload = requireRecord(replacement ?? payload, "final payload");
|
||||
return {} as never;
|
||||
};
|
||||
const wrapped = createMoonshotThinkingWrapper(baseStreamFn, "disabled", "all");
|
||||
|
||||
await wrapped(
|
||||
{ api: "openai-completions", provider: "moonshot", id: "kimi-k3" } as never,
|
||||
{} as never,
|
||||
{
|
||||
onPayload: async () => ({
|
||||
model: "kimi-k3",
|
||||
thinking: { type: "disabled" },
|
||||
reasoningEffort: "low",
|
||||
reasoning_effort: "low",
|
||||
temperature: 0,
|
||||
top_p: 0.5,
|
||||
tool_choice: pinnedToolChoice,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const payload = requirePayload(finalPayload);
|
||||
expect(payload).not.toHaveProperty("thinking");
|
||||
expect(payload).not.toHaveProperty("reasoningEffort");
|
||||
expect(payload.reasoning_effort).toBe("max");
|
||||
expect(payload).not.toHaveProperty("temperature");
|
||||
expect(payload).not.toHaveProperty("top_p");
|
||||
expect(payload.tool_choice).toEqual(pinnedToolChoice);
|
||||
});
|
||||
|
||||
it("does not apply the direct K3 contract to an Ollama-owned model", async () => {
|
||||
let finalPayload: Record<string, unknown> | undefined;
|
||||
const baseStreamFn: StreamFn = async (model, _context, options) => {
|
||||
const payload = { model: model.id, reasoning_effort: "low", temperature: 0 };
|
||||
const replacement = await options?.onPayload?.(payload, model);
|
||||
finalPayload = requireRecord(replacement ?? payload, "final payload");
|
||||
return {} as never;
|
||||
};
|
||||
const wrapped = createMoonshotThinkingWrapper(baseStreamFn, "enabled");
|
||||
|
||||
await wrapped(
|
||||
{ api: "openai-completions", provider: "ollama", id: "kimi-k3" } as never,
|
||||
{} as never,
|
||||
{},
|
||||
);
|
||||
|
||||
expect(requirePayload(finalPayload)).toMatchObject({
|
||||
thinking: { type: "enabled" },
|
||||
reasoning_effort: "low",
|
||||
temperature: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("composeProviderStreamWrappers", () => {
|
||||
@@ -320,6 +379,31 @@ describe("buildProviderStreamFamilyHooks", () => {
|
||||
expect(capturedReasoning).toBe("low");
|
||||
expect(capturedModelReasoning).toBe(true);
|
||||
|
||||
payloadSeed = {
|
||||
thinking: { type: "disabled" },
|
||||
tool_choice: { type: "tool", name: "read" },
|
||||
temperature: 0,
|
||||
reasoning_effort: "low",
|
||||
};
|
||||
await moonshotKeepStream(
|
||||
{
|
||||
api: "openai-completions",
|
||||
provider: "moonshot",
|
||||
id: "kimi-k3",
|
||||
reasoning: false,
|
||||
} as never,
|
||||
{} as never,
|
||||
{},
|
||||
);
|
||||
const moonshotK3Payload = requirePayload(capturedPayload);
|
||||
expectDefaultThinkingBudget(moonshotK3Payload);
|
||||
expect(moonshotK3Payload).not.toHaveProperty("thinking");
|
||||
expect(moonshotK3Payload.reasoning_effort).toBe("max");
|
||||
expect(moonshotK3Payload.tool_choice).toEqual({ type: "tool", name: "read" });
|
||||
expect(moonshotK3Payload).not.toHaveProperty("temperature");
|
||||
expect(capturedReasoning).toBe("max");
|
||||
expect(capturedModelReasoning).toBe(true);
|
||||
|
||||
const openAiHooks = OPENAI_RESPONSES_STREAM_HOOKS;
|
||||
payloadSeed = { reasoning: { effort: "medium", summary: "auto" } };
|
||||
void requireStreamFn(
|
||||
|
||||
@@ -107,7 +107,7 @@ describe("provider public artifacts", () => {
|
||||
).toBe("adaptive");
|
||||
});
|
||||
|
||||
it("loads Moonshot Kimi K2.7 thinking policy before runtime registration", () => {
|
||||
it("loads Moonshot always-thinking policies before runtime registration", () => {
|
||||
const surface = resolveBundledProviderPolicySurface("moonshot");
|
||||
|
||||
expect(
|
||||
@@ -120,6 +120,16 @@ describe("provider public artifacts", () => {
|
||||
defaultLevel: "low",
|
||||
preserveWhenCatalogReasoningFalse: true,
|
||||
});
|
||||
expect(
|
||||
surface?.resolveThinkingProfile?.({
|
||||
provider: "moonshot",
|
||||
modelId: "kimi-k3",
|
||||
}),
|
||||
).toEqual({
|
||||
levels: [{ id: "max", label: "max" }],
|
||||
defaultLevel: "max",
|
||||
preserveWhenCatalogReasoningFalse: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("loads trusted official external provider policy before runtime registration", () => {
|
||||
|
||||
Reference in New Issue
Block a user