/ utils / thinking.ts
thinking.ts
  1  // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
  2  import type { Theme } from './theme.js'
  3  import { feature } from 'bun:bundle'
  4  import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
  5  import { getCanonicalName } from './model/model.js'
  6  import { get3PModelCapabilityOverride } from './model/modelSupportOverrides.js'
  7  import { getAPIProvider } from './model/providers.js'
  8  import { getSettingsWithErrors } from './settings/settings.js'
  9  
 10  export type ThinkingConfig =
 11    | { type: 'adaptive' }
 12    | { type: 'enabled'; budgetTokens: number }
 13    | { type: 'disabled' }
 14  
 15  /**
 16   * Build-time gate (feature) + runtime gate (GrowthBook). The build flag
 17   * controls code inclusion in external builds; the GB flag controls rollout.
 18   */
 19  export function isUltrathinkEnabled(): boolean {
 20    if (!feature('ULTRATHINK')) {
 21      return false
 22    }
 23    return getFeatureValue_CACHED_MAY_BE_STALE('tengu_turtle_carbon', true)
 24  }
 25  
 26  /**
 27   * Check if text contains the "ultrathink" keyword.
 28   */
 29  export function hasUltrathinkKeyword(text: string): boolean {
 30    return /\bultrathink\b/i.test(text)
 31  }
 32  
 33  /**
 34   * Find positions of "ultrathink" keyword in text (for UI highlighting/notification)
 35   */
 36  export function findThinkingTriggerPositions(text: string): Array<{
 37    word: string
 38    start: number
 39    end: number
 40  }> {
 41    const positions: Array<{ word: string; start: number; end: number }> = []
 42    // Fresh /g literal each call — String.prototype.matchAll copies lastIndex
 43    // from the source regex, so a shared instance would leak state from
 44    // hasUltrathinkKeyword's .test() into this call on the next render.
 45    const matches = text.matchAll(/\bultrathink\b/gi)
 46  
 47    for (const match of matches) {
 48      if (match.index !== undefined) {
 49        positions.push({
 50          word: match[0],
 51          start: match.index,
 52          end: match.index + match[0].length,
 53        })
 54      }
 55    }
 56  
 57    return positions
 58  }
 59  
 60  const RAINBOW_COLORS: Array<keyof Theme> = [
 61    'rainbow_red',
 62    'rainbow_orange',
 63    'rainbow_yellow',
 64    'rainbow_green',
 65    'rainbow_blue',
 66    'rainbow_indigo',
 67    'rainbow_violet',
 68  ]
 69  
 70  const RAINBOW_SHIMMER_COLORS: Array<keyof Theme> = [
 71    'rainbow_red_shimmer',
 72    'rainbow_orange_shimmer',
 73    'rainbow_yellow_shimmer',
 74    'rainbow_green_shimmer',
 75    'rainbow_blue_shimmer',
 76    'rainbow_indigo_shimmer',
 77    'rainbow_violet_shimmer',
 78  ]
 79  
 80  export function getRainbowColor(
 81    charIndex: number,
 82    shimmer: boolean = false,
 83  ): keyof Theme {
 84    const colors = shimmer ? RAINBOW_SHIMMER_COLORS : RAINBOW_COLORS
 85    return colors[charIndex % colors.length]!
 86  }
 87  
 88  // TODO(inigo): add support for probing unknown models via API error detection
 89  // Provider-aware thinking support detection (aligns with modelSupportsISP in betas.ts)
 90  export function modelSupportsThinking(model: string): boolean {
 91    const supported3P = get3PModelCapabilityOverride(model, 'thinking')
 92    if (supported3P !== undefined) {
 93      return supported3P
 94    }
 95    if (process.env.USER_TYPE === 'ant') {
 96      if (resolveAntModel(model.toLowerCase())) {
 97        return true
 98      }
 99    }
100    // IMPORTANT: Do not change thinking support without notifying the model
101    // launch DRI and research. This can greatly affect model quality and bashing.
102    const canonical = getCanonicalName(model)
103    const provider = getAPIProvider()
104    // 1P and Foundry: all Claude 4+ models (including Haiku 4.5)
105    if (provider === 'foundry' || provider === 'firstParty') {
106      return !canonical.includes('claude-3-')
107    }
108    // 3P (Bedrock/Vertex): only Opus 4+ and Sonnet 4+
109    return canonical.includes('sonnet-4') || canonical.includes('opus-4')
110  }
111  
112  // @[MODEL LAUNCH]: Add the new model to the allowlist if it supports adaptive thinking.
113  export function modelSupportsAdaptiveThinking(model: string): boolean {
114    const supported3P = get3PModelCapabilityOverride(model, 'adaptive_thinking')
115    if (supported3P !== undefined) {
116      return supported3P
117    }
118    const canonical = getCanonicalName(model)
119    // Supported by a subset of Claude 4 models
120    if (canonical.includes('opus-4-6') || canonical.includes('sonnet-4-6')) {
121      return true
122    }
123    // Exclude any other known legacy models (allowlist above catches 4-6 variants first)
124    if (
125      canonical.includes('opus') ||
126      canonical.includes('sonnet') ||
127      canonical.includes('haiku')
128    ) {
129      return false
130    }
131    // IMPORTANT: Do not change adaptive thinking support without notifying the
132    // model launch DRI and research. This can greatly affect model quality and
133    // bashing.
134  
135    // Newer models (4.6+) are all trained on adaptive thinking and MUST have it
136    // enabled for model testing. DO NOT default to false for first party, otherwise
137    // we may silently degrade model quality.
138  
139    // Default to true for unknown model strings on 1P and Foundry (because Foundry
140    // is a proxy). Do not default to true for other 3P as they have different formats
141    // for their model strings.
142    const provider = getAPIProvider()
143    return provider === 'firstParty' || provider === 'foundry'
144  }
145  
146  export function shouldEnableThinkingByDefault(): boolean {
147    if (process.env.MAX_THINKING_TOKENS) {
148      return parseInt(process.env.MAX_THINKING_TOKENS, 10) > 0
149    }
150  
151    const { settings } = getSettingsWithErrors()
152    if (settings.alwaysThinkingEnabled === false) {
153      return false
154    }
155  
156    // IMPORTANT: Do not change default thinking enabled value without notifying
157    // the model launch DRI and research. This can greatly affect model quality and
158    // bashing.
159  
160    // Enable thinking by default unless explicitly disabled.
161    return true
162  }