mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(moonshot): preserve native Kimi tool_call IDs in openai-completions replay
This commit is contained in:
committed by
Peter Steinberger
parent
23a448986f
commit
c4dea58712
@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Providers/Moonshot: stop strict-sanitizing Kimi's native tool_call IDs (shaped like `functions.<name>:<index>`) on the OpenAI-compatible transport, so multi-turn agentic flows through Kimi K2.6 no longer break after 2-3 tool-calling rounds when the serving layer fails to match mangled IDs against the original tool definitions. Adds a `sanitizeToolCallIds` opt-out to the shared `openai-compatible` replay family helper and wires Moonshot to it. Fixes #62319. (#70030) Thanks @LeoDu0314.
|
||||
- Codex harness: ignore dynamic tool descriptions when deciding whether to reuse a native app-server thread while still fingerprinting tool schemas, so channel-specific copy changes no longer reset otherwise compatible Codex conversations. (#69976) Thanks @chen-zhang-cs-code.
|
||||
- Codex harness: drop invalid legacy app-server `serviceTier` values such as `"priority"` before native thread and turn requests, while keeping supported Codex tiers limited to `"fast"` and `"flex"`. Fixes #64815.
|
||||
- Codex harness: show bounded, sanitized permission target samples in app-server approval prompts, so native permission requests keep their specific hosts, roots, and paths visible without leaking home usernames or URL credentials. (#70340) Thanks @Lucenx9.
|
||||
|
||||
@@ -5,22 +5,22 @@ import { createCapturedThinkingConfigStream } from "../../test/helpers/plugins/s
|
||||
import plugin from "./index.js";
|
||||
|
||||
describe("moonshot provider plugin", () => {
|
||||
it("owns replay policy for OpenAI-compatible Moonshot transports", async () => {
|
||||
it("owns replay policy for OpenAI-compatible Moonshot transports without mangling native Kimi tool_call IDs", async () => {
|
||||
const provider = await registerSingleProviderPlugin(plugin);
|
||||
|
||||
expect(
|
||||
provider.buildReplayPolicy?.({
|
||||
provider: "moonshot",
|
||||
modelApi: "openai-completions",
|
||||
modelId: "kimi-k2.6",
|
||||
} as never),
|
||||
).toMatchObject({
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
const policy = provider.buildReplayPolicy?.({
|
||||
provider: "moonshot",
|
||||
modelApi: "openai-completions",
|
||||
modelId: "kimi-k2.6",
|
||||
} as never);
|
||||
|
||||
expect(policy).toMatchObject({
|
||||
applyAssistantFirstOrderingFix: true,
|
||||
validateGeminiTurns: true,
|
||||
validateAnthropicTurns: true,
|
||||
});
|
||||
expect(policy).not.toHaveProperty("sanitizeToolCallIds");
|
||||
expect(policy).not.toHaveProperty("toolCallIdMode");
|
||||
});
|
||||
|
||||
it("wires moonshot-thinking stream hooks", async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
|
||||
import { OPENAI_COMPATIBLE_REPLAY_HOOKS } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { MOONSHOT_THINKING_STREAM_HOOKS } from "openclaw/plugin-sdk/provider-stream-family";
|
||||
import { applyMoonshotNativeStreamingUsageCompat } from "./api.js";
|
||||
import { moonshotMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
@@ -57,7 +57,13 @@ export default defineSingleProviderPluginEntry({
|
||||
},
|
||||
applyNativeStreamingUsageCompat: ({ providerConfig }) =>
|
||||
applyMoonshotNativeStreamingUsageCompat(providerConfig),
|
||||
...OPENAI_COMPATIBLE_REPLAY_HOOKS,
|
||||
// Kimi K2+ returns native tool_call IDs shaped like `functions.<name>:<index>`.
|
||||
// Sanitizing them to alphanumeric-only breaks Kimi's serving-layer matching in
|
||||
// multi-turn replay. See openclaw/openclaw#62319.
|
||||
...buildProviderReplayFamilyHooks({
|
||||
family: "openai-compatible",
|
||||
sanitizeToolCallIds: false,
|
||||
}),
|
||||
...MOONSHOT_THINKING_STREAM_HOOKS,
|
||||
resolveThinkingProfile: () => ({
|
||||
levels: [
|
||||
|
||||
@@ -191,6 +191,23 @@ describe("buildProviderReplayFamilyHooks", () => {
|
||||
validateGeminiTurns: true,
|
||||
});
|
||||
|
||||
const nativeIdsHooks = buildProviderReplayFamilyHooks({
|
||||
family: "openai-compatible",
|
||||
sanitizeToolCallIds: false,
|
||||
});
|
||||
const nativeIdsPolicy = nativeIdsHooks.buildReplayPolicy?.({
|
||||
provider: "moonshot",
|
||||
modelApi: "openai-completions",
|
||||
modelId: "kimi-k2.6",
|
||||
} as never);
|
||||
expect(nativeIdsPolicy).toMatchObject({
|
||||
applyAssistantFirstOrderingFix: true,
|
||||
validateGeminiTurns: true,
|
||||
validateAnthropicTurns: true,
|
||||
});
|
||||
expect(nativeIdsPolicy).not.toHaveProperty("sanitizeToolCallIds");
|
||||
expect(nativeIdsPolicy).not.toHaveProperty("toolCallIdMode");
|
||||
|
||||
expect(
|
||||
PASSTHROUGH_GEMINI_REPLAY_HOOKS.buildReplayPolicy?.({
|
||||
provider: "openrouter",
|
||||
|
||||
@@ -118,7 +118,7 @@ type ProviderReplayFamilyHooks = Pick<
|
||||
>;
|
||||
|
||||
type BuildProviderReplayFamilyHooksOptions =
|
||||
| { family: "openai-compatible" }
|
||||
| { family: "openai-compatible"; sanitizeToolCallIds?: boolean }
|
||||
| { family: "anthropic-by-model" }
|
||||
| { family: "native-anthropic-by-model" }
|
||||
| { family: "google-gemini" }
|
||||
@@ -132,11 +132,13 @@ export function buildProviderReplayFamilyHooks(
|
||||
options: BuildProviderReplayFamilyHooksOptions,
|
||||
): ProviderReplayFamilyHooks {
|
||||
switch (options.family) {
|
||||
case "openai-compatible":
|
||||
case "openai-compatible": {
|
||||
const policyOptions = { sanitizeToolCallIds: options.sanitizeToolCallIds };
|
||||
return {
|
||||
buildReplayPolicy: (ctx: ProviderReplayPolicyContext) =>
|
||||
buildOpenAICompatibleReplayPolicy(ctx.modelApi),
|
||||
buildOpenAICompatibleReplayPolicy(ctx.modelApi, policyOptions),
|
||||
};
|
||||
}
|
||||
case "anthropic-by-model":
|
||||
return {
|
||||
buildReplayPolicy: ({ modelId }: ProviderReplayPolicyContext) =>
|
||||
|
||||
@@ -22,6 +22,32 @@ describe("provider replay helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("omits tool-call id sanitization when opted out for openai-completions", () => {
|
||||
const policy = buildOpenAICompatibleReplayPolicy("openai-completions", {
|
||||
sanitizeToolCallIds: false,
|
||||
});
|
||||
expect(policy).toMatchObject({
|
||||
applyAssistantFirstOrderingFix: true,
|
||||
validateGeminiTurns: true,
|
||||
validateAnthropicTurns: true,
|
||||
});
|
||||
expect(policy).not.toHaveProperty("sanitizeToolCallIds");
|
||||
expect(policy).not.toHaveProperty("toolCallIdMode");
|
||||
});
|
||||
|
||||
it("omits tool-call id sanitization when opted out for openai-responses", () => {
|
||||
const policy = buildOpenAICompatibleReplayPolicy("openai-responses", {
|
||||
sanitizeToolCallIds: false,
|
||||
});
|
||||
expect(policy).toMatchObject({
|
||||
applyAssistantFirstOrderingFix: false,
|
||||
validateGeminiTurns: false,
|
||||
validateAnthropicTurns: false,
|
||||
});
|
||||
expect(policy).not.toHaveProperty("sanitizeToolCallIds");
|
||||
expect(policy).not.toHaveProperty("toolCallIdMode");
|
||||
});
|
||||
|
||||
it("builds strict anthropic replay policy", () => {
|
||||
expect(buildStrictAnthropicReplayPolicy({ dropThinkingBlocks: true })).toMatchObject({
|
||||
sanitizeMode: "full",
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
|
||||
export function buildOpenAICompatibleReplayPolicy(
|
||||
modelApi: string | null | undefined,
|
||||
options: { sanitizeToolCallIds?: boolean } = {},
|
||||
): ProviderReplayPolicy | undefined {
|
||||
if (
|
||||
modelApi !== "openai-completions" &&
|
||||
@@ -21,9 +22,12 @@ export function buildOpenAICompatibleReplayPolicy(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sanitizeToolCallIds = options.sanitizeToolCallIds ?? true;
|
||||
|
||||
return {
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
...(sanitizeToolCallIds
|
||||
? { sanitizeToolCallIds: true, toolCallIdMode: "strict" as const }
|
||||
: {}),
|
||||
...(modelApi === "openai-completions"
|
||||
? {
|
||||
applyAssistantFirstOrderingFix: true,
|
||||
|
||||
Reference in New Issue
Block a user