mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-15 03:32:21 +03:00
fix(executors): inject reasoning_content for native Kimi provider (port from 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 lacks it. 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. Wire the same injection into DefaultExecutor, scoped to `kimi` so gateway-served models that merely match the thinking-model name pattern (and may reject an extra field) are unaffected. Regression guard: tests/unit/kimi-native-reasoning-injected-1480.test.ts. Reported-by: 2220258345 (https://github.com/decolua/9router/issues/1480)
This commit is contained in:
@@ -20,6 +20,8 @@
|
||||
|
||||
### 🐛 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(live-ws):** the Live Dashboard WebSocket server now **rejects on bind failure** (e.g. `EADDRINUSE` when the API bridge already holds the port) instead of letting the error surface as an unhandled `error` event that crash-loops the process — the `error` listener is attached to `wss` (not `server`) and releases the EventBus subscription on a failed start ([#6324](https://github.com/diegosouzapw/OmniRoute/issues/6324)). Regression guard: `tests/unit/live-ws-eaddrinuse-6324.test.ts`. (thanks @vinayakkulkarni)
|
||||
- **fix(dashboard):** the Home provider-topology widget now trusts the live provider-metrics snapshot — it uses `topology.errorProvider` and live `activeRequests` directly instead of re-deriving state from a stale `lastErrorAt` or applying a frontend timeout filter, so the topology reflects real-time provider health. Regression guard: `tests/unit/home-provider-topology-live-state.test.ts`. (thanks @xz-dev)
|
||||
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50).
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
65
tests/unit/kimi-native-reasoning-injected-1480.test.ts
Normal file
65
tests/unit/kimi-native-reasoning-injected-1480.test.ts
Normal 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"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user