fix(executors): inject reasoning_content for native Kimi provider (port from 9router#1480) (#6419)

inject reasoning_content for native Kimi (port #1480) (net +1/-0, test OK). Integrated into release/v3.8.46.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-06 19:22:02 -03:00
committed by GitHub
parent 1636ace600
commit f0b085ebca
3 changed files with 87 additions and 0 deletions

View File

@@ -21,6 +21,7 @@
### 🐛 Bug Fixes
- **fix(executors):** inject the `reasoning_content` echo for the native **Moonshot Kimi** provider. Kimi (executor `default`) is a thinking-mode upstream that 400s with "reasoning_content must be passed back" when a prior assistant turn lacks it; the placeholder injection was only wired into the OpenCode meta-provider, so direct multi-turn Kimi conversations failed. Scoped to `kimi` (gateway-served models matching the thinking-model name pattern are unaffected). Regression guard: `tests/unit/kimi-native-reasoning-injected-1480.test.ts`. (thanks @2220258345)
- **fix(executors):** recover from a strict gateway's `context_management: Extra inputs are not permitted` 400. **Claude Code** always sends a top-level `context_management` field; strict anthropic-compatible gateways reject it. The dedicated context-editing 400-fallback only fired when OmniRoute's own `contextEditing` feature was enabled (default off), so a client-sent field passed through untouched and 400'd. `context_management` is now in the generic reactive field-strip list, so it's stripped-and-retried once regardless of the feature flag (with correct request re-signing for claude-compatible relays). Regression guard: `tests/unit/provider-field-strips.test.ts`. (thanks @ohahe52-dot)
- **fix(network):** enable **RFC 8305 Happy Eyeballs** (`autoSelectFamily`) on the direct-egress undici dispatcher. When DNS returns both IPv6 (AAAA) and IPv4 (A) and the IPv6 route is broken (e.g. a NAT64 `64:ff9b::` prefix without routing), undici tried IPv6 first and hung until `ETIMEDOUT` (then a 502 + account lockout), even though `curl` reached the same host. The direct dispatcher now races both families and uses whichever connects first. Proxy paths pin family via `proxyTls` and are unaffected. Regression guard: `tests/unit/direct-dispatcher-pipelining-4580.test.ts`. (thanks @adentdk)
- **fix(combo):** round-robin now advances the rotation pointer past the model that **actually served**, not the eagerly-scheduled one. With `stickyLimit: 1` (true round-robin), when the scheduled model failed and a *different* model served via fallback, the counter had already advanced +1 from the scheduled index — so the next request reused the fallback-served model, degrading round-robin into hot-spotting on whichever model was healthy. The pointer now advances to the served index + 1 (mirroring the sticky-limit>1 path). Session-stickiness (#3825) and distribution are preserved. Regression guard: `tests/unit/combo-rr-fallback-advance-948.test.ts`. (thanks @binsarjr)

View File

@@ -17,6 +17,10 @@ import {
import { isOfficialAnthropicBaseUrl } from "../utils/anthropicHost.ts";
import { applyProviderRequestDefaults } from "../services/providerRequestDefaults.ts";
import { stripUnsupportedParams } from "../translator/paramSupport.ts";
import {
injectReasoningContentForThinkingModel,
isThinkingMessageModel,
} from "../utils/reasoningContentInjector.ts";
import {
detectFormat,
getOpenAICompatibleType,
@@ -695,6 +699,23 @@ export class DefaultExecutor extends BaseExecutor {
this.ensureThinkingBudget(withDefaults as Record<string, unknown>, model);
}
// 9router#1480: the native Moonshot `kimi` provider (executor "default")
// is a thinking-mode upstream that 400s with "reasoning_content must be
// passed back" when a prior assistant turn lacks it. OpencodeExecutor
// already injects a placeholder for OpenCode-routed thinking models; the
// direct kimi connection hit neither injection path. Scope to `kimi` so
// gateway-served models that merely match the thinking-model name pattern
// (and may reject an extra field) are unaffected.
if (this.provider === "kimi") {
const outboundModel =
typeof (withDefaults as Record<string, unknown>)?.model === "string"
? ((withDefaults as Record<string, unknown>).model as string)
: model;
if (isThinkingMessageModel(outboundModel)) {
withDefaults = injectReasoningContentForThinkingModel(withDefaults);
}
}
return withDefaults;
}

View File

@@ -0,0 +1,65 @@
import test from "node:test";
import assert from "node:assert/strict";
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
/**
* Regression guard for upstream 9router#1480.
*
* The native Moonshot `kimi` provider (executor "default") is a thinking-mode
* upstream that returns 400 "reasoning_content must be passed back" when a prior
* assistant turn in the history lacks `reasoning_content`. OpencodeExecutor
* already injects a placeholder for OpenCode-routed thinking models, but the
* direct kimi connection went through DefaultExecutor, which did not — so
* multi-turn kimi conversations 400'd. The injection must fire for `kimi`, and
* must NOT fire for unrelated providers that merely serve a matching model name.
*/
const STREAM = true;
const CREDENTIALS = { apiKey: "k" } as Record<string, unknown>;
function multiTurnBody(model: string) {
return {
model,
stream: STREAM,
messages: [
{ role: "user", content: "hi" },
{ role: "assistant", content: "previous answer" }, // no reasoning_content
{ role: "user", content: "follow up" },
],
};
}
test("DefaultExecutor(kimi) injects reasoning_content on assistant turns that lack it", () => {
const out = new DefaultExecutor("kimi").transformRequest(
"kimi-k2.6",
multiTurnBody("kimi-k2.6"),
STREAM,
CREDENTIALS
) as Record<string, unknown>;
const messages = out.messages as Array<Record<string, unknown>>;
const assistant = messages.find((m) => m.role === "assistant") as Record<string, unknown>;
assert.equal(
typeof assistant.reasoning_content === "string" &&
(assistant.reasoning_content as string).length > 0,
true,
"kimi assistant message must carry a non-empty reasoning_content placeholder"
);
});
test("DefaultExecutor(openai) does NOT inject reasoning_content (scoped to kimi)", () => {
// A non-kimi provider must not gain the injection even for a thinking-ish name.
const out = new DefaultExecutor("openai").transformRequest(
"kimi-k2.6",
multiTurnBody("kimi-k2.6"),
STREAM,
CREDENTIALS
) as Record<string, unknown>;
const messages = out.messages as Array<Record<string, unknown>>;
const assistant = messages.find((m) => m.role === "assistant") as Record<string, unknown>;
assert.equal(
Object.prototype.hasOwnProperty.call(assistant, "reasoning_content"),
false,
"non-kimi providers must not be given a reasoning_content placeholder"
);
});