fix(ai): send max_tokens to DeepSeek APIs

Auto-detect DeepSeek in generated and runtime compatibility metadata so built-in and custom models use the supported token limit field.
This commit is contained in:
David Brailovsky 2026-08-08 18:42:07 +02:00
parent 368e013dec
commit c185d41238
4 changed files with 53 additions and 3 deletions

View file

@ -11,6 +11,7 @@
- Fixed upstream request buffer limit failures to trigger automatic assistant retries.
- Fixed OpenAI Responses function and custom tool calls to preserve namespaces during streaming, proxying, and replay ([#7709](https://github.com/earendil-works/pi/issues/7709)).
- Fixed built-in and custom DeepSeek API models to send output limits through the supported `max_tokens` field.
## [0.84.1] - 2026-08-07

View file

@ -643,11 +643,18 @@ function detectOpenAICompletionsCompat(model: Model<"openai-completions">): Open
isCloudflareAiGateway ||
isAntLing;
const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com");
const useMaxTokens =
baseUrl.includes("chutes.ai") || isMoonshot || isCloudflareAiGateway || isTogether || isNvidia || isAntLing || isZai;
baseUrl.includes("chutes.ai") ||
isDeepSeek ||
isMoonshot ||
isCloudflareAiGateway ||
isTogether ||
isNvidia ||
isAntLing ||
isZai;
const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com");
const isOpenRouterDeveloperRoleModel =
isOpenRouter && (model.id.startsWith("anthropic/") || model.id.startsWith("openai/"));
const cacheControlFormat =

View file

@ -1475,8 +1475,10 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
isCloudflareAiGateway ||
isAntLing;
const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com");
const useMaxTokens =
baseUrl.includes("chutes.ai") ||
isDeepSeek ||
isMoonshot ||
isCloudflareAiGateway ||
isTogether ||
@ -1485,7 +1487,6 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet
isZai;
const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
const isDeepSeek = provider === "deepseek" || baseUrl.includes("deepseek.com");
const isOpenRouterDeveloperRoleModel =
isOpenRouter && (model.id.startsWith("anthropic/") || model.id.startsWith("openai/"));
const cacheControlFormat = provider === "openrouter" && model.id.startsWith("anthropic/") ? "anthropic" : undefined;

View file

@ -1429,6 +1429,47 @@ describe("openai-completions tool_choice", () => {
}
});
it("sends max_tokens for built-in and custom DeepSeek API models", async () => {
const customModel = {
...localOpenAICompletionsModel,
id: "custom-deepseek-model",
name: "Custom DeepSeek Model",
provider: "custom-deepseek",
baseUrl: "https://api.deepseek.com",
} satisfies Model<"openai-completions">;
const nativeModels = [
getModel("deepseek", "deepseek-v4-flash")!,
getModel("deepseek", "deepseek-v4-pro")!,
] as const;
const cases = [...nativeModels, customModel] as const;
for (const model of nativeModels) {
expect(model.compat?.maxTokensField).toBe("max_tokens");
}
for (const model of cases) {
let payload: unknown;
await streamSimple(
model,
{
messages: [{ role: "user", content: "Hi", timestamp: Date.now() }],
},
{
apiKey: "test",
maxTokens: 123,
onPayload: (params: unknown) => {
payload = params;
},
},
).result();
const params = (payload ?? mockState.lastParams) as { max_tokens?: number; max_completion_tokens?: number };
expect(params.max_tokens).toBe(123);
expect(params.max_completion_tokens).toBeUndefined();
}
});
it("sends max_tokens for Z.AI completions models", async () => {
const cases = [getModel("zai", "glm-5-turbo")!, getModel("zai", "glm-5.2")!] as const;