






















@@ -5414,17 +5414,8 @@ describe("openai transport stream", () => {
54145414});
54155415});
541654165417-describe("buildOpenAICompletionsParams strips response-only reasoning fields", () => {
5418-// OpenRouter and other OpenAI-Completions providers echo `reasoning_details`
5419-// (array) and `reasoning_content` (string) in response choices to expose the
5420-// model's chain of thought. If those fields leak back into a follow-up
5421-// request, OpenRouter rejects the call with HTTP 500 ("Internal Server
5422-// Error"). buildOpenAICompletionsParams must scrub them before the wire.
5423-// Repro recipe (verified against live OpenRouter):
5424-// POST /chat/completions with messages[*].reasoning_details: "..." => 500
5425-// Same body without that key => 200
5426-5427-const baseModel = {
5417+describe("buildOpenAICompletionsParams sanitizes reasoning replay fields", () => {
5418+const openRouterModel = {
54285419id: "deepseek/deepseek-v4-flash",
54295420name: "DeepSeek v4 Flash",
54305421api: "openai-completions",
@@ -5437,6 +5428,19 @@ describe("buildOpenAICompletionsParams strips response-only reasoning fields", (
54375428maxTokens: 8192,
54385429} satisfies Model<"openai-completions">;
543954305431+const openAIModel = {
5432+id: "gpt-5.4-mini",
5433+name: "GPT-5.4 Mini",
5434+api: "openai-completions",
5435+provider: "openai",
5436+baseUrl: "https://api.openai.com/v1",
5437+reasoning: true,
5438+input: ["text"],
5439+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
5440+contextWindow: 200_000,
5441+maxTokens: 8192,
5442+} satisfies Model<"openai-completions">;
5443+54405444function getAssistantMessage(params: { messages: unknown }) {
54415445expect(Array.isArray(params.messages)).toBe(true);
54425446const list = params.messages as Array<Record<string, unknown>>;
@@ -5445,13 +5449,80 @@ describe("buildOpenAICompletionsParams strips response-only reasoning fields", (
54455449return assistant as Record<string, unknown>;
54465450}
544754515448-it("removes reasoning_details, reasoning_content, and reasoning from assistant replay", () => {
5452+function buildReplayParams(model: Model<"openai-completions">, thinkingSignature: string) {
5453+return buildOpenAICompletionsParams(
5454+model,
5455+{
5456+systemPrompt: "system",
5457+messages: [
5458+{ role: "user", content: "hello" },
5459+{
5460+role: "assistant",
5461+provider: model.provider,
5462+api: model.api,
5463+model: model.id,
5464+stopReason: "stop",
5465+timestamp: 0,
5466+content: [
5467+{
5468+type: "thinking",
5469+thinking: "Need to answer politely.",
5470+ thinkingSignature,
5471+},
5472+{ type: "text", text: "Hello!" },
5473+],
5474+},
5475+{ role: "user", content: "again" },
5476+],
5477+tools: [],
5478+} as never,
5479+undefined,
5480+) as { messages: unknown };
5481+}
5482+5483+it.each(["reasoning_details", "reasoning_content", "reasoning", "reasoning_text"])(
5484+"strips %s from stock OpenAI Chat Completions assistant replay",
5485+(thinkingSignature) => {
5486+const assistant = getAssistantMessage(buildReplayParams(openAIModel, thinkingSignature));
5487+5488+expect(assistant).not.toHaveProperty("reasoning_details");
5489+expect(assistant).not.toHaveProperty("reasoning_content");
5490+expect(assistant).not.toHaveProperty("reasoning");
5491+expect(assistant).not.toHaveProperty("reasoning_text");
5492+},
5493+);
5494+5495+it("normalizes OpenRouter string reasoning_details to reasoning", () => {
5496+const assistant = getAssistantMessage(buildReplayParams(openRouterModel, "reasoning_details"));
5497+5498+expect(assistant).not.toHaveProperty("reasoning_details");
5499+expect(assistant.reasoning).toBe("Need to answer politely.");
5500+});
5501+5502+it.each(["reasoning", "reasoning_content"])(
5503+"preserves OpenRouter %s string reasoning replay",
5504+(thinkingSignature) => {
5505+const assistant = getAssistantMessage(buildReplayParams(openRouterModel, thinkingSignature));
5506+5507+expect(assistant[thinkingSignature]).toBe("Need to answer politely.");
5508+},
5509+);
5510+5511+it("normalizes OpenRouter reasoning_text to reasoning", () => {
5512+const assistant = getAssistantMessage(buildReplayParams(openRouterModel, "reasoning_text"));
5513+5514+expect(assistant).not.toHaveProperty("reasoning_text");
5515+expect(assistant.reasoning).toBe("Need to answer politely.");
5516+});
5517+5518+it("preserves OpenRouter array reasoning_details from tool-call signatures", () => {
5519+const reasoningDetail = { type: "reasoning.encrypted", id: "rs_1", data: "ciphertext" };
54495520const params = buildOpenAICompletionsParams(
5450-baseModel,
5521+openRouterModel,
54515522{
54525523systemPrompt: "system",
54535524messages: [
5454-{ role: "user", content: "你好" },
5525+{ role: "user", content: "lookup" },
54555526{
54565527role: "assistant",
54575528provider: "openrouter",
@@ -5461,23 +5532,30 @@ describe("buildOpenAICompletionsParams strips response-only reasoning fields", (
54615532timestamp: 0,
54625533content: [
54635534{
5464-type: "thinking",
5465-thinking: "User said hi, I should respond politely.",
5466-thinkingSignature: "considering",
5535+type: "toolCall",
5536+id: "call_1",
5537+name: "lookup",
5538+arguments: { query: "weather" },
5539+thoughtSignature: JSON.stringify(reasoningDetail),
54675540},
5468-{ type: "text", text: "Hello!" },
54695541],
54705542},
5471-{ role: "user", content: "再来一个" },
5543+{
5544+role: "toolResult",
5545+toolCallId: "call_1",
5546+toolName: "lookup",
5547+content: [{ type: "text", text: "sunny" }],
5548+isError: false,
5549+timestamp: 1,
5550+},
5551+{ role: "user", content: "answer" },
54725552],
54735553tools: [],
54745554} as never,
54755555undefined,
54765556) as { messages: unknown };
5477555754785558const assistant = getAssistantMessage(params);
5479-expect(assistant).not.toHaveProperty("reasoning_details");
5480-expect(assistant).not.toHaveProperty("reasoning_content");
5481-expect(assistant).not.toHaveProperty("reasoning");
5559+expect(assistant.reasoning_details).toEqual([reasoningDetail]);
54825560});
54835561});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。