From 3e062acbcbf33c822ec0b5e6ac9a5b6ac7926a00 Mon Sep 17 00:00:00 2001 From: Frank Yang Date: Thu, 9 Apr 2026 17:09:17 +0800 Subject: [PATCH] fix(fireworks): disable FirePass Kimi reasoning leak (#63607) * fix: disable FirePass Kimi reasoning leak * fix: preserve Fireworks wrapper fallbacks * fix: harden Fireworks Kimi model matching * fix: restore Fireworks payload sanitization --- extensions/fireworks/index.test.ts | 62 ++++++++- extensions/fireworks/index.ts | 6 +- extensions/fireworks/model-id.ts | 5 + extensions/fireworks/provider-catalog.ts | 2 +- extensions/fireworks/stream.test.ts | 155 +++++++++++++++++++++++ extensions/fireworks/stream.ts | 39 ++++++ 6 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 extensions/fireworks/model-id.ts create mode 100644 extensions/fireworks/stream.test.ts create mode 100644 extensions/fireworks/stream.ts diff --git a/extensions/fireworks/index.test.ts b/extensions/fireworks/index.test.ts index 38fed9883ee..a3cd5af4541 100644 --- a/extensions/fireworks/index.test.ts +++ b/extensions/fireworks/index.test.ts @@ -74,7 +74,7 @@ describe("fireworks provider plugin", () => { expect(catalog.provider.baseUrl).toBe(FIREWORKS_BASE_URL); expect(catalog.provider.models?.map((model) => model.id)).toEqual([FIREWORKS_DEFAULT_MODEL_ID]); expect(catalog.provider.models?.[0]).toMatchObject({ - reasoning: true, + reasoning: false, input: ["text", "image"], contextWindow: FIREWORKS_DEFAULT_CONTEXT_WINDOW, maxTokens: FIREWORKS_DEFAULT_MAX_TOKENS, @@ -112,4 +112,64 @@ describe("fireworks provider plugin", () => { reasoning: true, }); }); + + it("disables reasoning metadata for Fireworks Kimi dynamic models", async () => { + const provider = await registerSingleProviderPlugin(fireworksPlugin); + const resolved = provider.resolveDynamicModel?.( + createDynamicContext({ + provider: "fireworks", + modelId: "accounts/fireworks/models/kimi-k2p5", + models: [ + { + id: FIREWORKS_DEFAULT_MODEL_ID, + name: FIREWORKS_DEFAULT_MODEL_ID, + provider: "fireworks", + api: "openai-completions", + baseUrl: FIREWORKS_BASE_URL, + reasoning: false, + input: ["text", "image"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: FIREWORKS_DEFAULT_CONTEXT_WINDOW, + maxTokens: FIREWORKS_DEFAULT_MAX_TOKENS, + }, + ], + }), + ); + + expect(resolved).toMatchObject({ + provider: "fireworks", + id: "accounts/fireworks/models/kimi-k2p5", + reasoning: false, + }); + }); + + it("disables reasoning metadata for Fireworks Kimi k2.5 aliases", async () => { + const provider = await registerSingleProviderPlugin(fireworksPlugin); + const resolved = provider.resolveDynamicModel?.( + createDynamicContext({ + provider: "fireworks", + modelId: "accounts/fireworks/routers/kimi-k2.5-turbo", + models: [ + { + id: FIREWORKS_DEFAULT_MODEL_ID, + name: FIREWORKS_DEFAULT_MODEL_ID, + provider: "fireworks", + api: "openai-completions", + baseUrl: FIREWORKS_BASE_URL, + reasoning: false, + input: ["text", "image"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: FIREWORKS_DEFAULT_CONTEXT_WINDOW, + maxTokens: FIREWORKS_DEFAULT_MAX_TOKENS, + }, + ], + }), + ); + + expect(resolved).toMatchObject({ + provider: "fireworks", + id: "accounts/fireworks/routers/kimi-k2.5-turbo", + reasoning: false, + }); + }); }); diff --git a/extensions/fireworks/index.ts b/extensions/fireworks/index.ts index 1f7a334fb3c..5aa59edbb3e 100644 --- a/extensions/fireworks/index.ts +++ b/extensions/fireworks/index.ts @@ -6,6 +6,7 @@ import { DEFAULT_CONTEXT_TOKENS, normalizeModelCompat, } from "openclaw/plugin-sdk/provider-model-shared"; +import { isFireworksKimiModelId } from "./model-id.js"; import { applyFireworksConfig, FIREWORKS_DEFAULT_MODEL_REF } from "./onboard.js"; import { buildFireworksProvider, @@ -14,6 +15,7 @@ import { FIREWORKS_DEFAULT_MAX_TOKENS, FIREWORKS_DEFAULT_MODEL_ID, } from "./provider-catalog.js"; +import { wrapFireworksProviderStream } from "./stream.js"; const PROVIDER_ID = "fireworks"; const OPENAI_COMPATIBLE_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ @@ -34,6 +36,7 @@ function resolveFireworksDynamicModel(ctx: ProviderResolveDynamicModelContext) { ctx, patch: { provider: PROVIDER_ID, + reasoning: !isFireworksKimiModelId(modelId), }, }) ?? normalizeModelCompat({ @@ -42,7 +45,7 @@ function resolveFireworksDynamicModel(ctx: ProviderResolveDynamicModelContext) { provider: PROVIDER_ID, api: "openai-completions", baseUrl: FIREWORKS_BASE_URL, - reasoning: true, + reasoning: !isFireworksKimiModelId(modelId), input: ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: FIREWORKS_DEFAULT_CONTEXT_WINDOW, @@ -77,6 +80,7 @@ export default defineSingleProviderPluginEntry({ allowExplicitBaseUrl: true, }, ...OPENAI_COMPATIBLE_REPLAY_HOOKS, + wrapStreamFn: wrapFireworksProviderStream, resolveDynamicModel: (ctx) => resolveFireworksDynamicModel(ctx), isModernModelRef: () => true, }, diff --git a/extensions/fireworks/model-id.ts b/extensions/fireworks/model-id.ts new file mode 100644 index 00000000000..7447cc730c8 --- /dev/null +++ b/extensions/fireworks/model-id.ts @@ -0,0 +1,5 @@ +export function isFireworksKimiModelId(modelId: string): boolean { + const normalized = modelId.trim().toLowerCase(); + const lastSegment = normalized.split("/").pop() ?? normalized; + return /^kimi-k2(?:p5|\.5)(?:[-_].+)?$/.test(lastSegment); +} diff --git a/extensions/fireworks/provider-catalog.ts b/extensions/fireworks/provider-catalog.ts index f5bb19b0f9a..103cd98253f 100644 --- a/extensions/fireworks/provider-catalog.ts +++ b/extensions/fireworks/provider-catalog.ts @@ -20,7 +20,7 @@ export function buildFireworksCatalogModels(): ModelDefinitionConfig[] { { id: FIREWORKS_DEFAULT_MODEL_ID, name: "Kimi K2.5 Turbo (Fire Pass)", - reasoning: true, + reasoning: false, // Kimi K2.5 can expose reasoning in visible content on FirePass. input: ["text", "image"], cost: ZERO_COST, contextWindow: FIREWORKS_DEFAULT_CONTEXT_WINDOW, diff --git a/extensions/fireworks/stream.test.ts b/extensions/fireworks/stream.test.ts new file mode 100644 index 00000000000..5199c8f90c6 --- /dev/null +++ b/extensions/fireworks/stream.test.ts @@ -0,0 +1,155 @@ +import type { StreamFn } from "@mariozechner/pi-agent-core"; +import type { Context, Model } from "@mariozechner/pi-ai"; +import { describe, expect, it } from "vitest"; +import { + createFireworksKimiThinkingDisabledWrapper, + wrapFireworksProviderStream, +} from "./stream.js"; + +function capturePayload(params: { + provider: string; + api: string; + modelId: string; + initialPayload?: Record; +}): Record { + let captured: Record = {}; + const baseStreamFn: StreamFn = (_model, _context, options) => { + const payload = { ...params.initialPayload }; + options?.onPayload?.(payload, _model); + captured = payload; + return {} as ReturnType; + }; + + const wrapped = createFireworksKimiThinkingDisabledWrapper(baseStreamFn); + void wrapped( + { + api: params.api, + provider: params.provider, + id: params.modelId, + } as Model<"openai-completions">, + { messages: [] } as Context, + {}, + ); + + return captured; +} + +describe("createFireworksKimiThinkingDisabledWrapper", () => { + it("forces thinking disabled for Fireworks Kimi models", () => { + expect( + capturePayload({ + provider: "fireworks", + api: "openai-completions", + modelId: "accounts/fireworks/routers/kimi-k2p5-turbo", + }), + ).toMatchObject({ thinking: { type: "disabled" } }); + }); + + it("forces thinking disabled for Fireworks Kimi k2.5 aliases", () => { + expect( + capturePayload({ + provider: "fireworks", + api: "openai-completions", + modelId: "accounts/fireworks/routers/kimi-k2.5-turbo", + }), + ).toMatchObject({ thinking: { type: "disabled" } }); + }); + + it("strips reasoning fields when disabling Fireworks Kimi thinking", () => { + const payload = capturePayload({ + provider: "fireworks", + api: "openai-completions", + modelId: "accounts/fireworks/models/kimi-k2p5", + initialPayload: { + reasoning_effort: "low", + reasoning: { effort: "low" }, + reasoningEffort: "low", + }, + }); + + expect(payload).toEqual({ thinking: { type: "disabled" } }); + }); + + it("passes sanitized payloads to caller onPayload hooks", () => { + let callbackPayload: Record = {}; + const baseStreamFn: StreamFn = (_model, _context, options) => { + const payload = { + reasoning_effort: "high", + reasoning: { effort: "high" }, + }; + options?.onPayload?.(payload, _model); + return {} as ReturnType; + }; + + const wrapped = createFireworksKimiThinkingDisabledWrapper(baseStreamFn); + void wrapped( + { + api: "openai-completions", + provider: "fireworks", + id: "accounts/fireworks/routers/kimi-k2p5-turbo", + } as Model<"openai-completions">, + { messages: [] } as Context, + { + onPayload: (payload) => { + callbackPayload = payload as Record; + }, + }, + ); + + expect(callbackPayload).toEqual({ thinking: { type: "disabled" } }); + }); + + it("returns no provider wrapper for non-target Fireworks requests", () => { + expect( + wrapFireworksProviderStream({ + provider: "fireworks", + modelId: "accounts/fireworks/models/qwen3.6-plus", + model: { + api: "openai-completions", + provider: "fireworks", + id: "accounts/fireworks/models/qwen3.6-plus", + } as Model<"openai-completions">, + streamFn: undefined, + } as never), + ).toBeUndefined(); + + expect( + wrapFireworksProviderStream({ + provider: "fireworks", + modelId: "accounts/fireworks/routers/kimi-k2p5-turbo", + model: { + api: "openai-responses", + provider: "fireworks", + id: "accounts/fireworks/routers/kimi-k2p5-turbo", + } as Model<"openai-responses">, + streamFn: undefined, + } as never), + ).toBeUndefined(); + + expect( + wrapFireworksProviderStream({ + provider: "fireworks-ai", + modelId: "accounts/fireworks/routers/kimi-k2p5-turbo", + model: { + api: "openai-completions", + provider: "fireworks-ai", + id: "accounts/fireworks/routers/kimi-k2p5-turbo", + } as Model<"openai-completions">, + streamFn: undefined, + } as never), + ).toBeTypeOf("function"); + + expect( + wrapFireworksProviderStream({ + provider: "openai", + modelId: "gpt-5.4", + model: { + api: "openai-completions", + provider: "openai", + id: "gpt-5.4", + } as Model<"openai-completions">, + streamFn: undefined, + } as never), + ).toBeUndefined(); + }); +}); diff --git a/extensions/fireworks/stream.ts b/extensions/fireworks/stream.ts new file mode 100644 index 00000000000..c77cca106ec --- /dev/null +++ b/extensions/fireworks/stream.ts @@ -0,0 +1,39 @@ +import type { StreamFn } from "@mariozechner/pi-agent-core"; +import { streamSimple } from "@mariozechner/pi-ai"; +import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry"; +import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared"; +import { streamWithPayloadPatch } from "openclaw/plugin-sdk/provider-stream-shared"; +import { isFireworksKimiModelId } from "./model-id.js"; + +function isFireworksProviderId(providerId: string): boolean { + const normalized = normalizeProviderId(providerId); + return normalized === "fireworks" || normalized === "fireworks-ai"; +} + +export function createFireworksKimiThinkingDisabledWrapper( + baseStreamFn: StreamFn | undefined, +): StreamFn { + const underlying = baseStreamFn ?? streamSimple; + return (model, context, options) => + streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => { + // Fireworks Kimi can emit chain-of-thought in visible `content` unless + // the Anthropic-style thinking toggle is explicitly disabled. + payloadObj.thinking = { type: "disabled" }; + delete payloadObj.reasoning; + delete payloadObj.reasoning_effort; + delete payloadObj.reasoningEffort; + }); +} + +export function wrapFireworksProviderStream( + ctx: ProviderWrapStreamFnContext, +): StreamFn | undefined { + if ( + !isFireworksProviderId(ctx.provider) || + ctx.model?.api !== "openai-completions" || + !isFireworksKimiModelId(ctx.modelId) + ) { + return undefined; + } + return createFireworksKimiThinkingDisabledWrapper(ctx.streamFn); +}