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:
NianJiu
2026-07-16 11:41:12 -07:00
committed by GitHub
co-authored by NianJiuZst Peter Steinberger
parent e9b974e18b
commit 42ff5ec754
31 changed files with 723 additions and 73 deletions
+5 -3
View File
@@ -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
{
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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)",
+36
View File
@@ -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);
});
});
+21 -8
View File
@@ -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;
+33
View File
@@ -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" } }],
+17
View File
@@ -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 =
+39
View File
@@ -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);
});
});
+3 -3
View File
@@ -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);
+90
View File
@@ -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;
+27
View File
@@ -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"],
+15 -1
View File
@@ -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",
+2 -2
View File
@@ -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",
});
});
});
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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,
+1
View File
@@ -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",
+1
View File
@@ -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",
+84
View File
@@ -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(
+11 -1
View File
@@ -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", () => {