Skip to content
Merged
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
26 changes: 21 additions & 5 deletions apps/sim/providers/anthropic/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ const THINKING_BUDGET_TOKENS: Record<string, number> = {
high: 32768,
}

/** Anthropic's documented floor for `budget_tokens` (Messages API reference: "Must be >=1024 and less than max_tokens"). */
const ANTHROPIC_MIN_BUDGET_TOKENS = 1024

/** Headroom reserved for text output above the thinking budget when computing max_tokens. */
const ANTHROPIC_THINKING_OUTPUT_HEADROOM = 4096

/**
* Checks if a model supports adaptive thinking (thinking.type: "adaptive").
* Fable 5 supports ONLY adaptive thinking (always on; type: "disabled" is rejected).
Expand Down Expand Up @@ -338,16 +344,26 @@ export async function executeAnthropicProviderRequest(
payload.output_config = thinkingConfig.outputConfig
}

// Per Anthropic docs: budget_tokens must be less than max_tokens.
// Ensure max_tokens leaves room for both thinking and text output.
// Keep budget_tokens < max_tokens (see constants above) by shrinking the budget
// itself when the model's output cap is too tight — clamping max_tokens alone
// can leave budget_tokens >= max_tokens.
if (
thinkingConfig.thinking.type === 'enabled' &&
'budget_tokens' in thinkingConfig.thinking
) {
const budgetTokens = thinkingConfig.thinking.budget_tokens
const minMaxTokens = budgetTokens + 4096
const modelMax = getMaxOutputTokensForModel(request.model)
let budgetTokens = thinkingConfig.thinking.budget_tokens

if (budgetTokens + ANTHROPIC_THINKING_OUTPUT_HEADROOM > modelMax) {
budgetTokens = Math.max(
ANTHROPIC_MIN_BUDGET_TOKENS,
modelMax - ANTHROPIC_THINKING_OUTPUT_HEADROOM
)
thinkingConfig.thinking.budget_tokens = budgetTokens
}

const minMaxTokens = budgetTokens + ANTHROPIC_THINKING_OUTPUT_HEADROOM
if (payload.max_tokens < minMaxTokens) {
const modelMax = getMaxOutputTokensForModel(request.model)
payload.max_tokens = Math.min(minMaxTokens, modelMax)
logger.info(
`Adjusted max_tokens to ${payload.max_tokens} to satisfy budget_tokens (${budgetTokens}) constraint`
Expand Down
20 changes: 16 additions & 4 deletions apps/sim/providers/gemini/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import {
ensureStructResponse,
extractAllFunctionCallParts,
extractTextContent,
mapToThinkingBudget,
mapToThinkingLevel,
supportsDisablingGemini25Thinking,
} from '@/providers/google/utils'
import { enrichLastModelSegment } from '@/providers/trace-enrichment'
import type {
Expand Down Expand Up @@ -952,13 +954,23 @@ export async function executeGeminiRequest(
)
}

// Configure thinking only when the user explicitly selects a thinking level
// Gemini 3.x takes thinkingLevel directly; Gemini 2.5-series rejects it and needs thinkingBudget.
if (request.thinkingLevel && request.thinkingLevel !== 'none') {
const thinkingConfig: ThinkingConfig = {
includeThoughts: false,
thinkingLevel: mapToThinkingLevel(request.thinkingLevel),
const thinkingConfig: ThinkingConfig = { includeThoughts: false }
if (isGemini3Model(model)) {
thinkingConfig.thinkingLevel = mapToThinkingLevel(request.thinkingLevel)
} else {
thinkingConfig.thinkingBudget = mapToThinkingBudget(model, request.thinkingLevel)
}
geminiConfig.thinkingConfig = thinkingConfig
} else if (
request.thinkingLevel === 'none' &&
!isGemini3Model(model) &&
supportsDisablingGemini25Thinking(model)
) {
// Omitting thinkingConfig falls back to the API's dynamic default (ON for gemini-2.5-flash),
// so disabling requires an explicit budget of 0.
geminiConfig.thinkingConfig = { includeThoughts: false, thinkingBudget: 0 }
}

// Prepare tools
Expand Down
60 changes: 59 additions & 1 deletion apps/sim/providers/google/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,67 @@
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { convertToGeminiFormat, ensureStructResponse } from '@/providers/google/utils'
import {
convertToGeminiFormat,
ensureStructResponse,
mapToThinkingBudget,
supportsDisablingGemini25Thinking,
} from '@/providers/google/utils'
import type { ProviderRequest } from '@/providers/types'

describe('mapToThinkingBudget', () => {
it('maps named levels to a within-range budget for gemini-2.5-pro (128-32768, cannot disable)', () => {
expect(mapToThinkingBudget('gemini-2.5-pro', 'low')).toBeGreaterThanOrEqual(128)
expect(mapToThinkingBudget('gemini-2.5-pro', 'high')).toBeLessThanOrEqual(32768)
})

it('maps named levels to a within-range budget for gemini-2.5-flash (0-24576)', () => {
expect(mapToThinkingBudget('gemini-2.5-flash', 'low')).toBeLessThanOrEqual(24576)
expect(mapToThinkingBudget('gemini-2.5-flash', 'high')).toBeLessThanOrEqual(24576)
})

it('maps named levels to a within-range budget for gemini-2.5-flash-lite (512-24576)', () => {
expect(mapToThinkingBudget('gemini-2.5-flash-lite', 'low')).toBeGreaterThanOrEqual(512)
expect(mapToThinkingBudget('gemini-2.5-flash-lite', 'high')).toBeLessThanOrEqual(24576)
})

it('strips the vertex/ prefix before looking up the model', () => {
expect(mapToThinkingBudget('vertex/gemini-2.5-flash', 'medium')).toBe(
mapToThinkingBudget('gemini-2.5-flash', 'medium')
)
})

it('falls back to dynamic budget (-1) for models with no explicit mapping', () => {
expect(mapToThinkingBudget('gemini-2.0-flash', 'medium')).toBe(-1)
})

it('falls back to the high budget for an unrecognized level on a mapped model', () => {
expect(mapToThinkingBudget('gemini-2.5-flash', 'unknown-level')).toBe(
mapToThinkingBudget('gemini-2.5-flash', 'high')
)
})
})

describe('supportsDisablingGemini25Thinking', () => {
it('returns true for gemini-2.5-flash and gemini-2.5-flash-lite', () => {
expect(supportsDisablingGemini25Thinking('gemini-2.5-flash')).toBe(true)
expect(supportsDisablingGemini25Thinking('gemini-2.5-flash-lite')).toBe(true)
})

it('returns false for gemini-2.5-pro, which cannot disable thinking', () => {
expect(supportsDisablingGemini25Thinking('gemini-2.5-pro')).toBe(false)
})

it('strips the vertex/ prefix before checking', () => {
expect(supportsDisablingGemini25Thinking('vertex/gemini-2.5-flash')).toBe(true)
expect(supportsDisablingGemini25Thinking('vertex/gemini-2.5-pro')).toBe(false)
})

it('returns false for models with no explicit mapping', () => {
expect(supportsDisablingGemini25Thinking('gemini-3.5-flash')).toBe(false)
})
})

describe('ensureStructResponse', () => {
describe('should return objects unchanged', () => {
it('should return plain object unchanged', () => {
Expand Down
41 changes: 41 additions & 0 deletions apps/sim/providers/google/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,47 @@ export function mapToThinkingLevel(level: string): ThinkingLevel {
}
}

/**
* Per-model thinkingBudget ranges for Gemini 2.5-series models. Unlike Gemini 3.x, these
* models reject `thinkingLevel` entirely (Gemini API docs: "Gemini 2.5 series models don't
* support thinkingLevel; use thinkingBudget instead") and require a numeric token budget
* within each model's own documented range.
*/
const GEMINI_25_THINKING_BUDGETS: Record<string, Record<string, number>> = {
'gemini-2.5-pro': { low: 2048, medium: 8192, high: 32768 }, // valid range 128-32768, cannot disable
'gemini-2.5-flash': { low: 2048, medium: 8192, high: 24576 }, // valid range 0-24576
'gemini-2.5-flash-lite': { low: 1024, medium: 8192, high: 24576 }, // valid range 512-24576
}

/**
* Maps a named thinking level to a `thinkingBudget` token count for Gemini 2.5-series models.
* Falls back to -1 (dynamic/automatic budget) for any model not in the explicit table above,
* rather than guessing a number that could fall outside an unmapped model's valid range.
*/
export function mapToThinkingBudget(model: string, level: string): number {
const normalized = model.toLowerCase().replace(/^vertex\//, '')
const budgets = GEMINI_25_THINKING_BUDGETS[normalized]
if (!budgets) return -1
return budgets[level.toLowerCase()] ?? budgets.high
}

/**
* Gemini 2.5-series models that accept `thinkingBudget: 0` to explicitly disable thinking.
* gemini-2.5-pro cannot disable thinking at all (its documented budget floor is 128, not 0),
* so it's deliberately excluded here.
*/
const GEMINI_25_MODELS_SUPPORTING_DISABLE = new Set(['gemini-2.5-flash', 'gemini-2.5-flash-lite'])

/**
* Whether this Gemini 2.5-series model supports explicitly disabling thinking via budget=0.
* Omitting thinkingConfig entirely (the 'none' no-op path) falls back to the API's own
* dynamic default, which is ON for gemini-2.5-flash — not the same as actually disabling it.
*/
export function supportsDisablingGemini25Thinking(model: string): boolean {
const normalized = model.toLowerCase().replace(/^vertex\//, '')
return GEMINI_25_MODELS_SUPPORTING_DISABLE.has(normalized)
}

/**
* Result of checking forced tool usage
*/
Expand Down
32 changes: 28 additions & 4 deletions apps/sim/providers/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
},
capabilities: {
reasoningEffort: {
values: ['none', 'low', 'medium', 'high', 'xhigh', 'max'],
values: ['none', 'low', 'medium', 'high', 'xhigh'],
},
verbosity: {
values: ['low', 'medium', 'high'],
Expand All @@ -335,7 +335,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
},
capabilities: {
reasoningEffort: {
values: ['none', 'low', 'medium', 'high', 'xhigh', 'max'],
values: ['none', 'low', 'medium', 'high', 'xhigh'],
},
verbosity: {
values: ['low', 'medium', 'high'],
Expand Down Expand Up @@ -841,7 +841,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
capabilities: {
temperature: { min: 0, max: 1 },
nativeStructuredOutputs: true,
maxOutputTokens: 64000,
maxOutputTokens: 128000,
thinking: {
levels: ['low', 'medium', 'high', 'max'],
default: 'high',
Expand Down Expand Up @@ -1509,7 +1509,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
},
contextWindow: 1048576,
releaseDate: '2025-12-17',
deprecated: true,
},
{
id: 'gemini-2.5-pro',
Expand All @@ -1521,6 +1520,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
},
capabilities: {
temperature: { min: 0, max: 2 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'high',
},
maxOutputTokens: 65536,
},
contextWindow: 1048576,
Expand All @@ -1536,6 +1539,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
},
capabilities: {
temperature: { min: 0, max: 2 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'medium',
},
maxOutputTokens: 65536,
},
contextWindow: 1048576,
Expand All @@ -1551,6 +1558,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
},
capabilities: {
temperature: { min: 0, max: 2 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'low',
},
maxOutputTokens: 65536,
},
contextWindow: 1048576,
Expand Down Expand Up @@ -1725,6 +1736,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
},
capabilities: {
temperature: { min: 0, max: 2 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'high',
},
maxOutputTokens: 65535,
},
contextWindow: 1048576,
Expand All @@ -1740,6 +1755,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
},
capabilities: {
temperature: { min: 0, max: 2 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'medium',
},
maxOutputTokens: 65535,
},
contextWindow: 1048576,
Expand All @@ -1755,6 +1774,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
},
capabilities: {
temperature: { min: 0, max: 2 },
thinking: {
levels: ['low', 'medium', 'high'],
default: 'low',
},
maxOutputTokens: 65535,
},
contextWindow: 1048576,
Expand Down Expand Up @@ -2876,6 +2899,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
},
contextWindow: 200000,
releaseDate: '2025-08-05',
deprecated: true,
},
{
id: 'bedrock/amazon.nova-2-pro-v1:0',
Expand Down
Loading
Loading