feat(models): support Anthropic runtime catalog options
This commit is contained in:
parent
3265920ec6
commit
7c0e3db0b7
5 changed files with 335 additions and 30 deletions
|
|
@ -280,6 +280,16 @@ function hasAnthropicCompatibleRuntimeCatalog(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasAnthropicFirstPartyRuntimeCatalog(
|
||||||
|
providerStatus?: TeamModelRuntimeProviderStatus | null
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
providerStatus?.modelCatalog?.providerId === 'anthropic' &&
|
||||||
|
providerStatus.modelCatalog.source === 'anthropic-models-api' &&
|
||||||
|
providerStatus.modelCatalog.status !== 'unavailable'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function isAnthropicCompatibleRuntime(
|
export function isAnthropicCompatibleRuntime(
|
||||||
providerStatus?: TeamModelRuntimeProviderStatus | null
|
providerStatus?: TeamModelRuntimeProviderStatus | null
|
||||||
): boolean {
|
): boolean {
|
||||||
|
|
@ -323,6 +333,76 @@ export function canUseCustomAnthropicCompatibleModel(
|
||||||
return catalog.status !== 'ready' || !hasVisibleAnthropicCompatibleCatalogModels(providerStatus);
|
return catalog.status !== 'ready' || !hasVisibleAnthropicCompatibleCatalogModels(providerStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getVisibleRuntimeCatalogModels(
|
||||||
|
providerStatus?: TeamModelRuntimeProviderStatus | null
|
||||||
|
): string[] | null {
|
||||||
|
if (!providerStatus?.modelCatalog) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const models = providerStatus.modelCatalog.models
|
||||||
|
.filter((model) => !model.hidden)
|
||||||
|
.map((model) => model.launchModel.trim() || model.id.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return models.length > 0 ? models : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAnthropicFirstPartyRuntimeModels(
|
||||||
|
providerStatus?: TeamModelRuntimeProviderStatus | null
|
||||||
|
): string[] | null {
|
||||||
|
if (!hasAnthropicFirstPartyRuntimeCatalog(providerStatus)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getVisibleRuntimeCatalogModels(providerStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAnthropicFirstPartySelectorModels(
|
||||||
|
providerStatus?: TeamModelRuntimeProviderStatus | null
|
||||||
|
): string[] | null {
|
||||||
|
const catalogModels = getAnthropicFirstPartyRuntimeModels(providerStatus);
|
||||||
|
if (!catalogModels) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const catalogModelSet = new Set(catalogModels);
|
||||||
|
const seenModels = new Set<string>();
|
||||||
|
const seenLabels = new Set<string>();
|
||||||
|
const merged: string[] = [];
|
||||||
|
|
||||||
|
const appendModel = (model: string, skipDuplicateLabel: boolean): void => {
|
||||||
|
const trimmed = model.trim();
|
||||||
|
if (!trimmed || seenModels.has(trimmed)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label =
|
||||||
|
getRuntimeAwareProviderScopedTeamModelLabel('anthropic', trimmed, providerStatus) ?? trimmed;
|
||||||
|
const labelKey = label.trim().toLowerCase();
|
||||||
|
if (skipDuplicateLabel && labelKey && seenLabels.has(labelKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenModels.add(trimmed);
|
||||||
|
if (labelKey) {
|
||||||
|
seenLabels.add(labelKey);
|
||||||
|
}
|
||||||
|
merged.push(trimmed);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const option of getTeamProviderModelOptions('anthropic')) {
|
||||||
|
if (option.value && !catalogModelSet.has(option.value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
appendModel(option.value, false);
|
||||||
|
}
|
||||||
|
for (const model of catalogModels) {
|
||||||
|
appendModel(model, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
function getAnthropicCatalogModel(
|
function getAnthropicCatalogModel(
|
||||||
model: string,
|
model: string,
|
||||||
providerStatus?: TeamModelRuntimeProviderStatus | null
|
providerStatus?: TeamModelRuntimeProviderStatus | null
|
||||||
|
|
@ -350,15 +430,7 @@ function getRuntimeCatalogModels(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!providerStatus?.modelCatalog) {
|
return getVisibleRuntimeCatalogModels(providerStatus);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const models = providerStatus.modelCatalog.models
|
|
||||||
.filter((model) => !model.hidden)
|
|
||||||
.map((model) => model.launchModel.trim() || model.id.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
return models.length > 0 ? models : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRuntimeCatalogModelOption(
|
function getRuntimeCatalogModelOption(
|
||||||
|
|
@ -368,7 +440,7 @@ function getRuntimeCatalogModelOption(
|
||||||
): TeamRuntimeModelOption | null {
|
): TeamRuntimeModelOption | null {
|
||||||
const canUseCatalog =
|
const canUseCatalog =
|
||||||
(providerId === 'codex' && providerStatus?.modelCatalog?.providerId === 'codex') ||
|
(providerId === 'codex' && providerStatus?.modelCatalog?.providerId === 'codex') ||
|
||||||
(providerId === 'anthropic' && hasAnthropicCompatibleRuntimeCatalog(providerStatus));
|
(providerId === 'anthropic' && hasAnthropicRuntimeCatalog(providerStatus));
|
||||||
if (!canUseCatalog || !providerStatus?.modelCatalog) {
|
if (!canUseCatalog || !providerStatus?.modelCatalog) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -384,10 +456,11 @@ function getRuntimeCatalogModelOption(
|
||||||
return {
|
return {
|
||||||
value: launchModel,
|
value: launchModel,
|
||||||
label:
|
label:
|
||||||
|
getRuntimeAwareProviderScopedTeamModelLabel(providerId, launchModel, providerStatus) ??
|
||||||
getProviderScopedTeamModelLabel(providerId, catalogModel.displayName) ??
|
getProviderScopedTeamModelLabel(providerId, catalogModel.displayName) ??
|
||||||
catalogModel.displayName,
|
catalogModel.displayName,
|
||||||
badgeLabel:
|
badgeLabel:
|
||||||
catalogModel.badgeLabel ??
|
getRuntimeAwareTeamModelBadgeLabel(providerId, launchModel, providerStatus) ??
|
||||||
(getTeamProviderModelOptions(providerId).some((option) => option.value === model)
|
(getTeamProviderModelOptions(providerId).some((option) => option.value === model)
|
||||||
? undefined
|
? undefined
|
||||||
: 'New'),
|
: 'New'),
|
||||||
|
|
@ -505,7 +578,9 @@ function getRuntimeModelAvailability(
|
||||||
return isSupportedAnthropicTeamModel(model) ? 'available' : null;
|
return isSupportedAnthropicTeamModel(model) ? 'available' : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getAnthropicCatalogModel(model, providerStatus) ? 'available' : null;
|
return getAnthropicCatalogModel(model, providerStatus) || isSupportedAnthropicTeamModel(model)
|
||||||
|
? 'available'
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!providerStatus) {
|
if (!providerStatus) {
|
||||||
|
|
@ -537,7 +612,8 @@ export function getTeamProviderModelVerificationCounts(
|
||||||
if (providerId === 'anthropic') {
|
if (providerId === 'anthropic') {
|
||||||
const visibleAnthropicModels = isAnthropicCompatibleRuntime(providerStatus)
|
const visibleAnthropicModels = isAnthropicCompatibleRuntime(providerStatus)
|
||||||
? getRuntimeSelectorModels(providerId, providerStatus)
|
? getRuntimeSelectorModels(providerId, providerStatus)
|
||||||
: getFallbackTeamProviderModels(providerId);
|
: (getAnthropicFirstPartySelectorModels(providerStatus) ??
|
||||||
|
getFallbackTeamProviderModels(providerId));
|
||||||
return {
|
return {
|
||||||
checkedCount: visibleAnthropicModels.length,
|
checkedCount: visibleAnthropicModels.length,
|
||||||
totalCount: visibleAnthropicModels.length,
|
totalCount: visibleAnthropicModels.length,
|
||||||
|
|
@ -565,8 +641,16 @@ export function getAvailableTeamProviderModels(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return getFallbackTeamProviderModels(providerId).filter(
|
const visibleAnthropicModels =
|
||||||
(model) => getRuntimeModelAvailability(providerId, model, providerStatus) === 'available'
|
getAnthropicFirstPartySelectorModels(providerStatus) ??
|
||||||
|
getFallbackTeamProviderModels(providerId);
|
||||||
|
|
||||||
|
return sortTeamProviderModels(
|
||||||
|
providerId,
|
||||||
|
visibleAnthropicModels.filter(
|
||||||
|
(model) => getRuntimeModelAvailability(providerId, model, providerStatus) === 'available'
|
||||||
|
),
|
||||||
|
providerStatus
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -607,6 +691,34 @@ export function getAvailableTeamProviderModelOptions(
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const firstPartyModels = getAnthropicFirstPartySelectorModels(providerStatus);
|
||||||
|
if (firstPartyModels) {
|
||||||
|
const fallbackOptions = getFallbackTeamProviderModelOptions(providerId, providerStatus);
|
||||||
|
const fallbackOptionByValue = new Map(
|
||||||
|
fallbackOptions.map((option) => [option.value, option])
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
fallbackOptionByValue.get('') ?? { value: '', label: 'Default', badgeLabel: 'Default' },
|
||||||
|
...firstPartyModels.map((model) => {
|
||||||
|
const catalogOption = getRuntimeCatalogModelOption(providerId, model, providerStatus);
|
||||||
|
const fallbackOption = fallbackOptionByValue.get(model);
|
||||||
|
const option = catalogOption ??
|
||||||
|
fallbackOption ?? {
|
||||||
|
value: model,
|
||||||
|
label: getProviderScopedTeamModelLabel(providerId, model) ?? model,
|
||||||
|
badgeLabel: getRuntimeAwareTeamModelBadgeLabel(providerId, model, providerStatus),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...option,
|
||||||
|
availabilityStatus: getRuntimeModelAvailability(providerId, model, providerStatus),
|
||||||
|
availabilityReason: getRuntimeModelAvailabilityReason(model, providerStatus),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return getFallbackTeamProviderModelOptions(providerId, providerStatus).map((option) => ({
|
return getFallbackTeamProviderModelOptions(providerId, providerStatus).map((option) => ({
|
||||||
...option,
|
...option,
|
||||||
availabilityStatus:
|
availabilityStatus:
|
||||||
|
|
@ -691,7 +803,10 @@ export function isTeamModelAvailableForUi(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSupportedAnthropicTeamModel(trimmed)) {
|
if (
|
||||||
|
!isSupportedAnthropicTeamModel(trimmed) &&
|
||||||
|
!getAnthropicCatalogModel(trimmed, providerStatus)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,8 @@ const ANTHROPIC_MODEL_ORDER = [
|
||||||
const TEAM_MODEL_LABEL_OVERRIDES: Record<string, string> = {
|
const TEAM_MODEL_LABEL_OVERRIDES: Record<string, string> = {
|
||||||
default: 'Default',
|
default: 'Default',
|
||||||
...ANTHROPIC_ALIAS_LABELS,
|
...ANTHROPIC_ALIAS_LABELS,
|
||||||
|
'opus[1m]': 'Opus 4.8 (1M)',
|
||||||
|
'sonnet[1m]': 'Sonnet 4.6 (1M)',
|
||||||
'claude-opus-4-8': 'Opus 4.8',
|
'claude-opus-4-8': 'Opus 4.8',
|
||||||
'claude-opus-4-8[1m]': 'Opus 4.8 (1M)',
|
'claude-opus-4-8[1m]': 'Opus 4.8 (1M)',
|
||||||
'claude-opus-4-7': 'Opus 4.7',
|
'claude-opus-4-7': 'Opus 4.7',
|
||||||
|
|
@ -343,6 +345,81 @@ function getRuntimeCatalogModel(
|
||||||
return getRuntimeCatalogModelIndex(providerStatus.modelCatalog).get(trimmed) ?? null;
|
return getRuntimeCatalogModelIndex(providerStatus.modelCatalog).get(trimmed) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAnthropicAliasFamily(model: string | undefined): 'opus' | 'sonnet' | 'haiku' | null {
|
||||||
|
const baseModel =
|
||||||
|
model
|
||||||
|
?.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\[1m\]$/i, '') ?? '';
|
||||||
|
if (baseModel === 'opus' || baseModel === 'sonnet' || baseModel === 'haiku') {
|
||||||
|
return baseModel;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readAnthropicDisplayVersion(
|
||||||
|
label: string | undefined,
|
||||||
|
family: 'opus' | 'sonnet' | 'haiku'
|
||||||
|
): { major: number; minor: number | null } | null {
|
||||||
|
const pattern = new RegExp(`\\b${family}\\s+(\\d+)(?:\\.(\\d+))?\\b`, 'i');
|
||||||
|
const match = pattern.exec(label ?? '');
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const major = Number.parseInt(match[1], 10);
|
||||||
|
const minor = match[2] == null ? null : Number.parseInt(match[2], 10);
|
||||||
|
if (!Number.isFinite(major) || (minor !== null && !Number.isFinite(minor))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { major, minor };
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareAnthropicDisplayVersions(
|
||||||
|
left: { major: number; minor: number | null },
|
||||||
|
right: { major: number; minor: number | null }
|
||||||
|
): number {
|
||||||
|
if (left.major !== right.major) {
|
||||||
|
return left.major - right.major;
|
||||||
|
}
|
||||||
|
return (left.minor ?? 0) - (right.minor ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRuntimeSafeAnthropicAliasLabel(params: {
|
||||||
|
model: string | undefined;
|
||||||
|
runtimeLabel?: string | null;
|
||||||
|
fallbackLabel?: string;
|
||||||
|
}): string | null {
|
||||||
|
const family = getAnthropicAliasFamily(params.model);
|
||||||
|
if (!family) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackLabel =
|
||||||
|
params.fallbackLabel ?? getProviderScopedTeamModelLabel('anthropic', params.model);
|
||||||
|
if (!fallbackLabel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtimeLabel = params.runtimeLabel?.trim();
|
||||||
|
if (!runtimeLabel) {
|
||||||
|
return fallbackLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtimeVersion = readAnthropicDisplayVersion(runtimeLabel, family);
|
||||||
|
const fallbackVersion = readAnthropicDisplayVersion(fallbackLabel, family);
|
||||||
|
if (
|
||||||
|
runtimeVersion &&
|
||||||
|
fallbackVersion &&
|
||||||
|
compareAnthropicDisplayVersions(runtimeVersion, fallbackVersion) >= 0
|
||||||
|
) {
|
||||||
|
return getProviderScopedTeamModelLabel('anthropic', runtimeLabel) ?? runtimeLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackLabel;
|
||||||
|
}
|
||||||
|
|
||||||
export function getTeamModelBadgeLabel(
|
export function getTeamModelBadgeLabel(
|
||||||
providerId: SupportedProviderId,
|
providerId: SupportedProviderId,
|
||||||
model: string | undefined
|
model: string | undefined
|
||||||
|
|
@ -410,12 +487,16 @@ export function getRuntimeAwareProviderScopedTeamModelLabel(
|
||||||
providerStatus?: RuntimeAwareProviderStatus | null
|
providerStatus?: RuntimeAwareProviderStatus | null
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const trimmed = model?.trim();
|
const trimmed = model?.trim();
|
||||||
if (providerId === 'anthropic' && (trimmed === 'opus' || trimmed === 'opus[1m]')) {
|
|
||||||
return getProviderScopedTeamModelLabel(providerId, trimmed);
|
|
||||||
}
|
|
||||||
|
|
||||||
const runtimeModel = getRuntimeCatalogModel(providerId, model, providerStatus);
|
const runtimeModel = getRuntimeCatalogModel(providerId, model, providerStatus);
|
||||||
const runtimeLabel = runtimeModel?.displayName?.trim();
|
const runtimeLabel = runtimeModel?.displayName?.trim();
|
||||||
|
const safeAnthropicAliasLabel =
|
||||||
|
providerId === 'anthropic'
|
||||||
|
? getRuntimeSafeAnthropicAliasLabel({ model: trimmed, runtimeLabel })
|
||||||
|
: null;
|
||||||
|
if (safeAnthropicAliasLabel) {
|
||||||
|
return safeAnthropicAliasLabel;
|
||||||
|
}
|
||||||
|
|
||||||
if (runtimeLabel) {
|
if (runtimeLabel) {
|
||||||
return getProviderScopedTeamModelLabel(providerId, runtimeLabel) ?? runtimeLabel;
|
return getProviderScopedTeamModelLabel(providerId, runtimeLabel) ?? runtimeLabel;
|
||||||
}
|
}
|
||||||
|
|
@ -429,11 +510,19 @@ export function getRuntimeAwareTeamModelBadgeLabel(
|
||||||
providerStatus?: RuntimeAwareProviderStatus | null
|
providerStatus?: RuntimeAwareProviderStatus | null
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const trimmed = model?.trim();
|
const trimmed = model?.trim();
|
||||||
if (providerId === 'anthropic' && (trimmed === 'opus' || trimmed === 'opus[1m]')) {
|
const runtimeModel = getRuntimeCatalogModel(providerId, model, providerStatus);
|
||||||
return getTeamModelBadgeLabel(providerId, trimmed);
|
const safeAnthropicAliasLabel =
|
||||||
|
providerId === 'anthropic'
|
||||||
|
? getRuntimeSafeAnthropicAliasLabel({
|
||||||
|
model: trimmed,
|
||||||
|
runtimeLabel: runtimeModel?.badgeLabel?.trim() || runtimeModel?.displayName?.trim(),
|
||||||
|
fallbackLabel: getTeamModelBadgeLabel(providerId, trimmed),
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
if (safeAnthropicAliasLabel) {
|
||||||
|
return safeAnthropicAliasLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
const runtimeModel = getRuntimeCatalogModel(providerId, model, providerStatus);
|
|
||||||
if (runtimeModel?.badgeLabel?.trim()) {
|
if (runtimeModel?.badgeLabel?.trim()) {
|
||||||
return runtimeModel.badgeLabel.trim();
|
return runtimeModel.badgeLabel.trim();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,12 @@ describe('contextMetrics', () => {
|
||||||
modelName: 'claude-sonnet-4-6',
|
modelName: 'claude-sonnet-4-6',
|
||||||
})
|
})
|
||||||
).toBe(1_000_000);
|
).toBe(1_000_000);
|
||||||
|
expect(
|
||||||
|
inferContextWindowTokens({
|
||||||
|
providerId: 'anthropic',
|
||||||
|
modelName: 'claude-opus-4-9',
|
||||||
|
})
|
||||||
|
).toBe(1_000_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps older raw Anthropic models at 200K unless 1M is explicitly requested', () => {
|
it('keeps older raw Anthropic models at 200K unless 1M is explicitly requested', () => {
|
||||||
|
|
@ -142,6 +148,12 @@ describe('contextMetrics', () => {
|
||||||
modelName: 'claude-sonnet-4-5-20250929',
|
modelName: 'claude-sonnet-4-5-20250929',
|
||||||
})
|
})
|
||||||
).toBe(200_000);
|
).toBe(200_000);
|
||||||
|
expect(
|
||||||
|
inferContextWindowTokens({
|
||||||
|
providerId: 'anthropic',
|
||||||
|
modelName: 'claude-opus-4-20250514',
|
||||||
|
})
|
||||||
|
).toBe(200_000);
|
||||||
expect(
|
expect(
|
||||||
inferContextWindowTokens({
|
inferContextWindowTokens({
|
||||||
providerId: 'anthropic',
|
providerId: 'anthropic',
|
||||||
|
|
|
||||||
|
|
@ -111,13 +111,17 @@ function isAnthropicNativeLongContextModel(modelName: string | undefined): boole
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (normalized.startsWith('claude-mythos')) {
|
||||||
normalized.startsWith('claude-opus-4-8') ||
|
return true;
|
||||||
normalized.startsWith('claude-opus-4-7') ||
|
}
|
||||||
normalized.startsWith('claude-opus-4-6') ||
|
|
||||||
normalized.startsWith('claude-sonnet-4-6') ||
|
const claude4MinorMatch = /^claude-(opus|sonnet)-4-(\d{1,2})(?:-|$)/.exec(normalized);
|
||||||
normalized.startsWith('claude-mythos')
|
if (!claude4MinorMatch) {
|
||||||
);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minorVersion = Number.parseInt(claude4MinorMatch[2], 10);
|
||||||
|
return Number.isFinite(minorVersion) && minorVersion >= 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasOpenAiPromptDetails(usage: ContextUsageLike): boolean {
|
function hasOpenAiPromptDetails(usage: ContextUsageLike): boolean {
|
||||||
|
|
|
||||||
|
|
@ -644,6 +644,91 @@ describe('teamModelAvailability', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('merges first-party Anthropic catalog models with curated safety fallbacks', () => {
|
||||||
|
const providerStatus: TeamModelRuntimeProviderStatus = {
|
||||||
|
providerId: 'anthropic',
|
||||||
|
models: ['opus', 'claude-sonnet-4-7'],
|
||||||
|
authMethod: 'oauth',
|
||||||
|
backend: null,
|
||||||
|
authenticated: true,
|
||||||
|
supported: true,
|
||||||
|
modelVerificationState: 'idle',
|
||||||
|
modelAvailability: [],
|
||||||
|
modelCatalog: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
providerId: 'anthropic',
|
||||||
|
source: 'anthropic-models-api',
|
||||||
|
status: 'ready',
|
||||||
|
fetchedAt: '2026-06-20T00:00:00.000Z',
|
||||||
|
staleAt: '2026-06-20T00:10:00.000Z',
|
||||||
|
defaultModelId: 'opus',
|
||||||
|
defaultLaunchModel: 'opus',
|
||||||
|
diagnostics: {
|
||||||
|
configReadState: 'ready',
|
||||||
|
appServerState: 'healthy',
|
||||||
|
},
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: 'opus',
|
||||||
|
launchModel: 'opus',
|
||||||
|
displayName: 'Opus 4.9',
|
||||||
|
hidden: false,
|
||||||
|
supportedReasoningEfforts: ['low', 'medium', 'high', 'max'],
|
||||||
|
defaultReasoningEffort: 'high',
|
||||||
|
inputModalities: ['text', 'image'],
|
||||||
|
supportsPersonality: false,
|
||||||
|
isDefault: true,
|
||||||
|
upgrade: false,
|
||||||
|
source: 'anthropic-models-api',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'claude-sonnet-4-7',
|
||||||
|
launchModel: 'claude-sonnet-4-7',
|
||||||
|
displayName: 'Sonnet 4.7',
|
||||||
|
hidden: false,
|
||||||
|
supportedReasoningEfforts: ['low', 'medium', 'high', 'max'],
|
||||||
|
defaultReasoningEffort: 'high',
|
||||||
|
inputModalities: ['text', 'image'],
|
||||||
|
supportsPersonality: false,
|
||||||
|
isDefault: false,
|
||||||
|
upgrade: false,
|
||||||
|
source: 'anthropic-models-api',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'claude-sonnet-4-7[1m]',
|
||||||
|
launchModel: 'claude-sonnet-4-7[1m]',
|
||||||
|
displayName: 'Sonnet 4.7 (1M)',
|
||||||
|
hidden: true,
|
||||||
|
supportedReasoningEfforts: ['low', 'medium', 'high', 'max'],
|
||||||
|
defaultReasoningEffort: 'high',
|
||||||
|
inputModalities: ['text', 'image'],
|
||||||
|
supportsPersonality: false,
|
||||||
|
isDefault: false,
|
||||||
|
upgrade: false,
|
||||||
|
source: 'anthropic-models-api',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = getAvailableTeamProviderModelOptions('anthropic', providerStatus);
|
||||||
|
const values = options.map((option) => option.value);
|
||||||
|
|
||||||
|
expect(options.find((option) => option.value === 'opus')).toMatchObject({
|
||||||
|
label: 'Opus 4.9',
|
||||||
|
badgeLabel: 'Opus 4.9',
|
||||||
|
});
|
||||||
|
expect(values).toContain('claude-sonnet-4-7');
|
||||||
|
expect(values).toContain('claude-opus-4-7');
|
||||||
|
expect(values).not.toContain('claude-sonnet-4-7[1m]');
|
||||||
|
expect(normalizeTeamModelForUi('anthropic', 'claude-sonnet-4-7', providerStatus)).toBe(
|
||||||
|
'claude-sonnet-4-7'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
getTeamModelSelectionError('anthropic', 'claude-sonnet-4-7', providerStatus)
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps known Anthropic full model ids selectable without runtime verification', () => {
|
it('keeps known Anthropic full model ids selectable without runtime verification', () => {
|
||||||
expect(normalizeTeamModelForUi('anthropic', 'claude-opus-4-8')).toBe('claude-opus-4-8');
|
expect(normalizeTeamModelForUi('anthropic', 'claude-opus-4-8')).toBe('claude-opus-4-8');
|
||||||
expect(normalizeTeamModelForUi('anthropic', 'claude-opus-4-8[1m]')).toBe('claude-opus-4-8[1m]');
|
expect(normalizeTeamModelForUi('anthropic', 'claude-opus-4-8[1m]')).toBe('claude-opus-4-8[1m]');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue