mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 22:05:50 +00:00
feat: 添加 model/provider 层改进
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,21 +1,21 @@
|
||||
import capitalize from 'lodash-es/capitalize.js'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { has1mContext } from '../utils/context.js'
|
||||
import { useExitOnCtrlCDWithKeybindings } from 'src/hooks/useExitOnCtrlCDWithKeybindings.js'
|
||||
import capitalize from 'lodash-es/capitalize.js';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { has1mContext } from '../utils/context.js';
|
||||
import { useExitOnCtrlCDWithKeybindings } from 'src/hooks/useExitOnCtrlCDWithKeybindings.js';
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from 'src/services/analytics/index.js'
|
||||
} from 'src/services/analytics/index.js';
|
||||
import {
|
||||
FAST_MODE_MODEL_DISPLAY,
|
||||
isFastModeAvailable,
|
||||
isFastModeCooldown,
|
||||
isFastModeEnabled,
|
||||
} from 'src/utils/fastMode.js'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import { useKeybindings } from '../keybindings/useKeybinding.js'
|
||||
import { useAppState, useSetAppState } from '../state/AppState.js'
|
||||
} from 'src/utils/fastMode.js';
|
||||
import { Box, Text } from '@anthropic/ink';
|
||||
import { useKeybindings } from '../keybindings/useKeybinding.js';
|
||||
import { useAppState, useSetAppState } from '../state/AppState.js';
|
||||
import {
|
||||
convertEffortValueToLevel,
|
||||
type EffortLevel,
|
||||
@@ -24,42 +24,39 @@ import {
|
||||
modelSupportsMaxEffort,
|
||||
resolvePickerEffortPersistence,
|
||||
toPersistableEffort,
|
||||
} from '../utils/effort.js'
|
||||
} from '../utils/effort.js';
|
||||
import {
|
||||
getDefaultMainLoopModel,
|
||||
type ModelSetting,
|
||||
modelDisplayString,
|
||||
parseUserSpecifiedModel,
|
||||
} from '../utils/model/model.js'
|
||||
import { getModelOptions } from '../utils/model/modelOptions.js'
|
||||
import {
|
||||
getSettingsForSource,
|
||||
updateSettingsForSource,
|
||||
} from '../utils/settings/settings.js'
|
||||
import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js'
|
||||
import { Select } from './CustomSelect/index.js'
|
||||
import { Byline, KeyboardShortcutHint, Pane } from '@anthropic/ink'
|
||||
import { effortLevelToSymbol } from './EffortIndicator.js'
|
||||
} from '../utils/model/model.js';
|
||||
import { getModelOptions } from '../utils/model/modelOptions.js';
|
||||
import { getSettingsForSource, updateSettingsForSource } from '../utils/settings/settings.js';
|
||||
import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js';
|
||||
import { Select } from './CustomSelect/index.js';
|
||||
import { Byline, KeyboardShortcutHint, Pane } from '@anthropic/ink';
|
||||
import { effortLevelToSymbol } from './EffortIndicator.js';
|
||||
|
||||
export type Props = {
|
||||
initial: string | null
|
||||
sessionModel?: ModelSetting
|
||||
onSelect: (model: string | null, effort: EffortLevel | undefined) => void
|
||||
onCancel?: () => void
|
||||
isStandaloneCommand?: boolean
|
||||
showFastModeNotice?: boolean
|
||||
initial: string | null;
|
||||
sessionModel?: ModelSetting;
|
||||
onSelect: (model: string | null, effort: EffortLevel | undefined) => void;
|
||||
onCancel?: () => void;
|
||||
isStandaloneCommand?: boolean;
|
||||
showFastModeNotice?: boolean;
|
||||
/** Overrides the dim header line below "Select model". */
|
||||
headerText?: string
|
||||
headerText?: string;
|
||||
/**
|
||||
* When true, skip writing effortLevel to userSettings on selection.
|
||||
* Used by the assistant installer wizard where the model choice is
|
||||
* project-scoped (written to the assistant's .claude/settings.json via
|
||||
* install.ts) and should not leak to the user's global ~/.claude/settings.
|
||||
*/
|
||||
skipSettingsWrite?: boolean
|
||||
}
|
||||
skipSettingsWrite?: boolean;
|
||||
};
|
||||
|
||||
const NO_PREFERENCE = '__NO_PREFERENCE__'
|
||||
const NO_PREFERENCE = '__NO_PREFERENCE__';
|
||||
|
||||
export function ModelPicker({
|
||||
initial,
|
||||
@@ -71,49 +68,44 @@ export function ModelPicker({
|
||||
headerText,
|
||||
skipSettingsWrite,
|
||||
}: Props): React.ReactNode {
|
||||
const setAppState = useSetAppState()
|
||||
const exitState = useExitOnCtrlCDWithKeybindings()
|
||||
const maxVisible = 10
|
||||
const setAppState = useSetAppState();
|
||||
const exitState = useExitOnCtrlCDWithKeybindings();
|
||||
const maxVisible = 10;
|
||||
|
||||
const initialValue = initial === null ? NO_PREFERENCE : initial
|
||||
const [focusedValue, setFocusedValue] = useState<string | undefined>(
|
||||
initialValue,
|
||||
)
|
||||
const initialValue = initial === null ? NO_PREFERENCE : initial;
|
||||
const [focusedValue, setFocusedValue] = useState<string | undefined>(initialValue);
|
||||
|
||||
const isFastMode = useAppState(s =>
|
||||
isFastModeEnabled() ? s.fastMode : false,
|
||||
)
|
||||
const isFastMode = useAppState(s => (isFastModeEnabled() ? s.fastMode : false));
|
||||
|
||||
const [marked1MValues, setMarked1MValues] = useState<Set<string>>(
|
||||
() => new Set(has1mContext(initialValue) ? [initialValue.replace(/\[1m\]/i, '')] : [])
|
||||
)
|
||||
() => new Set(has1mContext(initialValue) ? [initialValue.replace(/\[1m\]/i, '')] : []),
|
||||
);
|
||||
|
||||
const handleToggle1M = useCallback(() => {
|
||||
if (!focusedValue || focusedValue === NO_PREFERENCE) return
|
||||
if (!focusedValue || focusedValue === NO_PREFERENCE) return;
|
||||
// Key on the base value so lookups in handleSelect / is1MMarked match the
|
||||
// initializer — predefined 1M options arrive with a `[1m]` suffix in
|
||||
// `focusedValue`, which would diverge from the base-value key set.
|
||||
const baseKey = focusedValue.replace(/\[1m\]/i, '');
|
||||
setMarked1MValues(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(focusedValue)) {
|
||||
next.delete(focusedValue)
|
||||
const next = new Set(prev);
|
||||
if (next.has(baseKey)) {
|
||||
next.delete(baseKey);
|
||||
} else {
|
||||
next.add(focusedValue)
|
||||
next.add(baseKey);
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [focusedValue])
|
||||
return next;
|
||||
});
|
||||
}, [focusedValue]);
|
||||
|
||||
const [hasToggledEffort, setHasToggledEffort] = useState(false)
|
||||
const effortValue = useAppState(s => s.effortValue)
|
||||
const [hasToggledEffort, setHasToggledEffort] = useState(false);
|
||||
const effortValue = useAppState(s => s.effortValue);
|
||||
const [effort, setEffort] = useState<EffortLevel | undefined>(
|
||||
effortValue !== undefined
|
||||
? convertEffortValueToLevel(effortValue)
|
||||
: undefined,
|
||||
)
|
||||
effortValue !== undefined ? convertEffortValueToLevel(effortValue) : undefined,
|
||||
);
|
||||
|
||||
// Memoize all derived values to prevent re-renders
|
||||
const modelOptions = useMemo(
|
||||
() => getModelOptions(isFastMode ?? false),
|
||||
[isFastMode],
|
||||
)
|
||||
const modelOptions = useMemo(() => getModelOptions(isFastMode ?? false), [isFastMode]);
|
||||
|
||||
// Ensure the initial value is in the options list
|
||||
// This handles edge cases where the user's current model (e.g., 'haiku' for 3P users)
|
||||
@@ -127,10 +119,10 @@ export function ModelPicker({
|
||||
label: modelDisplayString(initial),
|
||||
description: 'Current model',
|
||||
},
|
||||
]
|
||||
];
|
||||
}
|
||||
return modelOptions
|
||||
}, [modelOptions, initial])
|
||||
return modelOptions;
|
||||
}, [modelOptions, initial]);
|
||||
|
||||
const selectOptions = useMemo(
|
||||
() =>
|
||||
@@ -139,59 +131,46 @@ export function ModelPicker({
|
||||
value: opt.value === null ? NO_PREFERENCE : opt.value,
|
||||
})),
|
||||
[optionsWithInitial],
|
||||
)
|
||||
);
|
||||
const initialFocusValue = useMemo(
|
||||
() =>
|
||||
selectOptions.some(_ => _.value === initialValue)
|
||||
? initialValue
|
||||
: (selectOptions[0]?.value ?? undefined),
|
||||
() => (selectOptions.some(_ => _.value === initialValue) ? initialValue : (selectOptions[0]?.value ?? undefined)),
|
||||
[selectOptions, initialValue],
|
||||
)
|
||||
const visibleCount = Math.min(maxVisible, selectOptions.length)
|
||||
const hiddenCount = Math.max(0, selectOptions.length - visibleCount)
|
||||
);
|
||||
const visibleCount = Math.min(maxVisible, selectOptions.length);
|
||||
const hiddenCount = Math.max(0, selectOptions.length - visibleCount);
|
||||
|
||||
const focusedModelName = selectOptions.find(
|
||||
opt => opt.value === focusedValue,
|
||||
)?.label
|
||||
const focusedModel = resolveOptionModel(focusedValue)
|
||||
const is1MMarked = focusedValue !== undefined && focusedValue !== NO_PREFERENCE && marked1MValues.has(focusedValue)
|
||||
const focusedSupportsEffort = focusedModel
|
||||
? modelSupportsEffort(focusedModel)
|
||||
: false
|
||||
const focusedSupportsMax = focusedModel
|
||||
? modelSupportsMaxEffort(focusedModel)
|
||||
: false
|
||||
const focusedDefaultEffort = getDefaultEffortLevelForOption(focusedValue)
|
||||
const focusedModelName = selectOptions.find(opt => opt.value === focusedValue)?.label;
|
||||
const focusedModel = resolveOptionModel(focusedValue);
|
||||
const is1MMarked =
|
||||
focusedValue !== undefined &&
|
||||
focusedValue !== NO_PREFERENCE &&
|
||||
marked1MValues.has(focusedValue.replace(/\[1m\]/i, ''));
|
||||
const focusedSupportsEffort = focusedModel ? modelSupportsEffort(focusedModel) : false;
|
||||
const focusedSupportsMax = focusedModel ? modelSupportsMaxEffort(focusedModel) : false;
|
||||
const focusedDefaultEffort = getDefaultEffortLevelForOption(focusedValue);
|
||||
// Clamp display when 'max' is selected but the focused model doesn't support it.
|
||||
// resolveAppliedEffort() does the same downgrade at API-send time.
|
||||
const displayEffort =
|
||||
effort === 'max' && !focusedSupportsMax ? 'high' : effort
|
||||
const displayEffort = effort === 'max' && !focusedSupportsMax ? 'high' : effort;
|
||||
|
||||
const handleFocus = useCallback(
|
||||
(value: string) => {
|
||||
setFocusedValue(value)
|
||||
setFocusedValue(value);
|
||||
if (!hasToggledEffort && effortValue === undefined) {
|
||||
setEffort(getDefaultEffortLevelForOption(value))
|
||||
setEffort(getDefaultEffortLevelForOption(value));
|
||||
}
|
||||
},
|
||||
[hasToggledEffort, effortValue],
|
||||
)
|
||||
);
|
||||
|
||||
// Effort level cycling keybindings
|
||||
const handleCycleEffort = useCallback(
|
||||
(direction: 'left' | 'right') => {
|
||||
if (!focusedSupportsEffort) return
|
||||
setEffort(prev =>
|
||||
cycleEffortLevel(
|
||||
prev ?? focusedDefaultEffort,
|
||||
direction,
|
||||
focusedSupportsMax,
|
||||
),
|
||||
)
|
||||
setHasToggledEffort(true)
|
||||
if (!focusedSupportsEffort) return;
|
||||
setEffort(prev => cycleEffortLevel(prev ?? focusedDefaultEffort, direction, focusedSupportsMax));
|
||||
setHasToggledEffort(true);
|
||||
},
|
||||
[focusedSupportsEffort, focusedSupportsMax, focusedDefaultEffort],
|
||||
)
|
||||
);
|
||||
|
||||
useKeybindings(
|
||||
{
|
||||
@@ -200,13 +179,12 @@ export function ModelPicker({
|
||||
'modelPicker:toggle1M': () => handleToggle1M(),
|
||||
},
|
||||
{ context: 'ModelPicker' },
|
||||
)
|
||||
);
|
||||
|
||||
function handleSelect(value: string): void {
|
||||
logEvent('tengu_model_command_menu_effort', {
|
||||
effort:
|
||||
effort as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
effort: effort as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
});
|
||||
if (!skipSettingsWrite) {
|
||||
// Prior comes from userSettings on disk — NOT merged settings (which
|
||||
// includes project/policy layers that must not leak into the user's
|
||||
@@ -218,28 +196,28 @@ export function ModelPicker({
|
||||
getDefaultEffortLevelForOption(value),
|
||||
getSettingsForSource('userSettings')?.effortLevel,
|
||||
hasToggledEffort,
|
||||
)
|
||||
const persistable = toPersistableEffort(effortLevel)
|
||||
);
|
||||
const persistable = toPersistableEffort(effortLevel);
|
||||
if (persistable !== undefined) {
|
||||
updateSettingsForSource('userSettings', { effortLevel: persistable })
|
||||
updateSettingsForSource('userSettings', { effortLevel: persistable });
|
||||
}
|
||||
setAppState(prev => ({ ...prev, effortValue: effortLevel }))
|
||||
setAppState(prev => ({ ...prev, effortValue: effortLevel }));
|
||||
}
|
||||
|
||||
const selectedModel = resolveOptionModel(value)
|
||||
const selectedEffort =
|
||||
hasToggledEffort && selectedModel && modelSupportsEffort(selectedModel)
|
||||
? effort
|
||||
: undefined
|
||||
const selectedModel = resolveOptionModel(value);
|
||||
const selectedEffort = hasToggledEffort && selectedModel && modelSupportsEffort(selectedModel) ? effort : undefined;
|
||||
if (value === NO_PREFERENCE) {
|
||||
onSelect(null, selectedEffort)
|
||||
return
|
||||
onSelect(null, selectedEffort);
|
||||
return;
|
||||
}
|
||||
// Apply or strip [1m] suffix based on user toggle
|
||||
const wants1M = marked1MValues.has(value)
|
||||
const baseValue = value.replace(/\[1m\]/i, '')
|
||||
const finalValue = wants1M ? `${baseValue}[1m]` : baseValue
|
||||
onSelect(finalValue, selectedEffort)
|
||||
// Apply or strip [1m] suffix based on user toggle. marked1MValues is keyed
|
||||
// on the base value (see initializer + handleToggle1M), so look up with the
|
||||
// base form — not `value`, which may carry a `[1m]` suffix from predefined
|
||||
// 1M options and would never match.
|
||||
const baseValue = value.replace(/\[1m\]/i, '');
|
||||
const wants1M = marked1MValues.has(baseValue);
|
||||
const finalValue = wants1M ? `${baseValue}[1m]` : baseValue;
|
||||
onSelect(finalValue, selectedEffort);
|
||||
}
|
||||
|
||||
const content = (
|
||||
@@ -255,8 +233,8 @@ export function ModelPicker({
|
||||
</Text>
|
||||
{sessionModel && (
|
||||
<Text dimColor>
|
||||
Currently using {modelDisplayString(sessionModel)} for this
|
||||
session (set by plan mode). Selecting a model will undo this.
|
||||
Currently using {modelDisplayString(sessionModel)} for this session (set by plan mode). Selecting a model
|
||||
will undo this.
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
@@ -283,10 +261,8 @@ export function ModelPicker({
|
||||
<Box marginBottom={1} flexDirection="column">
|
||||
{focusedSupportsEffort ? (
|
||||
<Text dimColor>
|
||||
<EffortLevelIndicator effort={displayEffort} />{' '}
|
||||
{capitalize(displayEffort)} effort
|
||||
{displayEffort === focusedDefaultEffort ? ` (default)` : ``}{' '}
|
||||
<Text color="subtle">← → to adjust</Text>
|
||||
<EffortLevelIndicator effort={displayEffort} /> {capitalize(displayEffort)} effort
|
||||
{displayEffort === focusedDefaultEffort ? ` (default)` : ``} <Text color="subtle">← → to adjust</Text>
|
||||
</Text>
|
||||
) : (
|
||||
<Text color="subtle">
|
||||
@@ -311,16 +287,14 @@ export function ModelPicker({
|
||||
showFastModeNotice ? (
|
||||
<Box marginBottom={1}>
|
||||
<Text dimColor>
|
||||
Fast mode is <Text bold>ON</Text> and available with{' '}
|
||||
{FAST_MODE_MODEL_DISPLAY} only (/fast). Switching to other
|
||||
models turn off fast mode.
|
||||
Fast mode is <Text bold>ON</Text> and available with {FAST_MODE_MODEL_DISPLAY} only (/fast). Switching
|
||||
to other models turn off fast mode.
|
||||
</Text>
|
||||
</Box>
|
||||
) : isFastModeAvailable() && !isFastModeCooldown() ? (
|
||||
<Box marginBottom={1}>
|
||||
<Text dimColor>
|
||||
Use <Text bold>/fast</Text> to turn on Fast mode (
|
||||
{FAST_MODE_MODEL_DISPLAY} only).
|
||||
Use <Text bold>/fast</Text> to turn on Fast mode ({FAST_MODE_MODEL_DISPLAY} only).
|
||||
</Text>
|
||||
</Box>
|
||||
) : null
|
||||
@@ -334,68 +308,45 @@ export function ModelPicker({
|
||||
) : (
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="Enter" action="confirm" />
|
||||
<ConfigurableShortcutHint
|
||||
action="select:cancel"
|
||||
context="Select"
|
||||
fallback="Esc"
|
||||
description="exit"
|
||||
/>
|
||||
<ConfigurableShortcutHint action="select:cancel" context="Select" fallback="Esc" description="exit" />
|
||||
</Byline>
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
|
||||
if (!isStandaloneCommand) {
|
||||
return content
|
||||
return content;
|
||||
}
|
||||
|
||||
return <Pane color="permission">{content}</Pane>
|
||||
return <Pane color="permission">{content}</Pane>;
|
||||
}
|
||||
|
||||
function resolveOptionModel(value?: string): string | undefined {
|
||||
if (!value) return undefined
|
||||
return value === NO_PREFERENCE
|
||||
? getDefaultMainLoopModel()
|
||||
: parseUserSpecifiedModel(value)
|
||||
if (!value) return undefined;
|
||||
return value === NO_PREFERENCE ? getDefaultMainLoopModel() : parseUserSpecifiedModel(value);
|
||||
}
|
||||
|
||||
function EffortLevelIndicator({
|
||||
effort,
|
||||
}: {
|
||||
effort?: EffortLevel
|
||||
}): React.ReactNode {
|
||||
return (
|
||||
<Text color={effort ? 'claude' : 'subtle'}>
|
||||
{effortLevelToSymbol(effort ?? 'low')}
|
||||
</Text>
|
||||
)
|
||||
function EffortLevelIndicator({ effort }: { effort?: EffortLevel }): React.ReactNode {
|
||||
return <Text color={effort ? 'claude' : 'subtle'}>{effortLevelToSymbol(effort ?? 'low')}</Text>;
|
||||
}
|
||||
|
||||
function cycleEffortLevel(
|
||||
current: EffortLevel,
|
||||
direction: 'left' | 'right',
|
||||
includeMax: boolean,
|
||||
): EffortLevel {
|
||||
const levels: EffortLevel[] = includeMax
|
||||
? ['low', 'medium', 'high', 'max']
|
||||
: ['low', 'medium', 'high']
|
||||
function cycleEffortLevel(current: EffortLevel, direction: 'left' | 'right', includeMax: boolean): EffortLevel {
|
||||
const levels: EffortLevel[] = includeMax ? ['low', 'medium', 'high', 'max'] : ['low', 'medium', 'high'];
|
||||
// If the current level isn't in the cycle (e.g. 'max' after switching to a
|
||||
// non-Opus model), clamp to 'high'.
|
||||
const idx = levels.indexOf(current)
|
||||
const currentIndex = idx !== -1 ? idx : levels.indexOf('high')
|
||||
const idx = levels.indexOf(current);
|
||||
const currentIndex = idx !== -1 ? idx : levels.indexOf('high');
|
||||
if (direction === 'right') {
|
||||
return levels[(currentIndex + 1) % levels.length]!
|
||||
return levels[(currentIndex + 1) % levels.length]!;
|
||||
} else {
|
||||
return levels[(currentIndex - 1 + levels.length) % levels.length]!
|
||||
return levels[(currentIndex - 1 + levels.length) % levels.length]!;
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultEffortLevelForOption(value?: string): EffortLevel {
|
||||
const resolved = resolveOptionModel(value) ?? getDefaultMainLoopModel()
|
||||
const defaultValue = getDefaultEffortForModel(resolved)
|
||||
return defaultValue !== undefined
|
||||
? convertEffortValueToLevel(defaultValue)
|
||||
: 'high'
|
||||
const resolved = resolveOptionModel(value) ?? getDefaultMainLoopModel();
|
||||
const defaultValue = getDefaultEffortForModel(resolved);
|
||||
return defaultValue !== undefined ? convertEffortValueToLevel(defaultValue) : 'high';
|
||||
}
|
||||
|
||||
148
src/utils/model/__tests__/getDefaultOpusModel.test.ts
Normal file
148
src/utils/model/__tests__/getDefaultOpusModel.test.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import { resetModelStringsForTestingOnly } from 'src/bootstrap/state.js'
|
||||
import {
|
||||
resetSettingsCache,
|
||||
setSessionSettingsCache,
|
||||
} from 'src/utils/settings/settingsCache.js'
|
||||
import { ALL_MODEL_CONFIGS } from '../configs.js'
|
||||
import { getDefaultOpusModel } from '../model.js'
|
||||
import { getOpus46Option } from '../modelOptions.js'
|
||||
import { getModelStrings } from '../modelStrings.js'
|
||||
|
||||
/**
|
||||
* Verifies getDefaultOpusModel() returns Opus 4.7 across all providers
|
||||
* (firstParty + Bedrock/Vertex/Foundry). This is the Gap #2 assertion:
|
||||
* as of 2026-04-17 all 3P vendors have published Opus 4.7, so the fork
|
||||
* must not fall back to Opus 4.6 on 3P.
|
||||
*
|
||||
* Authoritative sources for 3P availability:
|
||||
* - AWS Bedrock: docs.aws.amazon.com/bedrock/.../model-card-anthropic-claude-opus-4-7.html
|
||||
* - Google Vertex AI: docs.cloud.google.com/vertex-ai/.../claude/opus-4-7
|
||||
* - Microsoft Foundry: ai.azure.com/catalog/models/claude-opus-4-7
|
||||
*/
|
||||
|
||||
const envKeys = [
|
||||
'CLAUDE_CODE_USE_GEMINI',
|
||||
'CLAUDE_CODE_USE_BEDROCK',
|
||||
'CLAUDE_CODE_USE_VERTEX',
|
||||
'CLAUDE_CODE_USE_FOUNDRY',
|
||||
'CLAUDE_CODE_USE_OPENAI',
|
||||
'CLAUDE_CODE_USE_GROK',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||
'OPENAI_DEFAULT_OPUS_MODEL',
|
||||
'GEMINI_DEFAULT_OPUS_MODEL',
|
||||
] as const
|
||||
|
||||
const savedEnv: Record<string, string | undefined> = {}
|
||||
|
||||
function resetProviderState(): void {
|
||||
resetSettingsCache()
|
||||
setSessionSettingsCache({ settings: {}, errors: [] })
|
||||
resetModelStringsForTestingOnly()
|
||||
}
|
||||
|
||||
describe('getDefaultOpusModel', () => {
|
||||
beforeEach(() => {
|
||||
for (const key of envKeys) {
|
||||
savedEnv[key] = process.env[key]
|
||||
delete process.env[key]
|
||||
}
|
||||
resetProviderState()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
for (const key of envKeys) {
|
||||
if (savedEnv[key] !== undefined) {
|
||||
process.env[key] = savedEnv[key]
|
||||
} else {
|
||||
delete process.env[key]
|
||||
}
|
||||
}
|
||||
resetProviderState()
|
||||
})
|
||||
|
||||
test('returns Opus 4.7 for firstParty', () => {
|
||||
expect(getDefaultOpusModel()).toBe(ALL_MODEL_CONFIGS.opus47.firstParty)
|
||||
})
|
||||
|
||||
test('returns Opus 4.7 for bedrock (3P no longer lags)', () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
||||
expect(getDefaultOpusModel()).toBe(ALL_MODEL_CONFIGS.opus47.bedrock)
|
||||
})
|
||||
|
||||
test('returns Opus 4.7 for vertex (3P no longer lags)', () => {
|
||||
process.env.CLAUDE_CODE_USE_VERTEX = '1'
|
||||
expect(getDefaultOpusModel()).toBe(ALL_MODEL_CONFIGS.opus47.vertex)
|
||||
})
|
||||
|
||||
test('returns Opus 4.7 for foundry (3P no longer lags)', () => {
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY = '1'
|
||||
expect(getDefaultOpusModel()).toBe(ALL_MODEL_CONFIGS.opus47.foundry)
|
||||
})
|
||||
|
||||
test('honors ANTHROPIC_DEFAULT_OPUS_MODEL env override (any provider)', () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
||||
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL = 'claude-opus-4-1-custom'
|
||||
expect(getDefaultOpusModel()).toBe('claude-opus-4-1-custom')
|
||||
})
|
||||
|
||||
test('honors OPENAI_DEFAULT_OPUS_MODEL for openai provider', () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_DEFAULT_OPUS_MODEL = 'gpt-5-turbo'
|
||||
expect(getDefaultOpusModel()).toBe('gpt-5-turbo')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Gap #3 addition — "Opus 4.6" must appear as an explicit opt-in option in
|
||||
* the /model picker across all non-ANT user tiers. The option's value MUST
|
||||
* be the canonical 4.6 model string, NOT the 'opus' alias (which would
|
||||
* resolve via getDefaultOpusModel back to 4.7 on firstParty, silently
|
||||
* defeating the user's explicit choice).
|
||||
*/
|
||||
describe('getOpus46Option', () => {
|
||||
beforeEach(() => {
|
||||
for (const key of envKeys) {
|
||||
savedEnv[key] = process.env[key]
|
||||
delete process.env[key]
|
||||
}
|
||||
resetProviderState()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
for (const key of envKeys) {
|
||||
if (savedEnv[key] !== undefined) {
|
||||
process.env[key] = savedEnv[key]
|
||||
} else {
|
||||
delete process.env[key]
|
||||
}
|
||||
}
|
||||
resetProviderState()
|
||||
})
|
||||
|
||||
test('firstParty: value is canonical opus46 string, NOT opus alias', () => {
|
||||
const opt = getOpus46Option(false)
|
||||
expect(opt.value).toBe(getModelStrings().opus46)
|
||||
expect(opt.value).not.toBe('opus')
|
||||
expect(opt.label).toBe('Opus 4.6')
|
||||
})
|
||||
|
||||
test('firstParty: description says "Previous generation", not "Legacy"', () => {
|
||||
const opt = getOpus46Option(false)
|
||||
expect(opt.description).toContain('Previous generation')
|
||||
expect(opt.description).not.toContain('Legacy')
|
||||
})
|
||||
|
||||
test('bedrock: value is canonical opus46 string (unchanged behavior)', () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
||||
const opt = getOpus46Option(false)
|
||||
expect(opt.value).toBe(getModelStrings().opus46)
|
||||
expect(opt.value).toBe(ALL_MODEL_CONFIGS.opus46.bedrock)
|
||||
})
|
||||
|
||||
test('option has descriptionForModel that mentions Opus 4.6', () => {
|
||||
const opt = getOpus46Option(false)
|
||||
expect(opt.descriptionForModel).toBeDefined()
|
||||
expect(opt.descriptionForModel).toContain('Opus 4.6')
|
||||
})
|
||||
})
|
||||
@@ -106,6 +106,16 @@ export const CLAUDE_OPUS_4_6_CONFIG = {
|
||||
grok: 'claude-opus-4-6',
|
||||
} as const satisfies ModelConfig
|
||||
|
||||
export const CLAUDE_OPUS_4_7_CONFIG = {
|
||||
firstParty: 'claude-opus-4-7',
|
||||
bedrock: 'us.anthropic.claude-opus-4-7-v1',
|
||||
vertex: 'claude-opus-4-7',
|
||||
foundry: 'claude-opus-4-7',
|
||||
openai: 'claude-opus-4-7',
|
||||
gemini: 'claude-opus-4-7',
|
||||
grok: 'claude-opus-4-7',
|
||||
} as const satisfies ModelConfig
|
||||
|
||||
export const CLAUDE_SONNET_4_6_CONFIG = {
|
||||
firstParty: 'claude-sonnet-4-6',
|
||||
bedrock: 'us.anthropic.claude-sonnet-4-6',
|
||||
@@ -129,6 +139,7 @@ export const ALL_MODEL_CONFIGS = {
|
||||
opus41: CLAUDE_OPUS_4_1_CONFIG,
|
||||
opus45: CLAUDE_OPUS_4_5_CONFIG,
|
||||
opus46: CLAUDE_OPUS_4_6_CONFIG,
|
||||
opus47: CLAUDE_OPUS_4_7_CONFIG,
|
||||
} as const satisfies Record<string, ModelConfig>
|
||||
|
||||
export type ModelKey = keyof typeof ALL_MODEL_CONFIGS
|
||||
|
||||
@@ -28,18 +28,6 @@ import { getAPIProvider } from './providers.js'
|
||||
import { LIGHTNING_BOLT } from '../../constants/figures.js'
|
||||
import { isModelAllowed } from './modelAllowlist.js'
|
||||
import { type ModelAlias, isModelAlias } from './aliases.js'
|
||||
|
||||
/**
|
||||
* Returns true if the value is a model alias or a model alias with a suffix
|
||||
* like [1m] (e.g. "opus", "opus[1m]", "sonnet", "haiku[1m]").
|
||||
* Used to guard against infinite recursion when getDefault*Model() falls back
|
||||
* to the user-specified setting — an alias like "opus[1m]" would cause
|
||||
* parseUserSpecifiedModel → getDefaultOpusModel → parseUserSpecifiedModel loop.
|
||||
*/
|
||||
function isAliasOrAliasWithSuffix(value: string): boolean {
|
||||
const base = value.replace(/\[1m\]$/i, '').trim()
|
||||
return isModelAlias(base)
|
||||
}
|
||||
import { capitalize } from '../stringUtils.js'
|
||||
|
||||
export type ModelShortName = string
|
||||
@@ -64,7 +52,8 @@ export function isNonCustomOpusModel(model: ModelName): boolean {
|
||||
model === getModelStrings().opus40 ||
|
||||
model === getModelStrings().opus41 ||
|
||||
model === getModelStrings().opus45 ||
|
||||
model === getModelStrings().opus46
|
||||
model === getModelStrings().opus46 ||
|
||||
model === getModelStrings().opus47
|
||||
)
|
||||
}
|
||||
|
||||
@@ -138,21 +127,14 @@ export function getDefaultOpusModel(): ModelName {
|
||||
if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL) {
|
||||
return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
|
||||
}
|
||||
// Fall back to user's configured model — custom providers may not
|
||||
// recognize hardcoded Anthropic model IDs.
|
||||
// Skip if the user setting is a model alias (e.g. "opus", "opus[1m]") to
|
||||
// avoid infinite recursion: parseUserSpecifiedModel(alias) → getDefaultOpusModel().
|
||||
const userSpecifiedOpus = getUserSpecifiedModelSetting()
|
||||
if (userSpecifiedOpus && !isAliasOrAliasWithSuffix(userSpecifiedOpus)) {
|
||||
return parseUserSpecifiedModel(userSpecifiedOpus)
|
||||
}
|
||||
// 3P providers (Bedrock, Vertex, Foundry) — kept as a separate branch
|
||||
// even when values match, since 3P availability lags firstParty and
|
||||
// these will diverge again at the next model launch.
|
||||
// 3P providers (Bedrock, Vertex, Foundry) all publish Opus 4.7 in sync
|
||||
// with firstParty as of 2026-04-17 (AWS Bedrock, Google Vertex AI, and
|
||||
// Microsoft Foundry announcements and model catalogs all confirm). The
|
||||
// branch is kept as a structural hook in case a future launch lags on 3P.
|
||||
if (provider !== 'firstParty') {
|
||||
return getModelStrings().opus46
|
||||
return getModelStrings().opus47
|
||||
}
|
||||
return getModelStrings().opus46
|
||||
return getModelStrings().opus47
|
||||
}
|
||||
|
||||
// @[MODEL LAUNCH]: Update the default Sonnet model (3P providers may lag so keep defaults unchanged).
|
||||
@@ -173,14 +155,6 @@ export function getDefaultSonnetModel(): ModelName {
|
||||
if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL) {
|
||||
return process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
|
||||
}
|
||||
// Fall back to user's configured model (ANTHROPIC_MODEL / settings) —
|
||||
// custom providers (proxies, national clouds) may not recognize the
|
||||
// hardcoded Anthropic model IDs.
|
||||
// Skip if the user setting is a model alias to avoid infinite recursion.
|
||||
const userSpecified = getUserSpecifiedModelSetting()
|
||||
if (userSpecified && !isAliasOrAliasWithSuffix(userSpecified)) {
|
||||
return parseUserSpecifiedModel(userSpecified)
|
||||
}
|
||||
// Default to Sonnet 4.5 for 3P since they may not have 4.6 yet
|
||||
if (provider !== 'firstParty') {
|
||||
return getModelStrings().sonnet45
|
||||
@@ -203,13 +177,6 @@ export function getDefaultHaikuModel(): ModelName {
|
||||
if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) {
|
||||
return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
|
||||
}
|
||||
// Fall back to user's configured model — custom providers may not
|
||||
// recognize hardcoded Anthropic model IDs.
|
||||
// Skip if the user setting is a model alias to avoid infinite recursion.
|
||||
const userSpecifiedHaiku = getUserSpecifiedModelSetting()
|
||||
if (userSpecifiedHaiku && !isAliasOrAliasWithSuffix(userSpecifiedHaiku)) {
|
||||
return parseUserSpecifiedModel(userSpecifiedHaiku)
|
||||
}
|
||||
|
||||
// Haiku 4.5 is available on all platforms (first-party, Foundry, Bedrock, Vertex)
|
||||
return getModelStrings().haiku45
|
||||
@@ -296,6 +263,9 @@ export function firstPartyNameToCanonical(name: ModelName): ModelShortName {
|
||||
name = name.toLowerCase()
|
||||
// Special cases for Claude 4+ models to differentiate versions
|
||||
// Order matters: check more specific versions first (4-5 before 4)
|
||||
if (name.includes('claude-opus-4-7')) {
|
||||
return 'claude-opus-4-7'
|
||||
}
|
||||
if (name.includes('claude-opus-4-6')) {
|
||||
return 'claude-opus-4-6'
|
||||
}
|
||||
@@ -366,9 +336,9 @@ export function getClaudeAiUserDefaultModelDescription(
|
||||
): string {
|
||||
if (isMaxSubscriber() || isTeamPremiumSubscriber()) {
|
||||
if (isOpus1mMergeEnabled()) {
|
||||
return `Opus 4.6 with 1M context · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`
|
||||
return `Opus 4.7 with 1M context · Most capable for complex work${fastMode ? getOpusPricingSuffix(true) : ''}`
|
||||
}
|
||||
return `Opus 4.6 · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`
|
||||
return `Opus 4.7 · Most capable for complex work${fastMode ? getOpusPricingSuffix(true) : ''}`
|
||||
}
|
||||
return 'Sonnet 4.6 · Best for everyday tasks'
|
||||
}
|
||||
@@ -377,12 +347,12 @@ export function renderDefaultModelSetting(
|
||||
setting: ModelName | ModelAlias,
|
||||
): string {
|
||||
if (setting === 'opusplan') {
|
||||
return 'Opus 4.6 in plan mode, else Sonnet 4.6'
|
||||
return 'Opus 4.7 in plan mode, else Sonnet 4.6'
|
||||
}
|
||||
return renderModelName(parseUserSpecifiedModel(setting))
|
||||
}
|
||||
|
||||
export function getOpus46PricingSuffix(fastMode: boolean): string {
|
||||
export function getOpusPricingSuffix(fastMode: boolean): string {
|
||||
if (getAPIProvider() !== 'firstParty') return ''
|
||||
const pricing = formatModelPricing(getOpus46CostTier(fastMode))
|
||||
const fastModeIndicator = fastMode ? ` (${LIGHTNING_BOLT})` : ''
|
||||
@@ -426,6 +396,10 @@ export function renderModelSetting(setting: ModelName | ModelAlias): string {
|
||||
*/
|
||||
export function getPublicModelDisplayName(model: ModelName): string | null {
|
||||
switch (model) {
|
||||
case getModelStrings().opus47:
|
||||
return 'Opus 4.7'
|
||||
case getModelStrings().opus47 + '[1m]':
|
||||
return 'Opus 4.7 (1M context)'
|
||||
case getModelStrings().opus46:
|
||||
return 'Opus 4.6'
|
||||
case getModelStrings().opus46 + '[1m]':
|
||||
@@ -549,9 +523,10 @@ export function parseUserSpecifiedModel(
|
||||
|
||||
// Opus 4/4.1 are no longer available on the first-party API (same as
|
||||
// Claude.ai) — silently remap to the current Opus default. The 'opus'
|
||||
// alias already resolves to 4.6, so the only users on these explicit
|
||||
// strings pinned them in settings/env/--model/SDK before 4.5 launched.
|
||||
// 3P providers may not yet have 4.6 capacity, so pass through unchanged.
|
||||
// alias resolves to the current default Opus (4.7), so the only users
|
||||
// on these explicit strings pinned them in settings/env/--model/SDK
|
||||
// before 4.5 launched. 3P providers may not yet have 4.6/4.7 capacity,
|
||||
// so pass through unchanged.
|
||||
if (
|
||||
getAPIProvider() === 'firstParty' &&
|
||||
isLegacyOpusFirstParty(modelString) &&
|
||||
@@ -654,6 +629,9 @@ export function getMarketingNameForModel(modelId: string): string | undefined {
|
||||
const has1m = modelId.toLowerCase().includes('[1m]')
|
||||
const canonical = getCanonicalName(modelId)
|
||||
|
||||
if (canonical.includes('claude-opus-4-7')) {
|
||||
return has1m ? 'Opus 4.7 (with 1M context)' : 'Opus 4.7'
|
||||
}
|
||||
if (canonical.includes('claude-opus-4-6')) {
|
||||
return has1m ? 'Opus 4.6 (with 1M context)' : 'Opus 4.6'
|
||||
}
|
||||
|
||||
@@ -44,7 +44,10 @@ function getCachePath(): string {
|
||||
}
|
||||
|
||||
function isModelCapabilitiesEligible(): boolean {
|
||||
if (process.env.USER_TYPE !== 'ant') return false
|
||||
// Upstream gates this to ant-only, but the /v1/models API is available
|
||||
// to all firstParty users (API key and OAuth). Enabling for everyone
|
||||
// lets model capabilities (max_input_tokens, max_tokens) be fetched
|
||||
// dynamically instead of relying on hardcoded values in context.ts.
|
||||
if (getAPIProvider() !== 'firstParty') return false
|
||||
if (!isFirstPartyAnthropicBaseUrl()) return false
|
||||
return true
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
getMarketingNameForModel,
|
||||
getUserSpecifiedModelSetting,
|
||||
isOpus1mMergeEnabled,
|
||||
getOpus46PricingSuffix,
|
||||
getOpusPricingSuffix,
|
||||
renderDefaultModelSetting,
|
||||
type ModelSetting,
|
||||
} from './model.js'
|
||||
@@ -82,8 +82,8 @@ function getCustomSonnetOption(): ModelOption | undefined {
|
||||
provider === 'openai'
|
||||
? process.env.OPENAI_DEFAULT_SONNET_MODEL
|
||||
: provider === 'gemini'
|
||||
? process.env.GEMINI_DEFAULT_SONNET_MODEL
|
||||
: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
|
||||
? process.env.GEMINI_DEFAULT_SONNET_MODEL
|
||||
: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL
|
||||
// When a 3P user has a custom sonnet model string, show it directly
|
||||
if (is3P && customSonnetModel) {
|
||||
const is1m = has1mContext(customSonnetModel)
|
||||
@@ -92,14 +92,14 @@ function getCustomSonnetOption(): ModelOption | undefined {
|
||||
provider === 'openai'
|
||||
? process.env.OPENAI_DEFAULT_SONNET_MODEL_NAME
|
||||
: provider === 'gemini'
|
||||
? process.env.GEMINI_DEFAULT_SONNET_MODEL_NAME
|
||||
: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME
|
||||
? process.env.GEMINI_DEFAULT_SONNET_MODEL_NAME
|
||||
: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_NAME
|
||||
const descEnv =
|
||||
provider === 'openai'
|
||||
? process.env.OPENAI_DEFAULT_SONNET_MODEL_DESCRIPTION
|
||||
: provider === 'gemini'
|
||||
? process.env.GEMINI_DEFAULT_SONNET_MODEL_DESCRIPTION
|
||||
: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION
|
||||
? process.env.GEMINI_DEFAULT_SONNET_MODEL_DESCRIPTION
|
||||
: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION
|
||||
return {
|
||||
value: 'sonnet',
|
||||
label: nameEnv ?? customSonnetModel,
|
||||
@@ -131,8 +131,8 @@ function getCustomOpusOption(): ModelOption | undefined {
|
||||
provider === 'openai'
|
||||
? process.env.OPENAI_DEFAULT_OPUS_MODEL
|
||||
: provider === 'gemini'
|
||||
? process.env.GEMINI_DEFAULT_OPUS_MODEL
|
||||
: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
|
||||
? process.env.GEMINI_DEFAULT_OPUS_MODEL
|
||||
: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
|
||||
// When a 3P user has a custom opus model string, show it directly
|
||||
if (is3P && customOpusModel) {
|
||||
const is1m = has1mContext(customOpusModel)
|
||||
@@ -141,14 +141,14 @@ function getCustomOpusOption(): ModelOption | undefined {
|
||||
provider === 'openai'
|
||||
? process.env.OPENAI_DEFAULT_OPUS_MODEL_NAME
|
||||
: provider === 'gemini'
|
||||
? process.env.GEMINI_DEFAULT_OPUS_MODEL_NAME
|
||||
: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME
|
||||
? process.env.GEMINI_DEFAULT_OPUS_MODEL_NAME
|
||||
: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_NAME
|
||||
const descEnv =
|
||||
provider === 'openai'
|
||||
? process.env.OPENAI_DEFAULT_OPUS_MODEL_DESCRIPTION
|
||||
: provider === 'gemini'
|
||||
? process.env.GEMINI_DEFAULT_OPUS_MODEL_DESCRIPTION
|
||||
: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION
|
||||
? process.env.GEMINI_DEFAULT_OPUS_MODEL_DESCRIPTION
|
||||
: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION
|
||||
return {
|
||||
value: 'opus',
|
||||
label: nameEnv ?? customOpusModel,
|
||||
@@ -167,13 +167,27 @@ function getOpus41Option(): ModelOption {
|
||||
}
|
||||
}
|
||||
|
||||
function getOpus46Option(fastMode = false): ModelOption {
|
||||
function getOpus47Option(fastMode = false): ModelOption {
|
||||
const is3P = getAPIProvider() !== 'firstParty'
|
||||
return {
|
||||
value: is3P ? getModelStrings().opus46 : 'opus',
|
||||
label: 'Opus',
|
||||
description: `Opus 4.6 · Most capable for complex work${getOpus46PricingSuffix(fastMode)}`,
|
||||
descriptionForModel: 'Opus 4.6 - most capable for complex work',
|
||||
value: is3P ? getModelStrings().opus47 : 'opus',
|
||||
label: 'Opus 4.7',
|
||||
description: `Opus 4.7 · Most capable for complex work${getOpusPricingSuffix(fastMode)}`,
|
||||
descriptionForModel: 'Opus 4.7 - most capable for complex work',
|
||||
}
|
||||
}
|
||||
|
||||
export function getOpus46Option(fastMode = false): ModelOption {
|
||||
// Always use the canonical 4.6 model string (not the 'opus' alias, which
|
||||
// resolves via getDefaultOpusModel() to opus47 on firstParty). Users
|
||||
// selecting "Opus 4.6" must get 4.6 actually dispatched, not alias-routed
|
||||
// to 4.7. The same string is correct for 3P (getModelStrings maps per
|
||||
// provider).
|
||||
return {
|
||||
value: getModelStrings().opus46,
|
||||
label: 'Opus 4.6',
|
||||
description: `Opus 4.6 · Previous generation Opus${getOpusPricingSuffix(fastMode)}`,
|
||||
descriptionForModel: 'Opus 4.6 - previous generation Opus model',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,12 +202,22 @@ export function getSonnet46_1MOption(): ModelOption {
|
||||
}
|
||||
}
|
||||
|
||||
export function getOpus46_1MOption(fastMode = false): ModelOption {
|
||||
export function getOpus47_1MOption(fastMode = false): ModelOption {
|
||||
const is3P = getAPIProvider() !== 'firstParty'
|
||||
return {
|
||||
value: is3P ? getModelStrings().opus46 + '[1m]' : 'opus[1m]',
|
||||
label: 'Opus (1M context)',
|
||||
description: `Opus 4.6 for long sessions${getOpus46PricingSuffix(fastMode)}`,
|
||||
value: is3P ? getModelStrings().opus47 + '[1m]' : 'opus[1m]',
|
||||
label: 'Opus 4.7 (1M context)',
|
||||
description: `Opus 4.7 with 1M context${getOpusPricingSuffix(fastMode)}`,
|
||||
descriptionForModel:
|
||||
'Opus 4.7 with 1M context window - for long sessions with large codebases',
|
||||
}
|
||||
}
|
||||
|
||||
export function getOpus46_1MOption(fastMode = false): ModelOption {
|
||||
return {
|
||||
value: getModelStrings().opus46 + '[1m]',
|
||||
label: 'Opus 4.6 (1M context)',
|
||||
description: `Opus 4.6 with 1M context${getOpusPricingSuffix(fastMode)}`,
|
||||
descriptionForModel:
|
||||
'Opus 4.6 with 1M context window - for long sessions with large codebases',
|
||||
}
|
||||
@@ -207,8 +231,8 @@ function getCustomHaikuOption(): ModelOption | undefined {
|
||||
provider === 'openai'
|
||||
? process.env.OPENAI_DEFAULT_HAIKU_MODEL
|
||||
: provider === 'gemini'
|
||||
? process.env.GEMINI_DEFAULT_HAIKU_MODEL
|
||||
: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
|
||||
? process.env.GEMINI_DEFAULT_HAIKU_MODEL
|
||||
: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL
|
||||
// When a 3P user has a custom haiku model string, show it directly
|
||||
if (is3P && customHaikuModel) {
|
||||
// Use appropriate NAME/DESCRIPTION env vars based on provider
|
||||
@@ -216,14 +240,14 @@ function getCustomHaikuOption(): ModelOption | undefined {
|
||||
provider === 'openai'
|
||||
? process.env.OPENAI_DEFAULT_HAIKU_MODEL_NAME
|
||||
: provider === 'gemini'
|
||||
? process.env.GEMINI_DEFAULT_HAIKU_MODEL_NAME
|
||||
: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME
|
||||
? process.env.GEMINI_DEFAULT_HAIKU_MODEL_NAME
|
||||
: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME
|
||||
const descEnv =
|
||||
provider === 'openai'
|
||||
? process.env.OPENAI_DEFAULT_HAIKU_MODEL_DESCRIPTION
|
||||
: provider === 'gemini'
|
||||
? process.env.GEMINI_DEFAULT_HAIKU_MODEL_DESCRIPTION
|
||||
: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION
|
||||
? process.env.GEMINI_DEFAULT_HAIKU_MODEL_DESCRIPTION
|
||||
: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION
|
||||
return {
|
||||
value: 'haiku',
|
||||
label: nameEnv ?? customHaikuModel,
|
||||
@@ -266,8 +290,8 @@ function getHaikuOption(): ModelOption {
|
||||
function getMaxOpusOption(fastMode = false): ModelOption {
|
||||
return {
|
||||
value: 'opus',
|
||||
label: 'Opus',
|
||||
description: `Opus 4.6 · Most capable for complex work${fastMode ? getOpus46PricingSuffix(true) : ''}`,
|
||||
label: 'Opus 4.7',
|
||||
description: `Opus 4.7 · Most capable for complex work${fastMode ? getOpusPricingSuffix(true) : ''}`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,23 +305,23 @@ export function getMaxSonnet46_1MOption(): ModelOption {
|
||||
}
|
||||
}
|
||||
|
||||
export function getMaxOpus46_1MOption(fastMode = false): ModelOption {
|
||||
export function getMaxOpus47_1MOption(fastMode = false): ModelOption {
|
||||
const billingInfo = isClaudeAISubscriber() ? ' · Billed as extra usage' : ''
|
||||
return {
|
||||
value: 'opus[1m]',
|
||||
label: 'Opus (1M context)',
|
||||
description: `Opus 4.6 with 1M context${billingInfo}${getOpus46PricingSuffix(fastMode)}`,
|
||||
label: 'Opus 4.7 (1M context)',
|
||||
description: `Opus 4.7 with 1M context${billingInfo}${getOpusPricingSuffix(fastMode)}`,
|
||||
}
|
||||
}
|
||||
|
||||
function getMergedOpus1MOption(fastMode = false): ModelOption {
|
||||
const is3P = getAPIProvider() !== 'firstParty'
|
||||
return {
|
||||
value: is3P ? getModelStrings().opus46 + '[1m]' : 'opus[1m]',
|
||||
label: 'Opus (1M context)',
|
||||
description: `Opus 4.6 with 1M context · Most capable for complex work${!is3P && fastMode ? getOpus46PricingSuffix(fastMode) : ''}`,
|
||||
value: is3P ? getModelStrings().opus47 + '[1m]' : 'opus[1m]',
|
||||
label: 'Opus 4.7 (1M context)',
|
||||
description: `Opus 4.7 with 1M context · Most capable for complex work${!is3P && fastMode ? getOpusPricingSuffix(fastMode) : ''}`,
|
||||
descriptionForModel:
|
||||
'Opus 4.6 with 1M context - most capable for complex work',
|
||||
'Opus 4.7 with 1M context - most capable for complex work',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +341,7 @@ function getOpusPlanOption(): ModelOption {
|
||||
return {
|
||||
value: 'opusplan',
|
||||
label: 'Opus Plan Mode',
|
||||
description: 'Use Opus 4.6 in plan mode, Sonnet 4.6 otherwise',
|
||||
description: 'Use Opus 4.7 in plan mode, Sonnet 4.6 otherwise',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,11 +368,9 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
|
||||
|
||||
if (isClaudeAISubscriber()) {
|
||||
if (isMaxSubscriber() || isTeamPremiumSubscriber()) {
|
||||
// Max and Team Premium users: Opus is default, show Sonnet as alternative
|
||||
// Max and Team Premium users: Default = Opus 4.7 1M (merged), plus Opus 4.6 1M
|
||||
const premiumOptions = [getDefaultOptionForUser(fastMode)]
|
||||
if (!isOpus1mMergeEnabled() && checkOpus1mAccess()) {
|
||||
premiumOptions.push(getMaxOpus46_1MOption(fastMode))
|
||||
}
|
||||
premiumOptions.push(getOpus46_1MOption(fastMode))
|
||||
|
||||
premiumOptions.push(MaxSonnet46Option)
|
||||
if (checkSonnet1mAccess()) {
|
||||
@@ -359,44 +381,47 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
|
||||
return premiumOptions
|
||||
}
|
||||
|
||||
// Pro/Team Standard/Enterprise users: Sonnet is default, show Opus as alternative
|
||||
// Pro/Team Standard/Enterprise users: Sonnet is default, show Opus 4.7 1M + Opus 4.6 1M
|
||||
const standardOptions = [getDefaultOptionForUser(fastMode)]
|
||||
if (checkSonnet1mAccess()) {
|
||||
standardOptions.push(getMaxSonnet46_1MOption())
|
||||
}
|
||||
|
||||
if (isOpus1mMergeEnabled()) {
|
||||
standardOptions.push(getMergedOpus1MOption(fastMode))
|
||||
} else {
|
||||
standardOptions.push(getMaxOpusOption(fastMode))
|
||||
if (checkOpus1mAccess()) {
|
||||
standardOptions.push(getMaxOpus46_1MOption(fastMode))
|
||||
standardOptions.push(getMaxOpus47_1MOption(fastMode))
|
||||
}
|
||||
}
|
||||
standardOptions.push(getOpus46_1MOption(fastMode))
|
||||
|
||||
if (checkSonnet1mAccess()) {
|
||||
standardOptions.push(getMaxSonnet46_1MOption())
|
||||
}
|
||||
|
||||
standardOptions.push(MaxHaiku45Option)
|
||||
return standardOptions
|
||||
}
|
||||
|
||||
// PAYG 1P API: Default (Sonnet) + Sonnet 1M + Opus 4.6 + Opus 1M + Haiku
|
||||
// PAYG 1P API: Default (Sonnet) + Opus 4.7 1M + Opus 4.6 1M + Sonnet 1M + Haiku
|
||||
if (getAPIProvider() === 'firstParty') {
|
||||
const payg1POptions = [getDefaultOptionForUser(fastMode)]
|
||||
if (checkSonnet1mAccess()) {
|
||||
payg1POptions.push(getSonnet46_1MOption())
|
||||
}
|
||||
if (isOpus1mMergeEnabled()) {
|
||||
payg1POptions.push(getMergedOpus1MOption(fastMode))
|
||||
} else {
|
||||
payg1POptions.push(getOpus46Option(fastMode))
|
||||
payg1POptions.push(getOpus47Option(fastMode))
|
||||
if (checkOpus1mAccess()) {
|
||||
payg1POptions.push(getOpus46_1MOption(fastMode))
|
||||
payg1POptions.push(getOpus47_1MOption(fastMode))
|
||||
}
|
||||
}
|
||||
payg1POptions.push(getOpus46_1MOption(fastMode))
|
||||
if (checkSonnet1mAccess()) {
|
||||
payg1POptions.push(getSonnet46_1MOption())
|
||||
}
|
||||
payg1POptions.push(getHaiku45Option())
|
||||
return payg1POptions
|
||||
}
|
||||
|
||||
// PAYG 3P: Default (Sonnet 4.5) + Sonnet (3P custom) or Sonnet 4.6/1M + Opus (3P custom) or Opus 4.1/Opus 4.6/Opus1M + Haiku + Opus 4.1
|
||||
// PAYG 3P: Default (Sonnet 4.5) + Sonnet (3P custom) or Sonnet 4.6/1M + Opus (3P custom) or Opus 4.7/Opus 4.6 Legacy/Opus 4.7 1M + Haiku
|
||||
const payg3pOptions = [getDefaultOptionForUser(fastMode)]
|
||||
|
||||
const customSonnet = getCustomSonnetOption()
|
||||
@@ -414,12 +439,9 @@ function getModelOptionsBase(fastMode = false): ModelOption[] {
|
||||
if (customOpus !== undefined) {
|
||||
payg3pOptions.push(customOpus)
|
||||
} else {
|
||||
// Add Opus 4.1, Opus 4.6 and Opus 4.6 1M
|
||||
payg3pOptions.push(getOpus41Option()) // This is the default opus
|
||||
payg3pOptions.push(getOpus46Option(fastMode))
|
||||
if (checkOpus1mAccess()) {
|
||||
payg3pOptions.push(getOpus46_1MOption(fastMode))
|
||||
}
|
||||
// Add Opus 4.7 1M + Opus 4.6 1M (no redundant non-1M entries)
|
||||
payg3pOptions.push(getOpus47_1MOption(fastMode))
|
||||
payg3pOptions.push(getOpus46_1MOption(fastMode))
|
||||
}
|
||||
const customHaiku = getCustomHaikuOption()
|
||||
if (customHaiku !== undefined) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getAPIProvider } from './providers.js'
|
||||
export type ModelCapabilityOverride =
|
||||
| 'effort'
|
||||
| 'max_effort'
|
||||
| 'xhigh_effort'
|
||||
| 'thinking'
|
||||
| 'adaptive_thinking'
|
||||
| 'interleaved_thinking'
|
||||
|
||||
@@ -146,6 +146,9 @@ function get3PFallbackSuggestion(model: string): string | undefined {
|
||||
return undefined
|
||||
}
|
||||
const lowerModel = model.toLowerCase()
|
||||
if (lowerModel.includes('opus-4-7') || lowerModel.includes('opus_4_7')) {
|
||||
return getModelStrings().opus46
|
||||
}
|
||||
if (lowerModel.includes('opus-4-6') || lowerModel.includes('opus_4_6')) {
|
||||
return getModelStrings().opus41
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user