Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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: 22 additions & 4 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,28 @@ export async function executeAnthropicProviderRequest(
payload.output_config = thinkingConfig.outputConfig
}

// Per Anthropic docs: budget_tokens must be less than max_tokens.
// Per Anthropic docs: budget_tokens must be >= 1024 and less than max_tokens.
// Ensure max_tokens leaves room for both thinking and text output.
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 this level's budget doesn't leave room for text output within the
// model's own output cap, shrink the budget itself (not just max_tokens) —
// otherwise clamping max_tokens alone can leave budget_tokens >= max_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
22 changes: 18 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,25 @@ export async function executeGeminiRequest(
)
}

// Configure thinking only when the user explicitly selects a thinking level
// Configure thinking based on the user's selected level.
// Gemini 3.x accepts thinkingLevel directly; Gemini 2.5-series rejects thinkingLevel
// entirely and requires a numeric thinkingBudget instead.
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 entirely falls back to the API's own dynamic default, which
// is ON for gemini-2.5-flash — an explicit budget of 0 is required to actually disable it.
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