Skip to content

Commit 3e062ac

Browse files
authored
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
1 parent 40c5edb commit 3e062ac

6 files changed

Lines changed: 266 additions & 3 deletions

File tree

extensions/fireworks/index.test.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ describe("fireworks provider plugin", () => {
7474
expect(catalog.provider.baseUrl).toBe(FIREWORKS_BASE_URL);
7575
expect(catalog.provider.models?.map((model) => model.id)).toEqual([FIREWORKS_DEFAULT_MODEL_ID]);
7676
expect(catalog.provider.models?.[0]).toMatchObject({
77-
reasoning: true,
77+
reasoning: false,
7878
input: ["text", "image"],
7979
contextWindow: FIREWORKS_DEFAULT_CONTEXT_WINDOW,
8080
maxTokens: FIREWORKS_DEFAULT_MAX_TOKENS,
@@ -112,4 +112,64 @@ describe("fireworks provider plugin", () => {
112112
reasoning: true,
113113
});
114114
});
115+
116+
it("disables reasoning metadata for Fireworks Kimi dynamic models", async () => {
117+
const provider = await registerSingleProviderPlugin(fireworksPlugin);
118+
const resolved = provider.resolveDynamicModel?.(
119+
createDynamicContext({
120+
provider: "fireworks",
121+
modelId: "accounts/fireworks/models/kimi-k2p5",
122+
models: [
123+
{
124+
id: FIREWORKS_DEFAULT_MODEL_ID,
125+
name: FIREWORKS_DEFAULT_MODEL_ID,
126+
provider: "fireworks",
127+
api: "openai-completions",
128+
baseUrl: FIREWORKS_BASE_URL,
129+
reasoning: false,
130+
input: ["text", "image"],
131+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
132+
contextWindow: FIREWORKS_DEFAULT_CONTEXT_WINDOW,
133+
maxTokens: FIREWORKS_DEFAULT_MAX_TOKENS,
134+
},
135+
],
136+
}),
137+
);
138+
139+
expect(resolved).toMatchObject({
140+
provider: "fireworks",
141+
id: "accounts/fireworks/models/kimi-k2p5",
142+
reasoning: false,
143+
});
144+
});
145+
146+
it("disables reasoning metadata for Fireworks Kimi k2.5 aliases", async () => {
147+
const provider = await registerSingleProviderPlugin(fireworksPlugin);
148+
const resolved = provider.resolveDynamicModel?.(
149+
createDynamicContext({
150+
provider: "fireworks",
151+
modelId: "accounts/fireworks/routers/kimi-k2.5-turbo",
152+
models: [
153+
{
154+
id: FIREWORKS_DEFAULT_MODEL_ID,
155+
name: FIREWORKS_DEFAULT_MODEL_ID,
156+
provider: "fireworks",
157+
api: "openai-completions",
158+
baseUrl: FIREWORKS_BASE_URL,
159+
reasoning: false,
160+
input: ["text", "image"],
161+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
162+
contextWindow: FIREWORKS_DEFAULT_CONTEXT_WINDOW,
163+
maxTokens: FIREWORKS_DEFAULT_MAX_TOKENS,
164+
},
165+
],
166+
}),
167+
);
168+
169+
expect(resolved).toMatchObject({
170+
provider: "fireworks",
171+
id: "accounts/fireworks/routers/kimi-k2.5-turbo",
172+
reasoning: false,
173+
});
174+
});
115175
});

extensions/fireworks/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
DEFAULT_CONTEXT_TOKENS,
77
normalizeModelCompat,
88
} from "openclaw/plugin-sdk/provider-model-shared";
9+
import { isFireworksKimiModelId } from "./model-id.js";
910
import { applyFireworksConfig, FIREWORKS_DEFAULT_MODEL_REF } from "./onboard.js";
1011
import {
1112
buildFireworksProvider,
@@ -14,6 +15,7 @@ import {
1415
FIREWORKS_DEFAULT_MAX_TOKENS,
1516
FIREWORKS_DEFAULT_MODEL_ID,
1617
} from "./provider-catalog.js";
18+
import { wrapFireworksProviderStream } from "./stream.js";
1719

1820
const PROVIDER_ID = "fireworks";
1921
const OPENAI_COMPATIBLE_REPLAY_HOOKS = buildProviderReplayFamilyHooks({
@@ -34,6 +36,7 @@ function resolveFireworksDynamicModel(ctx: ProviderResolveDynamicModelContext) {
3436
ctx,
3537
patch: {
3638
provider: PROVIDER_ID,
39+
reasoning: !isFireworksKimiModelId(modelId),
3740
},
3841
}) ??
3942
normalizeModelCompat({
@@ -42,7 +45,7 @@ function resolveFireworksDynamicModel(ctx: ProviderResolveDynamicModelContext) {
4245
provider: PROVIDER_ID,
4346
api: "openai-completions",
4447
baseUrl: FIREWORKS_BASE_URL,
45-
reasoning: true,
48+
reasoning: !isFireworksKimiModelId(modelId),
4649
input: ["text", "image"],
4750
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
4851
contextWindow: FIREWORKS_DEFAULT_CONTEXT_WINDOW,
@@ -77,6 +80,7 @@ export default defineSingleProviderPluginEntry({
7780
allowExplicitBaseUrl: true,
7881
},
7982
...OPENAI_COMPATIBLE_REPLAY_HOOKS,
83+
wrapStreamFn: wrapFireworksProviderStream,
8084
resolveDynamicModel: (ctx) => resolveFireworksDynamicModel(ctx),
8185
isModernModelRef: () => true,
8286
},

extensions/fireworks/model-id.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export function isFireworksKimiModelId(modelId: string): boolean {
2+
const normalized = modelId.trim().toLowerCase();
3+
const lastSegment = normalized.split("/").pop() ?? normalized;
4+
return /^kimi-k2(?:p5|\.5)(?:[-_].+)?$/.test(lastSegment);
5+
}

extensions/fireworks/provider-catalog.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function buildFireworksCatalogModels(): ModelDefinitionConfig[] {
2020
{
2121
id: FIREWORKS_DEFAULT_MODEL_ID,
2222
name: "Kimi K2.5 Turbo (Fire Pass)",
23-
reasoning: true,
23+
reasoning: false, // Kimi K2.5 can expose reasoning in visible content on FirePass.
2424
input: ["text", "image"],
2525
cost: ZERO_COST,
2626
contextWindow: FIREWORKS_DEFAULT_CONTEXT_WINDOW,
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import type { StreamFn } from "@mariozechner/pi-agent-core";
2+
import type { Context, Model } from "@mariozechner/pi-ai";
3+
import { describe, expect, it } from "vitest";
4+
import {
5+
createFireworksKimiThinkingDisabledWrapper,
6+
wrapFireworksProviderStream,
7+
} from "./stream.js";
8+
9+
function capturePayload(params: {
10+
provider: string;
11+
api: string;
12+
modelId: string;
13+
initialPayload?: Record<string, unknown>;
14+
}): Record<string, unknown> {
15+
let captured: Record<string, unknown> = {};
16+
const baseStreamFn: StreamFn = (_model, _context, options) => {
17+
const payload = { ...params.initialPayload };
18+
options?.onPayload?.(payload, _model);
19+
captured = payload;
20+
return {} as ReturnType<StreamFn>;
21+
};
22+
23+
const wrapped = createFireworksKimiThinkingDisabledWrapper(baseStreamFn);
24+
void wrapped(
25+
{
26+
api: params.api,
27+
provider: params.provider,
28+
id: params.modelId,
29+
} as Model<"openai-completions">,
30+
{ messages: [] } as Context,
31+
{},
32+
);
33+
34+
return captured;
35+
}
36+
37+
describe("createFireworksKimiThinkingDisabledWrapper", () => {
38+
it("forces thinking disabled for Fireworks Kimi models", () => {
39+
expect(
40+
capturePayload({
41+
provider: "fireworks",
42+
api: "openai-completions",
43+
modelId: "accounts/fireworks/routers/kimi-k2p5-turbo",
44+
}),
45+
).toMatchObject({ thinking: { type: "disabled" } });
46+
});
47+
48+
it("forces thinking disabled for Fireworks Kimi k2.5 aliases", () => {
49+
expect(
50+
capturePayload({
51+
provider: "fireworks",
52+
api: "openai-completions",
53+
modelId: "accounts/fireworks/routers/kimi-k2.5-turbo",
54+
}),
55+
).toMatchObject({ thinking: { type: "disabled" } });
56+
});
57+
58+
it("strips reasoning fields when disabling Fireworks Kimi thinking", () => {
59+
const payload = capturePayload({
60+
provider: "fireworks",
61+
api: "openai-completions",
62+
modelId: "accounts/fireworks/models/kimi-k2p5",
63+
initialPayload: {
64+
reasoning_effort: "low",
65+
reasoning: { effort: "low" },
66+
reasoningEffort: "low",
67+
},
68+
});
69+
70+
expect(payload).toEqual({ thinking: { type: "disabled" } });
71+
});
72+
73+
it("passes sanitized payloads to caller onPayload hooks", () => {
74+
let callbackPayload: Record<string, unknown> = {};
75+
const baseStreamFn: StreamFn = (_model, _context, options) => {
76+
const payload = {
77+
reasoning_effort: "high",
78+
reasoning: { effort: "high" },
79+
};
80+
options?.onPayload?.(payload, _model);
81+
return {} as ReturnType<StreamFn>;
82+
};
83+
84+
const wrapped = createFireworksKimiThinkingDisabledWrapper(baseStreamFn);
85+
void wrapped(
86+
{
87+
api: "openai-completions",
88+
provider: "fireworks",
89+
id: "accounts/fireworks/routers/kimi-k2p5-turbo",
90+
} as Model<"openai-completions">,
91+
{ messages: [] } as Context,
92+
{
93+
onPayload: (payload) => {
94+
callbackPayload = payload as Record<string, unknown>;
95+
},
96+
},
97+
);
98+
99+
expect(callbackPayload).toEqual({ thinking: { type: "disabled" } });
100+
});
101+
102+
it("returns no provider wrapper for non-target Fireworks requests", () => {
103+
expect(
104+
wrapFireworksProviderStream({
105+
provider: "fireworks",
106+
modelId: "accounts/fireworks/models/qwen3.6-plus",
107+
model: {
108+
api: "openai-completions",
109+
provider: "fireworks",
110+
id: "accounts/fireworks/models/qwen3.6-plus",
111+
} as Model<"openai-completions">,
112+
streamFn: undefined,
113+
} as never),
114+
).toBeUndefined();
115+
116+
expect(
117+
wrapFireworksProviderStream({
118+
provider: "fireworks",
119+
modelId: "accounts/fireworks/routers/kimi-k2p5-turbo",
120+
model: {
121+
api: "openai-responses",
122+
provider: "fireworks",
123+
id: "accounts/fireworks/routers/kimi-k2p5-turbo",
124+
} as Model<"openai-responses">,
125+
streamFn: undefined,
126+
} as never),
127+
).toBeUndefined();
128+
129+
expect(
130+
wrapFireworksProviderStream({
131+
provider: "fireworks-ai",
132+
modelId: "accounts/fireworks/routers/kimi-k2p5-turbo",
133+
model: {
134+
api: "openai-completions",
135+
provider: "fireworks-ai",
136+
id: "accounts/fireworks/routers/kimi-k2p5-turbo",
137+
} as Model<"openai-completions">,
138+
streamFn: undefined,
139+
} as never),
140+
).toBeTypeOf("function");
141+
142+
expect(
143+
wrapFireworksProviderStream({
144+
provider: "openai",
145+
modelId: "gpt-5.4",
146+
model: {
147+
api: "openai-completions",
148+
provider: "openai",
149+
id: "gpt-5.4",
150+
} as Model<"openai-completions">,
151+
streamFn: undefined,
152+
} as never),
153+
).toBeUndefined();
154+
});
155+
});

extensions/fireworks/stream.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { StreamFn } from "@mariozechner/pi-agent-core";
2+
import { streamSimple } from "@mariozechner/pi-ai";
3+
import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry";
4+
import { normalizeProviderId } from "openclaw/plugin-sdk/provider-model-shared";
5+
import { streamWithPayloadPatch } from "openclaw/plugin-sdk/provider-stream-shared";
6+
import { isFireworksKimiModelId } from "./model-id.js";
7+
8+
function isFireworksProviderId(providerId: string): boolean {
9+
const normalized = normalizeProviderId(providerId);
10+
return normalized === "fireworks" || normalized === "fireworks-ai";
11+
}
12+
13+
export function createFireworksKimiThinkingDisabledWrapper(
14+
baseStreamFn: StreamFn | undefined,
15+
): StreamFn {
16+
const underlying = baseStreamFn ?? streamSimple;
17+
return (model, context, options) =>
18+
streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => {
19+
// Fireworks Kimi can emit chain-of-thought in visible `content` unless
20+
// the Anthropic-style thinking toggle is explicitly disabled.
21+
payloadObj.thinking = { type: "disabled" };
22+
delete payloadObj.reasoning;
23+
delete payloadObj.reasoning_effort;
24+
delete payloadObj.reasoningEffort;
25+
});
26+
}
27+
28+
export function wrapFireworksProviderStream(
29+
ctx: ProviderWrapStreamFnContext,
30+
): StreamFn | undefined {
31+
if (
32+
!isFireworksProviderId(ctx.provider) ||
33+
ctx.model?.api !== "openai-completions" ||
34+
!isFireworksKimiModelId(ctx.modelId)
35+
) {
36+
return undefined;
37+
}
38+
return createFireworksKimiThinkingDisabledWrapper(ctx.streamFn);
39+
}

0 commit comments

Comments
 (0)