fix(ai): remove unnecessary LongCat content compatibility

This commit is contained in:
yuzehui02 2026-11-05 15:53:10 +08:00
parent 83124c38c2
commit 13c4e5264d
10 changed files with 3 additions and 79 deletions

View file

@ -1461,7 +1461,6 @@ interface OpenAICompletionsCompat {
maxTokensField?: 'max_completion_tokens' | 'max_tokens'; // Which field name to use (default: max_completion_tokens)
requiresToolResultName?: boolean; // Whether tool results require the `name` field (default: false)
requiresAssistantAfterToolResult?: boolean; // Whether tool results must be followed by an assistant message (default: false)
requiresAssistantContentString?: boolean; // Whether tool-only assistant messages use empty string instead of null content (default: false)
requiresThinkingAsText?: boolean; // Whether thinking blocks must be converted to text (default: false)
requiresReasoningContentOnAssistantMessages?: boolean; // Whether all replayed assistant messages must include empty reasoning_content when reasoning is enabled (default: auto-detected for DeepSeek)
thinkingFormat?: 'openai' | 'openrouter' | 'deepseek' | 'together' | 'baseten' | 'zai' | 'qwen' | 'chat-template' | 'qwen-chat-template' | 'string-thinking' | 'ant-ling'; // Format for reasoning param: 'openai' uses reasoning_effort, 'openrouter' uses reasoning: { effort }, 'deepseek' uses thinking: { type } plus reasoning_effort when supported, 'together' uses reasoning: { enabled } plus reasoning_effort when supported, 'baseten' uses configurable chat_template_args plus reasoning_effort when supported, 'zai' uses thinking: { type }, 'qwen' uses enable_thinking, 'chat-template' uses configurable chat_template_kwargs, 'qwen-chat-template' uses chat_template_kwargs.enable_thinking and preserve_thinking, 'string-thinking' uses top-level thinking, 'ant-ling' uses reasoning: { effort } only for mapped efforts (default: openai)

View file

@ -32,7 +32,6 @@ export default defineProvider({
supportsDeveloperRole: false,
supportsReasoningEffort: false,
maxTokensField: "max_tokens",
requiresAssistantContentString: true,
requiresReasoningContentOnAssistantMessages: true,
thinkingFormat: "deepseek",
supportsStrictMode: false,

View file

@ -1281,7 +1281,7 @@ export function convertMessages(
// Some providers don't accept null content, use empty string instead
const assistantMsg: ChatCompletionAssistantMessageParamWithReasoning = {
role: "assistant",
content: compat.requiresAssistantContentString || compat.requiresAssistantAfterToolResult ? "" : null,
content: compat.requiresAssistantAfterToolResult ? "" : null,
};
const assistantTextParts = msg.content
@ -1645,7 +1645,6 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
requiresToolResultName: false,
requiresAssistantAfterToolResult: false,
requiresAssistantContentString: false,
requiresThinkingAsText: false,
requiresReasoningContentOnAssistantMessages: isDeepSeek,
thinkingFormat: isDeepSeek
@ -1702,8 +1701,6 @@ function getCompat(model: Model<"openai-completions">): ResolvedOpenAICompletion
requiresToolResultName: model.compat.requiresToolResultName ?? detected.requiresToolResultName,
requiresAssistantAfterToolResult:
model.compat.requiresAssistantAfterToolResult ?? detected.requiresAssistantAfterToolResult,
requiresAssistantContentString:
model.compat.requiresAssistantContentString ?? detected.requiresAssistantContentString,
requiresThinkingAsText: model.compat.requiresThinkingAsText ?? detected.requiresThinkingAsText,
requiresReasoningContentOnAssistantMessages:
model.compat.requiresReasoningContentOnAssistantMessages ??

View file

@ -232,10 +232,6 @@ const OpenAICompletionsCompatProperties = {
"Whether a user message after tool results requires an assistant message in between. Default: auto-detected from URL.",
}),
),
requiresAssistantContentString: optionalCompatBoolean({
description: "Whether tool-only assistant messages need an empty string instead of null content.",
default: false,
}),
requiresThinkingAsText: opt(
bool({
description:

View file

@ -4,7 +4,7 @@ import { streamSimple } from "../src/api/openai-completions.ts";
import { InMemoryCredentialStore } from "../src/auth/credential-store.ts";
import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.ts";
import { getSupportedThinkingLevels } from "../src/models.ts";
import { builtinModels, getBuiltinModel } from "../src/providers/all.ts";
import { builtinModels } from "../src/providers/all.ts";
import type { Context, FetchFunction, Model } from "../src/types.ts";
import { normalizeContext } from "../src/utils/transcript.ts";
@ -230,7 +230,7 @@ describe("LongCat provider", () => {
const messages = payload?.messages as Record<string, unknown>[];
expect(messages.map((message) => message.role)).toEqual(["system", "user", "assistant", "tool", "user"]);
expect(messages[2]).toMatchObject({
content: "",
content: null,
reasoning_content: "Check the weather.",
tool_calls: [{ id: "call_weather", function: { name: "weather", arguments: '{"city":"Paris"}' } }],
});
@ -274,61 +274,3 @@ describe("LongCat provider", () => {
]);
});
});
describe("assistant content compatibility", () => {
it.each([true, false, undefined])(
"uses an empty string only when required (%s)",
async (requiresAssistantContentString) => {
const base = getBuiltinModel("openai", "gpt-4o");
const model: Model<"openai-completions"> = {
...base,
api: "openai-completions",
compat: { requiresAssistantContentString },
};
const messages: Context = {
...context,
messages: [
...context.messages,
{
role: "assistant",
api: model.api,
provider: model.provider,
model: model.id,
content: [{ type: "toolCall", id: "call_weather", name: "weather", arguments: { city: "Paris" } }],
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "toolUse",
timestamp: 1,
},
{
role: "toolResult",
toolCallId: "call_weather",
toolName: "weather",
content: [{ type: "text", text: "Sunny" }],
isError: false,
timestamp: 2,
},
],
};
let payload: Record<string, unknown> | undefined;
const result = await streamSimple(model, normalizeContext(messages), {
apiKey: "test-key",
fetch: async (input, init) => {
payload = (await new Request(input, init).json()) as Record<string, unknown>;
return sse([chunk({ content: "ok" }, "stop")]);
},
}).result();
expect(result.stopReason).toBe("stop");
const assistant = (payload?.messages as Record<string, unknown>[]).find(
(message) => message.role === "assistant",
);
expect(assistant?.content).toBe(requiresAssistantContentString ? "" : null);
},
);
});

View file

@ -31,7 +31,6 @@ const compat = {
maxTokensField: "max_completion_tokens",
requiresToolResultName: false,
requiresAssistantAfterToolResult: false,
requiresAssistantContentString: false,
requiresThinkingAsText: true,
requiresReasoningContentOnAssistantMessages: false,
thinkingFormat: "openai",

View file

@ -1405,7 +1405,6 @@ describe("openai-completions tool_choice", () => {
maxTokensField: "max_completion_tokens",
requiresToolResultName: false,
requiresAssistantAfterToolResult: false,
requiresAssistantContentString: false,
requiresThinkingAsText: false,
requiresReasoningContentOnAssistantMessages: false,
thinkingFormat: "openai",

View file

@ -23,7 +23,6 @@ const compat: Omit<Required<OpenAICompletionsCompat>, "thinkingTokenBudgetField"
maxTokensField: "max_completion_tokens",
requiresToolResultName: false,
requiresAssistantAfterToolResult: false,
requiresAssistantContentString: false,
requiresThinkingAsText: false,
requiresReasoningContentOnAssistantMessages: false,
thinkingFormat: "openai",

View file

@ -118,7 +118,6 @@ See [Describe model input and caching](models.md#describe-model-input-and-cachin
| `maxTokensField` | `"max_completion_tokens" \| "max_tokens"` | | Which field to use for max tokens. Default: auto-detected from URL. |
| `requiresToolResultName` | boolean | | Whether tool results require the name field. Default: auto-detected from URL. |
| `requiresAssistantAfterToolResult` | boolean | | Whether a user message after tool results requires an assistant message in between. Default: auto-detected from URL. |
| `requiresAssistantContentString` | boolean | `false` | Whether tool-only assistant messages need an empty string instead of null content. |
| `requiresThinkingAsText` | boolean | | Whether thinking blocks must be converted to text blocks with &lt;thinking> delimiters. Default: auto-detected from URL. |
| `requiresReasoningContentOnAssistantMessages` | boolean | | Whether all replayed assistant messages must include an empty reasoning_content field when reasoning is enabled. Default: auto-detected from URL. |
| `thinkingFormat` | `"openai" \| "openrouter" \| "deepseek" \| "together" \| "baseten" \| "zai" \| "qwen" \| "chat-template" \| "qwen-chat-template" \| "string-thinking" \| "ant-ling"` | | Format for reasoning or thinking parameters. When omitted, Pi auto-detects the format from the provider URL. "openai" uses reasoning_effort, "openrouter" uses reasoning.effort, "deepseek" uses thinking.type plus reasoning_effort when supported, "together" uses reasoning.enabled plus reasoning_effort when supported, "baseten" uses configurable chat_template_args plus reasoning_effort when supported, "zai" uses thinking.type, "qwen" uses top-level enable_thinking, "qwen-chat-template" uses chat_template_kwargs.enable_thinking and preserve_thinking, "chat-template" uses configurable chat_template_kwargs, "string-thinking" uses top-level thinking, and "ant-ling" uses reasoning.effort only when the mapped effort is non-null. |

View file

@ -552,11 +552,6 @@
"type": "boolean",
"description": "Whether a user message after tool results requires an assistant message in between. Default: auto-detected from URL."
},
"requiresAssistantContentString": {
"type": "boolean",
"description": "Whether tool-only assistant messages need an empty string instead of null content.",
"default": false
},
"requiresThinkingAsText": {
"type": "boolean",
"description": "Whether thinking blocks must be converted to text blocks with <thinking> delimiters. Default: auto-detected from URL."