From 13c4e5264d7d9d4430a6d4a109972c60831d400b Mon Sep 17 00:00:00 2001 From: yuzehui02 Date: Thu, 5 Nov 2026 15:53:10 +0800 Subject: [PATCH] fix(ai): remove unnecessary LongCat content compatibility --- packages/ai/README.md | 1 - packages/ai/catalog/providers/longcat.ts | 1 - packages/ai/src/api/openai-completions.ts | 5 +- packages/ai/src/providers/compat-schema.ts | 4 -- packages/ai/test/longcat-models.test.ts | 62 +------------------ ...penai-completions-thinking-as-text.test.ts | 1 - .../openai-completions-tool-choice.test.ts | 1 - ...nai-completions-tool-result-images.test.ts | 1 - .../coding-agent/docs/models-configuration.md | 1 - .../coding-agent/schemas/models.schema.json | 5 -- 10 files changed, 3 insertions(+), 79 deletions(-) diff --git a/packages/ai/README.md b/packages/ai/README.md index 88c4877b2..3dfd398c5 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -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) diff --git a/packages/ai/catalog/providers/longcat.ts b/packages/ai/catalog/providers/longcat.ts index 4a6ff61a6..84ddb8b77 100644 --- a/packages/ai/catalog/providers/longcat.ts +++ b/packages/ai/catalog/providers/longcat.ts @@ -32,7 +32,6 @@ export default defineProvider({ supportsDeveloperRole: false, supportsReasoningEffort: false, maxTokensField: "max_tokens", - requiresAssistantContentString: true, requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "deepseek", supportsStrictMode: false, diff --git a/packages/ai/src/api/openai-completions.ts b/packages/ai/src/api/openai-completions.ts index 358cc196d..0e5b606e6 100644 --- a/packages/ai/src/api/openai-completions.ts +++ b/packages/ai/src/api/openai-completions.ts @@ -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 ?? diff --git a/packages/ai/src/providers/compat-schema.ts b/packages/ai/src/providers/compat-schema.ts index ce5c48363..792b7a1b7 100644 --- a/packages/ai/src/providers/compat-schema.ts +++ b/packages/ai/src/providers/compat-schema.ts @@ -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: diff --git a/packages/ai/test/longcat-models.test.ts b/packages/ai/test/longcat-models.test.ts index 21f891d71..499f0f0fd 100644 --- a/packages/ai/test/longcat-models.test.ts +++ b/packages/ai/test/longcat-models.test.ts @@ -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[]; 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 | undefined; - const result = await streamSimple(model, normalizeContext(messages), { - apiKey: "test-key", - fetch: async (input, init) => { - payload = (await new Request(input, init).json()) as Record; - return sse([chunk({ content: "ok" }, "stop")]); - }, - }).result(); - expect(result.stopReason).toBe("stop"); - const assistant = (payload?.messages as Record[]).find( - (message) => message.role === "assistant", - ); - expect(assistant?.content).toBe(requiresAssistantContentString ? "" : null); - }, - ); -}); diff --git a/packages/ai/test/openai-completions-thinking-as-text.test.ts b/packages/ai/test/openai-completions-thinking-as-text.test.ts index 812bdd760..e609b1453 100644 --- a/packages/ai/test/openai-completions-thinking-as-text.test.ts +++ b/packages/ai/test/openai-completions-thinking-as-text.test.ts @@ -31,7 +31,6 @@ const compat = { maxTokensField: "max_completion_tokens", requiresToolResultName: false, requiresAssistantAfterToolResult: false, - requiresAssistantContentString: false, requiresThinkingAsText: true, requiresReasoningContentOnAssistantMessages: false, thinkingFormat: "openai", diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 475f1888d..cf6aeebb5 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -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", diff --git a/packages/ai/test/openai-completions-tool-result-images.test.ts b/packages/ai/test/openai-completions-tool-result-images.test.ts index 26a9f18cd..7acc7221c 100644 --- a/packages/ai/test/openai-completions-tool-result-images.test.ts +++ b/packages/ai/test/openai-completions-tool-result-images.test.ts @@ -23,7 +23,6 @@ const compat: Omit, "thinkingTokenBudgetField" maxTokensField: "max_completion_tokens", requiresToolResultName: false, requiresAssistantAfterToolResult: false, - requiresAssistantContentString: false, requiresThinkingAsText: false, requiresReasoningContentOnAssistantMessages: false, thinkingFormat: "openai", diff --git a/packages/coding-agent/docs/models-configuration.md b/packages/coding-agent/docs/models-configuration.md index 8eb4b79b8..2a25bdb48 100644 --- a/packages/coding-agent/docs/models-configuration.md +++ b/packages/coding-agent/docs/models-configuration.md @@ -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 <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. | diff --git a/packages/coding-agent/schemas/models.schema.json b/packages/coding-agent/schemas/models.schema.json index 268cd8761..8861f8542 100644 --- a/packages/coding-agent/schemas/models.schema.json +++ b/packages/coding-agent/schemas/models.schema.json @@ -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 delimiters. Default: auto-detected from URL."