-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy patheffort.ts
More file actions
326 lines (297 loc) · 11.1 KB
/
Copy patheffort.ts
File metadata and controls
326 lines (297 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { isUltrathinkEnabled } from './thinking.js'
import { getInitialSettings } from './settings/settings.js'
import { isProSubscriber, isMaxSubscriber, isTeamSubscriber } from './auth.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
import { get3PModelCapabilityOverride } from './model/modelSupportOverrides.js'
import { isEnvTruthy } from './envUtils.js'
import type { EffortLevel } from 'src/entrypoints/sdk/runtimeTypes.js'
export type { EffortLevel }
export const EFFORT_LEVELS = [
'low',
'medium',
'high',
'max',
] as const satisfies readonly EffortLevel[]
export type EffortValue = EffortLevel | number
// @[MODEL LAUNCH]: Add the new model to the allowlist if it supports the effort parameter.
export function modelSupportsEffort(model: string): boolean {
const m = model.toLowerCase()
if (isEnvTruthy(process.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT)) {
return true
}
const supported3P = get3PModelCapabilityOverride(model, 'effort')
if (supported3P !== undefined) {
return supported3P
}
// Supported by a subset of Claude 4 models
if (m.includes('opus-4-6') || m.includes('sonnet-4-6')) {
return true
}
// Exclude any other known legacy models (haiku, older opus/sonnet variants)
if (m.includes('haiku') || m.includes('sonnet') || m.includes('opus')) {
return false
}
// IMPORTANT: Do not change the default effort support without notifying
// the model launch DRI and research. This is a sensitive setting that can
// greatly affect model quality and bashing.
// NOTE: Removed firstParty check to allow custom proxies to use effort
return true
}
// @[MODEL LAUNCH]: Add the new model to the allowlist if it supports 'max' effort.
// Per API docs, 'max' is Opus 4.6 only for public models — other models return an error.
export function modelSupportsMaxEffort(model: string): boolean {
const supported3P = get3PModelCapabilityOverride(model, 'max_effort')
if (supported3P !== undefined) {
return supported3P
}
if (model.toLowerCase().includes('opus-4-6')) {
return true
}
if (process.env.USER_TYPE === 'ant' && resolveAntModel(model)) {
return true
}
return false
}
export function isEffortLevel(value: string): value is EffortLevel {
return (EFFORT_LEVELS as readonly string[]).includes(value)
}
export function parseEffortValue(value: unknown): EffortValue | undefined {
if (value === undefined || value === null || value === '') {
return undefined
}
if (typeof value === 'number' && isValidNumericEffort(value)) {
return value
}
const str = String(value).toLowerCase()
if (isEffortLevel(str)) {
return str
}
const numericValue = parseInt(str, 10)
if (!isNaN(numericValue) && isValidNumericEffort(numericValue)) {
return numericValue
}
return undefined
}
/**
* Numeric values are model-default only and not persisted.
* 'max' is session-scoped for external users (ants can persist it).
* Write sites call this before saving to settings so the Zod schema
* (which only accepts string levels) never rejects a write.
*/
export function toPersistableEffort(
value: EffortValue | undefined,
): EffortLevel | undefined {
if (value === 'low' || value === 'medium' || value === 'high') {
return value
}
if (value === 'max' && process.env.USER_TYPE === 'ant') {
return value
}
return undefined
}
export function getInitialEffortSetting(): EffortLevel | undefined {
// toPersistableEffort filters 'max' for non-ants on read, so a manually
// edited settings.json doesn't leak session-scoped max into a fresh session.
return toPersistableEffort(getInitialSettings().effortLevel)
}
/**
* Decide what effort level (if any) to persist when the user selects a model
* in ModelPicker. Keeps an explicit prior /effort choice sticky even when it
* matches the picked model's default, while letting purely-default and
* session-ephemeral effort (CLI --effort, EffortCallout default) fall through
* to undefined so it follows future model-default changes.
*
* priorPersisted must come from userSettings on disk
* (getSettingsForSource('userSettings')?.effortLevel), NOT merged settings
* (project/policy layers would leak into the user's global settings.json)
* and NOT AppState.effortValue (includes session-scoped sources that
* deliberately do not write to settings.json).
*/
export function resolvePickerEffortPersistence(
picked: EffortLevel | undefined,
modelDefault: EffortLevel,
priorPersisted: EffortLevel | undefined,
toggledInPicker: boolean,
): EffortLevel | undefined {
const hadExplicit = priorPersisted !== undefined || toggledInPicker
return hadExplicit || picked !== modelDefault ? picked : undefined
}
export function getEffortEnvOverride(): EffortValue | null | undefined {
const envOverride = process.env.CLAUDE_CODE_EFFORT_LEVEL
return envOverride?.toLowerCase() === 'unset' ||
envOverride?.toLowerCase() === 'auto'
? null
: parseEffortValue(envOverride)
}
/**
* Resolve the effort value that will actually be sent to the API for a given
* model, following the full precedence chain:
* env CLAUDE_CODE_EFFORT_LEVEL → appState.effortValue → model default
*
* Returns undefined when no effort parameter should be sent (env set to
* 'unset', or no default exists for the model).
*/
export function resolveAppliedEffort(
model: string,
appStateEffortValue: EffortValue | undefined,
): EffortValue | undefined {
const envOverride = getEffortEnvOverride()
if (envOverride === null) {
return undefined
}
const resolved =
envOverride ?? appStateEffortValue ?? getDefaultEffortForModel(model)
// API rejects 'max' on non-Opus-4.6 models — downgrade to 'high'.
if (resolved === 'max' && !modelSupportsMaxEffort(model)) {
return 'high'
}
return resolved
}
/**
* Resolve the effort level to show the user. Wraps resolveAppliedEffort
* with the 'high' fallback (what the API uses when no effort param is sent).
* Single source of truth for the status bar and /effort output (CC-1088).
*/
export function getDisplayedEffortLevel(
model: string,
appStateEffort: EffortValue | undefined,
): EffortLevel {
const resolved = resolveAppliedEffort(model, appStateEffort) ?? 'high'
return convertEffortValueToLevel(resolved)
}
/**
* Build the ` with {level} effort` suffix shown in Logo/Spinner.
* Returns empty string if the user hasn't explicitly set an effort value.
* Delegates to resolveAppliedEffort() so the displayed level matches what
* the API actually receives (including max→high clamp for non-Opus models).
*/
export function getEffortSuffix(
model: string,
effortValue: EffortValue | undefined,
): string {
if (effortValue === undefined) return ''
const resolved = resolveAppliedEffort(model, effortValue)
if (resolved === undefined) return ''
return ` with ${convertEffortValueToLevel(resolved)} effort`
}
export function isValidNumericEffort(value: number): boolean {
return Number.isInteger(value)
}
export function convertEffortValueToLevel(value: EffortValue): EffortLevel {
if (typeof value === 'string') {
// Runtime guard: value may come from remote config (GrowthBook) where
// TypeScript types can't help us. Coerce unknown strings to 'high'
// rather than passing them through unchecked.
return isEffortLevel(value) ? value : 'high'
}
if (process.env.USER_TYPE === 'ant' && typeof value === 'number') {
if (value <= 50) return 'low'
if (value <= 85) return 'medium'
if (value <= 100) return 'high'
return 'max'
}
return 'high'
}
/**
* Get user-facing description for effort levels
*
* @param level The effort level to describe
* @returns Human-readable description
*/
export function getEffortLevelDescription(level: EffortLevel): string {
switch (level) {
case 'low':
return 'Quick, straightforward implementation with minimal overhead'
case 'medium':
return 'Balanced approach with standard implementation and testing'
case 'high':
return 'Comprehensive implementation with extensive testing and documentation'
case 'max':
return 'Maximum capability with deepest reasoning (Opus 4.6 only)'
}
}
/**
* Get user-facing description for effort values (both string and numeric)
*
* @param value The effort value to describe
* @returns Human-readable description
*/
export function getEffortValueDescription(value: EffortValue): string {
if (process.env.USER_TYPE === 'ant' && typeof value === 'number') {
return `[ANT-ONLY] Numeric effort value of ${value}`
}
if (typeof value === 'string') {
return getEffortLevelDescription(value)
}
return 'Balanced approach with standard implementation and testing'
}
export type OpusDefaultEffortConfig = {
enabled: boolean
dialogTitle: string
dialogDescription: string
}
const OPUS_DEFAULT_EFFORT_CONFIG_DEFAULT: OpusDefaultEffortConfig = {
enabled: true,
dialogTitle: 'We recommend medium effort for Opus',
dialogDescription:
'Effort determines how long Claude thinks for when completing your task. We recommend medium effort for most tasks to balance speed and intelligence and maximize rate limits. Use ultrathink to trigger high effort when needed.',
}
export function getOpusDefaultEffortConfig(): OpusDefaultEffortConfig {
const config = getFeatureValue_CACHED_MAY_BE_STALE(
'tengu_grey_step2',
OPUS_DEFAULT_EFFORT_CONFIG_DEFAULT,
)
return {
...OPUS_DEFAULT_EFFORT_CONFIG_DEFAULT,
...config,
}
}
// @[MODEL LAUNCH]: Update the default effort levels for new models
export function getDefaultEffortForModel(
model: string,
): EffortValue | undefined {
if (process.env.USER_TYPE === 'ant') {
const config = getAntModelOverrideConfig()
const isDefaultModel =
config?.defaultModel !== undefined &&
model.toLowerCase() === config.defaultModel.toLowerCase()
if (isDefaultModel && config?.defaultModelEffortLevel) {
return config.defaultModelEffortLevel
}
const antModel = resolveAntModel(model)
if (antModel) {
if (antModel.defaultEffortLevel) {
return antModel.defaultEffortLevel
}
if (antModel.defaultEffortValue !== undefined) {
return antModel.defaultEffortValue
}
}
// Always default ants to undefined/high
return undefined
}
// IMPORTANT: Do not change the default effort level without notifying
// the model launch DRI and research. Default effort is a sensitive setting
// that can greatly affect model quality and bashing.
// Default effort on Opus 4.6 to medium for Pro.
// Max/Team also get medium when the tengu_grey_step2 config is enabled.
if (model.toLowerCase().includes('opus-4-6')) {
if (isProSubscriber()) {
return 'medium'
}
if (
getOpusDefaultEffortConfig().enabled &&
(isMaxSubscriber() || isTeamSubscriber())
) {
return 'medium'
}
}
// When ultrathink feature is on, default effort to medium (ultrathink bumps to high)
if (isUltrathinkEnabled() && modelSupportsEffort(model)) {
return 'medium'
}
// Fallback to undefined, which means we don't set an effort level. This
// should resolve to high effort level in the API.
return undefined
}