Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from "zod"
import { providerDefinitionList, type ProviderDefinition } from "./providers/index.js"
import { API_PROVIDER_FIELD, SETTINGS_SHAPE_FIELD } from "./providers/common.js"
export {
DEFAULT_OPEN_AI_INCLUDE_MAX_TOKENS,
DEFAULT_OPEN_AI_STRICT_TOOL_SCHEMAS,
OPEN_AI_CODEX_SERVICE_TIER_KEY,
parseOpenAiExtraBody,
Expand Down
7 changes: 7 additions & 0 deletions packages/types/src/providers/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,13 @@
*/
export const DEFAULT_OPEN_AI_STRICT_TOOL_SCHEMAS = true

/**
* Default for the OpenAI Compatible "Include max output tokens" checkbox.
* The settings UI and the request builder must both read this so an unset
* value is shown and sent the same way.
*/
export const DEFAULT_OPEN_AI_INCLUDE_MAX_TOKENS = true

Check warning on line 1004 in packages/types/src/providers/openai.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

packages/types/src/providers/openai.ts:1004: Survived BooleanLiteral mutant (replacement: false). See the job summary for the complete list and resolution guidance.

const OPENAI_EXTRA_BODY_RESERVED_KEYS = [
// Prototype-pollution defenses; remaining keys are request-owned, including tool-call protocol controls.
"__proto__",
Expand Down
16 changes: 16 additions & 0 deletions src/api/providers/__tests__/kimi-code.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@ describe("KimiCodeHandler", () => {
expect(handler.getModel().id).toBe("kimi-for-coding")
})

it.each([
{ includeMaxTokens: undefined, expected: false },
{ includeMaxTokens: false, expected: false },
{ includeMaxTokens: true, expected: true },
])(
"keeps max output tokens opt-in when includeMaxTokens is $includeMaxTokens",
({ includeMaxTokens, expected }) => {
const handler = new KimiCodeHandler({
kimiCodeAuthMethod: "api-key",
kimiCodeApiKey: "key",
includeMaxTokens,
})
expect(handler["options"].includeMaxTokens).toBe(expected)
},
)

it("uses API key when auth method is api-key", async () => {
const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "my-api-key" })
const gen = handler.createMessage("system", [{ role: "user", content: "test" }])
Expand Down
47 changes: 28 additions & 19 deletions src/api/providers/__tests__/openai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -687,25 +687,34 @@ describe("OpenAiHandler", () => {
expect(callArgs.max_completion_tokens).toBeUndefined()
})

it("should not include max_tokens when includeMaxTokens is undefined", async () => {
const optionsWithUndefinedMaxTokens: ApiHandlerOptions = {
...mockOptions,
// includeMaxTokens is not set, should not include max_tokens
openAiCustomModelInfo: {
contextWindow: 128_000,
maxTokens: 4096,
supportsPromptCache: false,
},
}
const handlerWithDefaultMaxTokens = new OpenAiHandler(optionsWithUndefinedMaxTokens)
const stream = handlerWithDefaultMaxTokens.createMessage(systemPrompt, messages)
// Consume the stream to trigger the API call
await collectStream(stream)
// Assert the mockCreate was called without max_tokens
expect(mockCreate).toHaveBeenCalled()
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.max_completion_tokens).toBeUndefined()
})
it.each([
{ label: "unset (UI shows it checked)", includeMaxTokens: undefined, expected: 4096 },
{ label: "true", includeMaxTokens: true, expected: 4096 },
{ label: "false", includeMaxTokens: false, expected: undefined },
])(
"sends max_completion_tokens matching the checkbox when includeMaxTokens is $label",
async ({ includeMaxTokens, expected }) => {
const customModelInfo = { contextWindow: 128_000, maxTokens: 4096, supportsPromptCache: false }
const streamingHandler = new OpenAiHandler({
...mockOptions,
includeMaxTokens,
openAiCustomModelInfo: customModelInfo,
})
await collectStream(streamingHandler.createMessage(systemPrompt, messages))
const nonStreamingHandler = new OpenAiHandler({
...mockOptions,
includeMaxTokens,
openAiStreamingEnabled: false,
openAiCustomModelInfo: customModelInfo,
})
await collectStream(nonStreamingHandler.createMessage(systemPrompt, messages))

expect(mockCreate).toHaveBeenCalledTimes(2)
for (const [callArgs] of mockCreate.mock.calls) {
expect(callArgs.max_completion_tokens).toBe(expected)
}
},
)

it("should use user-configured modelMaxTokens instead of model default maxTokens", async () => {
const optionsWithUserMaxTokens: ApiHandlerOptions = {
Expand Down
3 changes: 3 additions & 0 deletions src/api/providers/kimi-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export class KimiCodeHandler extends OpenAiHandler {
openAiApiKey: options.kimiCodeApiKey ?? NOT_PROVIDED,
openAiModelId: options.apiModelId ?? kimiCodeDefaultModelId,
openAiStreamingEnabled: true,
// Kimi Code has no "Include max output tokens" control, so a value left
// in the profile by OpenAI Compatible must not opt it in by default.
includeMaxTokens: options.includeMaxTokens === true,
})
this.kimiOptions = options
}
Expand Down
10 changes: 7 additions & 3 deletions src/api/providers/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
isAzureOpenAiBaseUrl,
openAiModelInfoSaneDefaults,
DEEP_SEEK_DEFAULT_TEMPERATURE,
DEFAULT_OPEN_AI_INCLUDE_MAX_TOKENS,
DEFAULT_OPEN_AI_STRICT_TOOL_SCHEMAS,
OPENAI_AZURE_AI_INFERENCE_PATH,
parseOpenAiExtraBody,
Expand Down Expand Up @@ -596,11 +597,14 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
| OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming,
modelInfo: ModelInfo,
): void {
// Only add max_completion_tokens if includeMaxTokens is true
if (this.options.includeMaxTokens === true) {
// Unset means the default (checked in the UI); only an explicit false opts out
if (this.options.includeMaxTokens ?? DEFAULT_OPEN_AI_INCLUDE_MAX_TOKENS) {
// Use user-configured modelMaxTokens if available, otherwise fall back to model's default maxTokens
// Using max_completion_tokens as max_tokens is deprecated
requestOptions.max_completion_tokens = this.options.modelMaxTokens || modelInfo.maxTokens
const maxTokens = this.options.modelMaxTokens || modelInfo.maxTokens
if (maxTokens) {
requestOptions.max_completion_tokens = maxTokens
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
azureOpenAiDefaultApiVersion,
isAzureOpenAiBaseUrl,
openAiModelInfoSaneDefaults,
DEFAULT_OPEN_AI_INCLUDE_MAX_TOKENS,
DEFAULT_OPEN_AI_STRICT_TOOL_SCHEMAS,
OpenAiModelsMessageType,
parseOpenAiExtraBody,
Expand Down Expand Up @@ -182,7 +183,7 @@ export const OpenAICompatible = ({
</Checkbox>
<div>
<Checkbox
checked={apiConfiguration.includeMaxTokens ?? true}
checked={apiConfiguration.includeMaxTokens ?? DEFAULT_OPEN_AI_INCLUDE_MAX_TOKENS}
onChange={handleInputChange("includeMaxTokens", noTransform)}>
{t("settings:includeMaxOutputTokens")}
</Checkbox>
Expand Down
Loading