mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-30 06:43:13 +00:00
When no custom temperature is set and the model is not a DeepSeek reasoner, the OpenAI compatible provider was hardcoding temperature=0. This overrode whatever server-side default the provider may have. Changed the fallback to undefined so the temperature field is omitted from the API request, letting the remote provider use its own default. Fixes #12042
1008 lines
25 KiB
TypeScript
1008 lines
25 KiB
TypeScript
// npx vitest run api/transform/__tests__/model-params.spec.ts
|
|
|
|
import { type ModelInfo, ANTHROPIC_DEFAULT_MAX_TOKENS } from "@roo-code/types"
|
|
|
|
import { getModelParams } from "../model-params"
|
|
import {
|
|
DEFAULT_HYBRID_REASONING_MODEL_MAX_TOKENS,
|
|
DEFAULT_HYBRID_REASONING_MODEL_THINKING_TOKENS,
|
|
} from "../../../shared/api"
|
|
|
|
describe("getModelParams", () => {
|
|
const baseModel: ModelInfo = {
|
|
contextWindow: 16000,
|
|
supportsPromptCache: true,
|
|
}
|
|
|
|
const anthropicParams = {
|
|
modelId: "test",
|
|
format: "anthropic" as const,
|
|
defaultTemperature: 0,
|
|
}
|
|
|
|
const openaiParams = {
|
|
modelId: "test",
|
|
format: "openai" as const,
|
|
defaultTemperature: 0,
|
|
}
|
|
|
|
const openrouterParams = {
|
|
modelId: "test",
|
|
format: "openrouter" as const,
|
|
defaultTemperature: 0,
|
|
}
|
|
|
|
describe("Basic functionality", () => {
|
|
it("should return default values when no custom values are provided", () => {
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: {},
|
|
model: baseModel,
|
|
defaultTemperature: 0.5,
|
|
})
|
|
|
|
expect(result).toEqual({
|
|
format: anthropicParams.format,
|
|
maxTokens: ANTHROPIC_DEFAULT_MAX_TOKENS,
|
|
temperature: 0.5,
|
|
reasoningEffort: undefined,
|
|
reasoningBudget: undefined,
|
|
reasoning: undefined,
|
|
})
|
|
})
|
|
|
|
it("should use the provided defaultTemperature when no user or model temperature is set", () => {
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: {},
|
|
model: baseModel,
|
|
defaultTemperature: 0,
|
|
})
|
|
|
|
expect(result.temperature).toBe(0)
|
|
})
|
|
|
|
it("should use custom temperature from settings when provided", () => {
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: { modelTemperature: 0.7 },
|
|
model: baseModel,
|
|
defaultTemperature: 0.5,
|
|
})
|
|
|
|
expect(result).toEqual({
|
|
format: anthropicParams.format,
|
|
maxTokens: ANTHROPIC_DEFAULT_MAX_TOKENS,
|
|
temperature: 0.7,
|
|
reasoningEffort: undefined,
|
|
reasoningBudget: undefined,
|
|
reasoning: undefined,
|
|
})
|
|
})
|
|
|
|
it("should handle null temperature in settings", () => {
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: { modelTemperature: null },
|
|
model: baseModel,
|
|
defaultTemperature: 0.5,
|
|
})
|
|
|
|
expect(result.temperature).toBe(0.5)
|
|
})
|
|
|
|
it("should use model defaultTemperature over provider defaultTemperature", () => {
|
|
const modelWithDefaultTemp: ModelInfo = {
|
|
...baseModel,
|
|
defaultTemperature: 0.8,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: {},
|
|
model: modelWithDefaultTemp,
|
|
defaultTemperature: 0.5,
|
|
})
|
|
|
|
expect(result.temperature).toBe(0.8)
|
|
})
|
|
|
|
it("should prefer settings temperature over model defaultTemperature", () => {
|
|
const modelWithDefaultTemp: ModelInfo = {
|
|
...baseModel,
|
|
defaultTemperature: 0.8,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: { modelTemperature: 0.3 },
|
|
model: modelWithDefaultTemp,
|
|
defaultTemperature: 0.5,
|
|
})
|
|
|
|
expect(result.temperature).toBe(0.3)
|
|
})
|
|
|
|
it("should return undefined temperature when defaultTemperature is undefined and no user/model temperature", () => {
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: {},
|
|
model: baseModel,
|
|
defaultTemperature: undefined,
|
|
})
|
|
|
|
expect(result.temperature).toBeUndefined()
|
|
})
|
|
|
|
it("should use model maxTokens when available", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
maxTokens: 2000,
|
|
}
|
|
|
|
expect(getModelParams({ ...anthropicParams, settings: {}, model })).toEqual({
|
|
format: anthropicParams.format,
|
|
maxTokens: 2000,
|
|
temperature: 0,
|
|
reasoningEffort: undefined,
|
|
reasoningBudget: undefined,
|
|
reasoning: undefined,
|
|
})
|
|
})
|
|
|
|
it("should handle null maxTokens in model", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
maxTokens: null,
|
|
}
|
|
|
|
const result = getModelParams({ ...anthropicParams, settings: {}, model })
|
|
expect(result.maxTokens).toBe(ANTHROPIC_DEFAULT_MAX_TOKENS)
|
|
})
|
|
})
|
|
|
|
describe("Format-specific behavior", () => {
|
|
it("should return correct format for anthropic", () => {
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: {},
|
|
model: baseModel,
|
|
})
|
|
|
|
expect(result.format).toBe("anthropic")
|
|
})
|
|
|
|
it("should return correct format for openai", () => {
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: {},
|
|
model: baseModel,
|
|
})
|
|
|
|
expect(result.format).toBe("openai")
|
|
})
|
|
|
|
it("should return correct format for openrouter", () => {
|
|
const result = getModelParams({
|
|
...openrouterParams,
|
|
settings: {},
|
|
model: baseModel,
|
|
})
|
|
|
|
expect(result.format).toBe("openrouter")
|
|
})
|
|
|
|
it("should use ANTHROPIC_DEFAULT_MAX_TOKENS for anthropic format when no maxTokens", () => {
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: {},
|
|
model: baseModel,
|
|
})
|
|
|
|
expect(result.maxTokens).toBe(ANTHROPIC_DEFAULT_MAX_TOKENS)
|
|
})
|
|
|
|
it("should use ANTHROPIC_DEFAULT_MAX_TOKENS for openrouter with anthropic model", () => {
|
|
const result = getModelParams({
|
|
modelId: "anthropic/claude-3-sonnet",
|
|
format: "openrouter" as const,
|
|
settings: {},
|
|
model: baseModel,
|
|
defaultTemperature: 0,
|
|
})
|
|
|
|
expect(result.maxTokens).toBe(ANTHROPIC_DEFAULT_MAX_TOKENS)
|
|
})
|
|
|
|
it("should not force maxTokens for openai format", () => {
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: {},
|
|
model: baseModel,
|
|
})
|
|
|
|
expect(result.maxTokens).toBeUndefined()
|
|
})
|
|
|
|
it("should not force maxTokens for openrouter with non-anthropic model", () => {
|
|
const result = getModelParams({
|
|
modelId: "openai/gpt-4",
|
|
format: "openrouter" as const,
|
|
settings: {},
|
|
model: baseModel,
|
|
defaultTemperature: 0,
|
|
})
|
|
|
|
expect(result.maxTokens).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe("Reasoning Budget (Hybrid reasoning models)", () => {
|
|
it("should handle requiredReasoningBudget models correctly", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
requiredReasoningBudget: true,
|
|
}
|
|
|
|
expect(getModelParams({ ...anthropicParams, settings: { modelMaxTokens: 2000 }, model })).toEqual({
|
|
format: anthropicParams.format,
|
|
maxTokens: 2000,
|
|
temperature: 1.0, // Thinking models require temperature 1.0.
|
|
reasoningEffort: undefined,
|
|
reasoningBudget: 0.8 * 2000,
|
|
reasoning: {
|
|
type: "enabled",
|
|
budget_tokens: 1600,
|
|
},
|
|
})
|
|
})
|
|
|
|
it("should handle supportsReasoningBudget with enableReasoningEffort setting", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
supportsReasoningBudget: true,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: { enableReasoningEffort: true, modelMaxTokens: 2000 },
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoningBudget).toBe(1600) // 80% of 2000
|
|
expect(result.temperature).toBe(1.0)
|
|
expect(result.reasoning).toEqual({
|
|
type: "enabled",
|
|
budget_tokens: 1600,
|
|
})
|
|
})
|
|
|
|
it("should not use reasoning budget when supportsReasoningBudget is true but enableReasoningEffort is false", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
maxTokens: 2000,
|
|
supportsReasoningBudget: true,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: { enableReasoningEffort: false },
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoningBudget).toBeUndefined()
|
|
expect(result.temperature).toBe(0)
|
|
})
|
|
|
|
it("should honor customMaxTokens for reasoning budget models", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
requiredReasoningBudget: true,
|
|
}
|
|
|
|
expect(getModelParams({ ...anthropicParams, settings: { modelMaxTokens: 3000 }, model })).toEqual({
|
|
format: anthropicParams.format,
|
|
maxTokens: 3000,
|
|
temperature: 1.0,
|
|
reasoningEffort: undefined,
|
|
reasoningBudget: 2400, // 80% of 3000,
|
|
reasoning: {
|
|
type: "enabled",
|
|
budget_tokens: 2400,
|
|
},
|
|
})
|
|
})
|
|
|
|
it("should honor customMaxThinkingTokens for reasoning budget models", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
requiredReasoningBudget: true,
|
|
}
|
|
|
|
expect(
|
|
getModelParams({
|
|
...anthropicParams,
|
|
settings: { modelMaxTokens: 4000, modelMaxThinkingTokens: 1500 },
|
|
model,
|
|
}),
|
|
).toEqual({
|
|
format: anthropicParams.format,
|
|
maxTokens: 4000,
|
|
temperature: 1.0,
|
|
reasoningEffort: undefined,
|
|
reasoningBudget: 1500, // Using the custom value.
|
|
reasoning: {
|
|
type: "enabled",
|
|
budget_tokens: 1500,
|
|
},
|
|
})
|
|
})
|
|
|
|
it("should not honor customMaxThinkingTokens for non-reasoning budget models", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
maxTokens: 3000, // 3000 is 18.75% of 16000 context window, within 20% threshold
|
|
}
|
|
|
|
expect(getModelParams({ ...anthropicParams, settings: { modelMaxThinkingTokens: 1500 }, model })).toEqual({
|
|
format: anthropicParams.format,
|
|
maxTokens: 3000, // Uses model.maxTokens since it's within 20% threshold
|
|
temperature: 0, // Using default temperature.
|
|
reasoningEffort: undefined,
|
|
reasoningBudget: undefined, // Should remain undefined despite customMaxThinkingTokens being set.
|
|
reasoning: undefined,
|
|
})
|
|
})
|
|
|
|
it("should clamp thinking budget to at least 1024 tokens", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
requiredReasoningBudget: true,
|
|
}
|
|
|
|
expect(
|
|
getModelParams({
|
|
...anthropicParams,
|
|
settings: { modelMaxTokens: 2000, modelMaxThinkingTokens: 500 },
|
|
model,
|
|
}),
|
|
).toEqual({
|
|
format: anthropicParams.format,
|
|
maxTokens: 2000,
|
|
temperature: 1.0,
|
|
reasoningEffort: undefined,
|
|
reasoningBudget: 1024, // Minimum is 1024
|
|
reasoning: {
|
|
type: "enabled",
|
|
budget_tokens: 1024,
|
|
},
|
|
})
|
|
})
|
|
|
|
it("should clamp Gemini 2.5 Pro thinking budget to at least 128 tokens", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
requiredReasoningBudget: true,
|
|
}
|
|
|
|
expect(
|
|
getModelParams({
|
|
modelId: "gemini-2.5-pro",
|
|
format: "gemini" as const,
|
|
settings: { modelMaxTokens: 2000, modelMaxThinkingTokens: 50 },
|
|
model,
|
|
defaultTemperature: 0,
|
|
}),
|
|
).toEqual({
|
|
format: "gemini",
|
|
maxTokens: 2000,
|
|
temperature: 1.0,
|
|
reasoningEffort: undefined,
|
|
reasoningBudget: 128, // Minimum is 128 for Gemini 2.5 Pro
|
|
reasoning: {
|
|
thinkingBudget: 128,
|
|
includeThoughts: true,
|
|
},
|
|
})
|
|
})
|
|
|
|
it("should use 128 as default thinking budget for Gemini 2.5 Pro", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
requiredReasoningBudget: true,
|
|
}
|
|
|
|
expect(
|
|
getModelParams({
|
|
modelId: "google/gemini-2.5-pro",
|
|
format: "openrouter" as const,
|
|
settings: { modelMaxTokens: 4000 },
|
|
model,
|
|
defaultTemperature: 0,
|
|
}),
|
|
).toEqual({
|
|
format: "openrouter",
|
|
maxTokens: 4000,
|
|
temperature: 1.0,
|
|
reasoningEffort: undefined,
|
|
reasoningBudget: 128, // Default is 128 for Gemini 2.5 Pro
|
|
reasoning: {
|
|
max_tokens: 128,
|
|
},
|
|
})
|
|
})
|
|
|
|
it("should clamp thinking budget to at most 80% of max tokens", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
requiredReasoningBudget: true,
|
|
}
|
|
|
|
expect(
|
|
getModelParams({
|
|
...anthropicParams,
|
|
settings: { modelMaxTokens: 4000, modelMaxThinkingTokens: 5000 },
|
|
model,
|
|
}),
|
|
).toEqual({
|
|
format: anthropicParams.format,
|
|
maxTokens: 4000,
|
|
temperature: 1.0,
|
|
reasoningEffort: undefined,
|
|
reasoningBudget: 0.8 * 4000,
|
|
reasoning: {
|
|
type: "enabled",
|
|
budget_tokens: 3200,
|
|
},
|
|
})
|
|
})
|
|
|
|
it("should use DEFAULT_HYBRID_REASONING_MODEL_MAX_TOKENS when no maxTokens is provided for reasoning budget models", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
requiredReasoningBudget: true,
|
|
}
|
|
|
|
expect(getModelParams({ ...anthropicParams, settings: {}, model })).toEqual({
|
|
format: anthropicParams.format,
|
|
maxTokens: DEFAULT_HYBRID_REASONING_MODEL_MAX_TOKENS,
|
|
temperature: 1.0,
|
|
reasoningEffort: undefined,
|
|
reasoningBudget: DEFAULT_HYBRID_REASONING_MODEL_THINKING_TOKENS,
|
|
reasoning: {
|
|
type: "enabled",
|
|
budget_tokens: DEFAULT_HYBRID_REASONING_MODEL_THINKING_TOKENS,
|
|
},
|
|
})
|
|
})
|
|
|
|
it("should handle both customMaxTokens and customMaxThinkingTokens for reasoning budget models", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
requiredReasoningBudget: true,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: { modelMaxTokens: 5000, modelMaxThinkingTokens: 2000 },
|
|
model,
|
|
})
|
|
|
|
expect(result.maxTokens).toBe(5000)
|
|
expect(result.reasoningBudget).toBe(2000) // Custom thinking tokens takes precedence
|
|
})
|
|
|
|
it("should clamp custom thinking tokens even when custom max tokens is provided", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
requiredReasoningBudget: true,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: { modelMaxTokens: 2000, modelMaxThinkingTokens: 5000 },
|
|
model,
|
|
})
|
|
|
|
expect(result.maxTokens).toBe(2000)
|
|
expect(result.reasoningBudget).toBe(1600) // 80% of 2000, not 5000
|
|
})
|
|
})
|
|
|
|
describe("Reasoning Effort (Traditional reasoning models)", () => {
|
|
it("should handle supportsReasoningEffort with model reasoningEffort", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
supportsReasoningEffort: true,
|
|
reasoningEffort: "medium",
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: {},
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoningEffort).toBe("medium")
|
|
expect(result.reasoningBudget).toBeUndefined()
|
|
expect(result.temperature).toBe(0) // Not forced to 1.0 for reasoning effort models
|
|
expect(result.reasoning).toEqual({ reasoning_effort: "medium" })
|
|
})
|
|
|
|
it("should handle supportsReasoningEffort with settings reasoningEffort", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
supportsReasoningEffort: true,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: { reasoningEffort: "high" },
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoningEffort).toBe("high")
|
|
expect(result.reasoning).toEqual({ reasoning_effort: "high" })
|
|
})
|
|
|
|
it("should prefer settings reasoningEffort over model reasoningEffort", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
supportsReasoningEffort: true,
|
|
reasoningEffort: "low",
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: { reasoningEffort: "high" },
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoningEffort).toBe("high")
|
|
expect(result.reasoning).toEqual({ reasoning_effort: "high" })
|
|
})
|
|
|
|
it("should not use reasoning effort when supportsReasoningEffort is true but no effort is specified", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
supportsReasoningEffort: true,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: {},
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoningEffort).toBeUndefined()
|
|
})
|
|
|
|
it("should handle reasoning effort for openrouter format", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
supportsReasoningEffort: true,
|
|
reasoningEffort: "medium",
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openrouterParams,
|
|
settings: {},
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoningEffort).toBe("medium")
|
|
expect(result.reasoning).toEqual({ effort: "medium" })
|
|
})
|
|
|
|
it("should include 'minimal' effort for openai format", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
// Array capability explicitly includes minimal
|
|
supportsReasoningEffort: ["minimal", "low", "medium", "high"] as any,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: { reasoningEffort: "minimal" as any },
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoningEffort).toBe("minimal")
|
|
expect(result.reasoning).toEqual({ reasoning_effort: "minimal" })
|
|
})
|
|
|
|
it("should include 'none' effort for openai format", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
// Array capability explicitly includes none
|
|
supportsReasoningEffort: ["none", "low", "medium", "high"] as any,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: { reasoningEffort: "none" as any },
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoningEffort).toBe("none")
|
|
expect(result.reasoning).toEqual({ reasoning_effort: "none" })
|
|
})
|
|
|
|
it("should omit reasoning for 'disable' selection", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
supportsReasoningEffort: true,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: { reasoningEffort: "disable" as any },
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoningEffort).toBeUndefined()
|
|
})
|
|
|
|
it("should include 'minimal' and 'none' for openrouter format", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
// Array capability explicitly includes both
|
|
supportsReasoningEffort: ["none", "minimal", "low", "medium", "high"] as any,
|
|
}
|
|
|
|
const minimalRes = getModelParams({
|
|
...openrouterParams,
|
|
settings: { reasoningEffort: "minimal" as any },
|
|
model,
|
|
})
|
|
expect(minimalRes.reasoningEffort).toBe("minimal")
|
|
expect(minimalRes.reasoning).toEqual({ effort: "minimal" })
|
|
|
|
const noneRes = getModelParams({
|
|
...openrouterParams,
|
|
settings: { reasoningEffort: "none" as any },
|
|
model,
|
|
})
|
|
expect(noneRes.reasoningEffort).toBe("none")
|
|
expect(noneRes.reasoning).toEqual({ effort: "none" })
|
|
})
|
|
it("should not use reasoning effort for anthropic format", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
supportsReasoningEffort: true,
|
|
reasoningEffort: "medium",
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: {},
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoningEffort).toBe("medium")
|
|
expect(result.reasoning).toBeUndefined() // Anthropic doesn't support reasoning effort
|
|
})
|
|
|
|
it("should use reasoningEffort if supportsReasoningEffort is false but reasoningEffort is set", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
maxTokens: 3000, // 3000 is 18.75% of 16000, within 20% threshold
|
|
supportsReasoningEffort: false,
|
|
reasoningEffort: "medium",
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: {},
|
|
model,
|
|
})
|
|
|
|
expect(result.maxTokens).toBe(3000)
|
|
// Now uses model.maxTokens since it's within 20% threshold
|
|
expect(result.reasoningEffort).toBe("medium")
|
|
})
|
|
})
|
|
|
|
describe("Hybrid reasoning models (supportsReasoningEffort)", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
maxTokens: 8000,
|
|
supportsReasoningBudget: true,
|
|
}
|
|
|
|
it("should use ANTHROPIC_DEFAULT_MAX_TOKENS for hybrid models when not using reasoning", () => {
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: {},
|
|
model,
|
|
})
|
|
|
|
// For hybrid models (supportsReasoningBudget) in Anthropic contexts,
|
|
// should discard model's maxTokens and use ANTHROPIC_DEFAULT_MAX_TOKENS
|
|
expect(result.maxTokens).toBe(ANTHROPIC_DEFAULT_MAX_TOKENS)
|
|
expect(result.reasoningBudget).toBeUndefined()
|
|
})
|
|
|
|
it("should keep model maxTokens for hybrid models when using reasoning budget", () => {
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: { enableReasoningEffort: true },
|
|
model,
|
|
})
|
|
|
|
expect(result.maxTokens).toBe(16384) // Default value.
|
|
expect(result.reasoningBudget).toBe(8192) // Default value.
|
|
})
|
|
})
|
|
|
|
describe("Edge cases and combinations", () => {
|
|
it("should handle model with both reasoning capabilities but only one enabled", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
supportsReasoningBudget: true,
|
|
supportsReasoningEffort: true,
|
|
reasoningEffort: "medium",
|
|
}
|
|
|
|
// Only reasoning budget should be used (takes precedence)
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: { enableReasoningEffort: true, modelMaxTokens: 4000 },
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoningBudget).toBe(3200) // 80% of 4000
|
|
expect(result.reasoningEffort).toBeUndefined() // Budget takes precedence
|
|
expect(result.temperature).toBe(1.0)
|
|
})
|
|
|
|
it("should handle zero maxTokens", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
maxTokens: 0,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: {},
|
|
model,
|
|
})
|
|
|
|
expect(result.maxTokens).toBe(ANTHROPIC_DEFAULT_MAX_TOKENS) // Should fallback for anthropic
|
|
})
|
|
|
|
it("should handle very small maxTokens for reasoning budget models", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
requiredReasoningBudget: true,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: { modelMaxTokens: 1000 }, // Less than minimum reasoning budget.
|
|
model,
|
|
})
|
|
|
|
expect(result.maxTokens).toBe(1000)
|
|
expect(result.reasoningBudget).toBe(1024) // Clamped to minimum.
|
|
})
|
|
|
|
it("should handle undefined settings", () => {
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: {},
|
|
model: baseModel,
|
|
})
|
|
|
|
expect(result.temperature).toBe(0)
|
|
expect(result.maxTokens).toBe(ANTHROPIC_DEFAULT_MAX_TOKENS)
|
|
})
|
|
|
|
it("should handle all reasoning effort values", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
supportsReasoningEffort: true,
|
|
}
|
|
|
|
const efforts: Array<"low" | "medium" | "high"> = ["low", "medium", "high"]
|
|
|
|
efforts.forEach((effort) => {
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: { reasoningEffort: effort },
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoningEffort).toBe(effort)
|
|
expect(result.reasoning).toEqual({ reasoning_effort: effort })
|
|
})
|
|
})
|
|
|
|
it("should handle complex model configuration", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
maxTokens: 16000,
|
|
maxThinkingTokens: 8000,
|
|
supportsReasoningBudget: true,
|
|
supportsReasoningEffort: true,
|
|
reasoningEffort: "low",
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: {
|
|
enableReasoningEffort: true,
|
|
modelMaxTokens: 20000,
|
|
modelMaxThinkingTokens: 10000,
|
|
modelTemperature: 0.8,
|
|
},
|
|
model,
|
|
})
|
|
|
|
expect(result.maxTokens).toBe(20000)
|
|
expect(result.reasoningBudget).toBe(10000)
|
|
expect(result.temperature).toBe(1.0) // Overridden for reasoning budget models
|
|
expect(result.reasoningEffort).toBeUndefined() // Budget takes precedence
|
|
})
|
|
})
|
|
|
|
describe("Provider-specific reasoning behavior", () => {
|
|
it("should return correct reasoning format for openai with reasoning effort", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
supportsReasoningEffort: true,
|
|
reasoningEffort: "medium",
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: {},
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoning).toEqual({ reasoning_effort: "medium" })
|
|
})
|
|
|
|
it("should return correct reasoning format for openrouter with reasoning effort", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
supportsReasoningEffort: true,
|
|
reasoningEffort: "high",
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openrouterParams,
|
|
settings: {},
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoning).toEqual({ effort: "high" })
|
|
})
|
|
|
|
it("should return correct reasoning format for openrouter with reasoning budget", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
requiredReasoningBudget: true,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openrouterParams,
|
|
settings: { modelMaxTokens: 4000 },
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoning).toEqual({ max_tokens: 3200 })
|
|
})
|
|
|
|
it("should return undefined reasoning for anthropic with reasoning effort", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
supportsReasoningEffort: true,
|
|
reasoningEffort: "medium",
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: {},
|
|
model,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("Verbosity settings", () => {
|
|
it("should include verbosity when specified in settings", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: { verbosity: "low" },
|
|
model,
|
|
})
|
|
|
|
expect(result.verbosity).toBe("low")
|
|
})
|
|
|
|
it("should handle medium verbosity", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: { verbosity: "medium" },
|
|
model,
|
|
})
|
|
|
|
expect(result.verbosity).toBe("medium")
|
|
})
|
|
|
|
it("should handle high verbosity", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: { verbosity: "high" },
|
|
model,
|
|
})
|
|
|
|
expect(result.verbosity).toBe("high")
|
|
})
|
|
|
|
it("should return undefined verbosity when not specified", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: {},
|
|
model,
|
|
})
|
|
|
|
expect(result.verbosity).toBeUndefined()
|
|
})
|
|
|
|
it("should include verbosity alongside reasoning settings", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
supportsReasoningEffort: true,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...openaiParams,
|
|
settings: {
|
|
reasoningEffort: "high",
|
|
verbosity: "low",
|
|
},
|
|
model,
|
|
})
|
|
|
|
expect(result.reasoningEffort).toBe("high")
|
|
expect(result.verbosity).toBe("low")
|
|
expect(result.reasoning).toEqual({ reasoning_effort: "high" })
|
|
})
|
|
|
|
it("should include verbosity with reasoning budget models", () => {
|
|
const model: ModelInfo = {
|
|
...baseModel,
|
|
supportsReasoningBudget: true,
|
|
}
|
|
|
|
const result = getModelParams({
|
|
...anthropicParams,
|
|
settings: {
|
|
enableReasoningEffort: true,
|
|
verbosity: "high",
|
|
},
|
|
model,
|
|
})
|
|
|
|
expect(result.verbosity).toBe("high")
|
|
expect(result.reasoningBudget).toBe(8192) // Default thinking tokens
|
|
})
|
|
})
|
|
})
|