From 62dff69300816bc3c474e9a96a9504bed13eda11 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 28 May 2026 13:50:39 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=A0=20feat:=20Add=20Claude=20Opus=204.?= =?UTF-8?q?8=20Support=20(#13380)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add Claude Opus 4.8 support * fix: omit sampling params for Claude Opus 4.8 * fix: flatten Bedrock beta header merge * fix: strip Bedrock sampling params for Opus 4.8 --- .env.example | 6 +- api/utils/tokens.spec.js | 31 ++++++++ client/src/locales/en/translation.json | 2 +- librechat.example.yaml | 3 + .../api/src/endpoints/anthropic/llm.spec.ts | 18 ++++- packages/api/src/endpoints/anthropic/llm.ts | 16 +++- packages/api/src/utils/tokens.ts | 2 + packages/data-provider/specs/bedrock.spec.ts | 79 ++++++++++++++++++- packages/data-provider/src/bedrock.ts | 43 ++++++++-- packages/data-provider/src/config.ts | 2 + packages/data-schemas/src/methods/tx.spec.ts | 47 ++++++++++- packages/data-schemas/src/methods/tx.ts | 2 + 12 files changed, 234 insertions(+), 17 deletions(-) diff --git a/.env.example b/.env.example index cb8653ad40..b5f9f79474 100644 --- a/.env.example +++ b/.env.example @@ -171,7 +171,7 @@ PROXY= #============# ANTHROPIC_API_KEY=user_provided -# ANTHROPIC_MODELS=claude-opus-4-7,claude-sonnet-4-6,claude-opus-4-6,claude-opus-4-20250514,claude-3-7-sonnet-20250219,claude-3-5-sonnet-20241022,claude-3-5-haiku-20241022,claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307 +# ANTHROPIC_MODELS=claude-opus-4-8,claude-opus-4-7,claude-sonnet-4-6,claude-opus-4-6,claude-opus-4-20250514,claude-3-7-sonnet-20250219,claude-3-5-sonnet-20241022,claude-3-5-haiku-20241022,claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307 # ANTHROPIC_REVERSE_PROXY= # Set to true to use Anthropic models through Google Vertex AI instead of direct API @@ -237,8 +237,8 @@ ANTHROPIC_API_KEY=user_provided # BEDROCK_AWS_BEARER_TOKEN=yourBedrockApiKey # Note: This example list is not meant to be exhaustive. If omitted, all known, supported model IDs will be included for you. -# BEDROCK_AWS_MODELS=anthropic.claude-opus-4-7,anthropic.claude-sonnet-4-6,anthropic.claude-opus-4-6-v1,anthropic.claude-3-5-sonnet-20240620-v1:0,meta.llama3-1-8b-instruct-v1:0 -# Cross-region inference model IDs: us.anthropic.claude-opus-4-7,us.anthropic.claude-sonnet-4-6,us.anthropic.claude-opus-4-6-v1,global.anthropic.claude-opus-4-6-v1 +# BEDROCK_AWS_MODELS=anthropic.claude-opus-4-8,anthropic.claude-opus-4-7,anthropic.claude-sonnet-4-6,anthropic.claude-opus-4-6-v1,anthropic.claude-3-5-sonnet-20240620-v1:0,meta.llama3-1-8b-instruct-v1:0 +# Cross-region inference model IDs: us.anthropic.claude-opus-4-8,us.anthropic.claude-opus-4-7,us.anthropic.claude-sonnet-4-6,us.anthropic.claude-opus-4-6-v1,global.anthropic.claude-opus-4-6-v1 # See all Bedrock model IDs here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns diff --git a/api/utils/tokens.spec.js b/api/utils/tokens.spec.js index ceb44c3620..a97694a872 100644 --- a/api/utils/tokens.spec.js +++ b/api/utils/tokens.spec.js @@ -1459,6 +1459,37 @@ describe('Claude Model Tests', () => { }); }); + it('should return correct context length for Claude Opus 4.8 (1M)', () => { + expect(getModelMaxTokens('claude-opus-4-8', EModelEndpoint.anthropic)).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-opus-4-8'], + ); + expect(getModelMaxTokens('claude-opus-4-8')).toBe( + maxTokensMap[EModelEndpoint.anthropic]['claude-opus-4-8'], + ); + }); + + it('should return correct max output tokens for Claude Opus 4.8 (128K)', () => { + const { getModelMaxOutputTokens } = require('@librechat/api'); + expect(getModelMaxOutputTokens('claude-opus-4-8', EModelEndpoint.anthropic)).toBe( + maxOutputTokensMap[EModelEndpoint.anthropic]['claude-opus-4-8'], + ); + }); + + it('should match model names correctly for Claude Opus 4.8', () => { + const modelVariations = [ + 'claude-opus-4-8', + 'claude-opus-4-8-20260528', + 'claude-opus-4-8-latest', + 'anthropic/claude-opus-4-8', + 'claude-opus-4-8/anthropic', + 'claude-opus-4-8-preview', + ]; + + modelVariations.forEach((model) => { + expect(matchModelName(model, EModelEndpoint.anthropic)).toBe('claude-opus-4-8'); + }); + }); + it('should return correct context length for Claude Sonnet 4.6 (1M)', () => { expect(getModelMaxTokens('claude-sonnet-4-6', EModelEndpoint.anthropic)).toBe( maxTokensMap[EModelEndpoint.anthropic]['claude-sonnet-4-6'], diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 64604e895d..f8c859fc55 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -223,7 +223,7 @@ "com_endpoint_agent": "Agent", "com_endpoint_agent_placeholder": "Please select an Agent", "com_endpoint_ai": "AI", - "com_endpoint_anthropic_effort": "Controls how much computational effort Claude applies. Lower effort saves tokens and reduces latency; higher effort produces more thorough responses. 'Max' enables the deepest reasoning (Opus 4.6 only).", + "com_endpoint_anthropic_effort": "Controls how much computational effort Claude applies. Lower effort saves tokens and reduces latency; higher effort produces more thorough responses. 'Max' enables the deepest reasoning on supported adaptive-thinking models.", "com_endpoint_anthropic_maxoutputtokens": "Maximum number of tokens that can be generated in the response. Specify a lower value for shorter responses and a higher value for longer responses. Note: models may stop before reaching this maximum.", "com_endpoint_anthropic_prompt_cache": "Prompt caching allows reusing large context or instructions across API calls, reducing costs and latency", "com_endpoint_anthropic_temp": "Ranges from 0 to 1. Use temp closer to 0 for analytical / multiple choice, and closer to 1 for creative and generative tasks. We recommend altering this or Top P but not both.", diff --git a/librechat.example.yaml b/librechat.example.yaml index 58dab0300a..0887cbba15 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -401,6 +401,7 @@ endpoints: # # Option 1: Simple array (legacy format - model name = deployment name) # # Use this if you want the technical model IDs to show in the UI # # models: + # # - "claude-opus-4-8" # # - "claude-sonnet-4-6" # # - "claude-3-7-sonnet-20250219" # # - "claude-3-5-sonnet-v2@20241022" @@ -411,6 +412,8 @@ endpoints: # # The deploymentName is the actual Vertex AI model ID used for API calls # # You can use friendly names (avoid spaces for cleaner YAML) or technical IDs as keys # models: + # claude-opus-4.8: + # deploymentName: claude-opus-4-8 # claude-opus-4.5: # deploymentName: claude-opus-4-5@20251101 # claude-sonnet-4: diff --git a/packages/api/src/endpoints/anthropic/llm.spec.ts b/packages/api/src/endpoints/anthropic/llm.spec.ts index 9ab4ef11e8..41fc898cff 100644 --- a/packages/api/src/endpoints/anthropic/llm.spec.ts +++ b/packages/api/src/endpoints/anthropic/llm.spec.ts @@ -888,7 +888,7 @@ describe('getLLMConfig', () => { expect(opus64kResult.llmConfig.maxTokens).toBe(64000); // opus-4-6+ get 128K - const opus128kModels = ['claude-opus-4-7', 'claude-opus-4-10']; + const opus128kModels = ['claude-opus-4-7', 'claude-opus-4-8', 'claude-opus-4-10']; opus128kModels.forEach((model) => { const result = getLLMConfig('test-key', { modelOptions: { model }, @@ -1066,6 +1066,22 @@ describe('getLLMConfig', () => { expect(thinking.display).toBe('summarized'); }); + it('should omit sampling parameters for Opus 4.8', () => { + const result = getLLMConfig('test-key', { + modelOptions: { + model: 'claude-opus-4-8', + thinking: false, + temperature: 0.7, + topP: 0.9, + topK: 40, + }, + }); + + expect(result.llmConfig).not.toHaveProperty('temperature'); + expect(result.llmConfig).not.toHaveProperty('topP'); + expect(result.llmConfig).not.toHaveProperty('topK'); + }); + it('should NOT set thinking.display for pre-Opus-4.7 adaptive models', () => { const pre47Models = ['claude-opus-4-6', 'claude-sonnet-4-6']; diff --git a/packages/api/src/endpoints/anthropic/llm.ts b/packages/api/src/endpoints/anthropic/llm.ts index 0c4929621e..134fbcbd40 100644 --- a/packages/api/src/endpoints/anthropic/llm.ts +++ b/packages/api/src/endpoints/anthropic/llm.ts @@ -3,6 +3,7 @@ import { logger } from '@librechat/data-schemas'; import { AnthropicClientOptions } from '@librechat/agents'; import { anthropicSettings, + omitsSamplingParameters, removeNullishValues, ThinkingDisplay, AuthKeys, @@ -177,9 +178,12 @@ function getLLMConfig( ); } + const resolvedModel = requestOptions.model ?? mergedOptions.model; + const shouldOmitSamplingParameters = omitsSamplingParameters(resolvedModel); + requestOptions = configureReasoning(requestOptions, systemOptions); - if (supportsAdaptiveThinking(mergedOptions.model)) { + if (supportsAdaptiveThinking(resolvedModel)) { if ( systemOptions.effort && (systemOptions.effort as string) !== '' && @@ -204,8 +208,8 @@ function getLLMConfig( const hasActiveThinking = requestOptions.thinking != null; const isThinkingModel = - /claude-3[-.]7/.test(mergedOptions.model) || supportsAdaptiveThinking(mergedOptions.model); - if (!isThinkingModel || !hasActiveThinking) { + /claude-3[-.]7/.test(resolvedModel) || supportsAdaptiveThinking(resolvedModel); + if (!shouldOmitSamplingParameters && (!isThinkingModel || !hasActiveThinking)) { requestOptions.topP = mergedOptions.topP; requestOptions.topK = mergedOptions.topK; } @@ -293,6 +297,12 @@ function getLLMConfig( }); } + if (shouldOmitSamplingParameters) { + delete requestOptions.temperature; + delete requestOptions.topP; + delete requestOptions.topK; + } + const tools = []; if (enableWebSearch) { diff --git a/packages/api/src/utils/tokens.ts b/packages/api/src/utils/tokens.ts index 019a0f1366..7e5cb53fff 100644 --- a/packages/api/src/utils/tokens.ts +++ b/packages/api/src/utils/tokens.ts @@ -146,6 +146,7 @@ const anthropicModels = { 'claude-sonnet-4-6': 1000000, 'claude-opus-4-6': 1000000, 'claude-opus-4-7': 1000000, + 'claude-opus-4-8': 1000000, }; const deepseekModels = { @@ -394,6 +395,7 @@ const anthropicMaxOutputs = { 'claude-opus-4-5': 64000, 'claude-opus-4-6': 128000, 'claude-opus-4-7': 128000, + 'claude-opus-4-8': 128000, 'claude-3.5-sonnet': 8192, 'claude-3-5-sonnet': 8192, 'claude-3.7-sonnet': 128000, diff --git a/packages/data-provider/specs/bedrock.spec.ts b/packages/data-provider/specs/bedrock.spec.ts index bbb57310c6..d54e5c504e 100644 --- a/packages/data-provider/specs/bedrock.spec.ts +++ b/packages/data-provider/specs/bedrock.spec.ts @@ -2,6 +2,7 @@ import { ThinkingDisplay } from '../src/schemas'; import { BEDROCK_OUTPUT_128K_BETA, supportsAdaptiveThinking, + omitsSamplingParameters, omitsThinkingByDefault, resolveThinkingDisplay, bedrockOutputParser, @@ -222,7 +223,7 @@ describe('omitsThinkingByDefault', () => { expect(omitsThinkingByDefault('us.anthropic.claude-opus-4-7')).toBe(true); }); - test('returns true for claude-opus-4-8 (future Opus 4.x)', () => { + test('returns true for claude-opus-4-8', () => { expect(omitsThinkingByDefault('claude-opus-4-8')).toBe(true); }); @@ -238,6 +239,11 @@ describe('omitsThinkingByDefault', () => { expect(omitsThinkingByDefault('claude-opus-4-6')).toBe(false); }); + test('returns false for base Opus 4 snapshot IDs', () => { + expect(omitsThinkingByDefault('claude-opus-4-20250514')).toBe(false); + expect(omitsThinkingByDefault('anthropic.claude-opus-4-20250514-v1:0')).toBe(false); + }); + test('returns false for claude-opus-4-5', () => { expect(omitsThinkingByDefault('claude-opus-4-5')).toBe(false); }); @@ -264,6 +270,36 @@ describe('omitsThinkingByDefault', () => { }); }); +describe('omitsSamplingParameters', () => { + test('returns true for Opus 4.7+ models', () => { + const models = [ + 'claude-opus-4-7', + 'claude-opus-4-8', + 'anthropic.claude-opus-4-8', + 'us.anthropic.claude-opus-4-8', + 'claude-opus-5', + ]; + + models.forEach((model) => { + expect(omitsSamplingParameters(model)).toBe(true); + }); + }); + + test('returns false for older Opus and non-Opus models', () => { + const models = [ + 'claude-opus-4-20250514', + 'anthropic.claude-opus-4-20250514-v1:0', + 'claude-opus-4-1-20250805', + 'claude-opus-4-6', + 'claude-sonnet-4-7', + ]; + + models.forEach((model) => { + expect(omitsSamplingParameters(model)).toBe(false); + }); + }); +}); + describe('resolveThinkingDisplay', () => { test('returns "summarized" for Opus 4.7 when explicit is auto/null/undefined', () => { expect(resolveThinkingDisplay('claude-opus-4-7', ThinkingDisplay.auto)).toBe('summarized'); @@ -541,6 +577,47 @@ describe('bedrockInputParser', () => { expect(additionalFields.effort).toBeUndefined(); }); + test('should strip sampling parameters for Opus 4.8 Bedrock models', () => { + const input = { + model: 'anthropic.claude-opus-4-8', + temperature: 0.7, + topP: 0.9, + topK: 40, + top_p: 0.8, + additionalModelRequestFields: { + custom_flag: true, + temperature: 0.5, + topP: 0.95, + top_k: 20, + }, + }; + const result = bedrockInputParser.parse(input) as Record; + const additionalFields = result.additionalModelRequestFields as Record; + expect(result.temperature).toBeUndefined(); + expect(result.topP).toBeUndefined(); + expect(additionalFields.temperature).toBeUndefined(); + expect(additionalFields.topP).toBeUndefined(); + expect(additionalFields.top_p).toBeUndefined(); + expect(additionalFields.top_k).toBeUndefined(); + expect(additionalFields.custom_flag).toBe(true); + expect(additionalFields.thinking).toEqual({ type: 'adaptive', display: 'summarized' }); + expect(additionalFields.anthropic_beta).toEqual(BEDROCK_CLAUDE_4_BETAS); + }); + + test('should preserve sampling parameters for Opus 4.6 Bedrock models', () => { + const input = { + model: 'anthropic.claude-opus-4-6-v1', + temperature: 0.7, + topP: 0.9, + topK: 40, + }; + const result = bedrockInputParser.parse(input) as Record; + const additionalFields = result.additionalModelRequestFields as Record; + expect(result.temperature).toBe(0.7); + expect(result.topP).toBe(0.9); + expect(additionalFields.top_k).toBe(40); + }); + test('should set thinking.display to "summarized" so Opus 4.7 returns reasoning blocks', () => { const input = { model: 'anthropic.claude-opus-4-7', diff --git a/packages/data-provider/src/bedrock.ts b/packages/data-provider/src/bedrock.ts index 96a4998079..a52da9a26f 100644 --- a/packages/data-provider/src/bedrock.ts +++ b/packages/data-provider/src/bedrock.ts @@ -68,14 +68,14 @@ type AnthropicInput = BedrockConverseInput & { /** Extracts opus major/minor version from both naming formats */ function parseOpusVersion(model: string): { major: number; minor: number } | null { - const nameFirst = model.match(/claude-opus[-.]?(\d+)(?:[-.](\d+))?/); + const nameFirst = model.match(/claude-opus[-.]?(\d+)(?:[-.](\d{1,2})(?!\d))?/); if (nameFirst) { return { major: parseInt(nameFirst[1], 10), minor: nameFirst[2] != null ? parseInt(nameFirst[2], 10) : 0, }; } - const numFirst = model.match(/claude-(\d+)(?:[-.](\d+))?-opus/); + const numFirst = model.match(/claude-(\d+)(?:[-.](\d{1,2})(?!\d))?-opus/); if (numFirst) { return { major: parseInt(numFirst[1], 10), @@ -136,6 +136,14 @@ export function omitsThinkingByDefault(model: string): boolean { return false; } +export function omitsSamplingParameters(model: string): boolean { + const opus = parseOpusVersion(model); + if (opus && (opus.major > 4 || (opus.major === 4 && opus.minor >= 7))) { + return true; + } + return false; +} + /** Checks if a model has a 1M context window (Sonnet 4.6+, Opus 4.6+, Opus 5+) */ export function supportsContext1m(model: string): boolean { const sonnet = parseSonnetVersion(model); @@ -177,11 +185,12 @@ function getBedrockAnthropicBetaHeaders(model: string): string[] { } function mergeBedrockAnthropicBetaHeaders(existing: unknown, generated: string[]): string[] { - const existingValues: unknown[] = Array.isArray(existing) - ? existing - : typeof existing === 'string' - ? [existing] - : []; + let existingValues: unknown[] = []; + if (Array.isArray(existing)) { + existingValues = existing; + } else if (typeof existing === 'string') { + existingValues = [existing]; + } const betaHeaders = new Set(); @@ -308,6 +317,8 @@ export const bedrockInputParser = s.tConversationSchema const additionalFields: Record = {}; const typedData = data as Record; + const shouldOmitSamplingParameters = + typeof typedData.model === 'string' && omitsSamplingParameters(typedData.model); Object.entries(typedData).forEach(([key, value]) => { if (!knownKeys.includes(key)) { @@ -436,6 +447,24 @@ export const bedrockInputParser = s.tConversationSchema delete amrf.reasoning_config; delete amrf.reasoning_effort; } + + if (shouldOmitSamplingParameters) { + delete amrf.temperature; + delete amrf.topP; + delete amrf.top_p; + delete amrf.topK; + delete amrf.top_k; + } + } + + if (shouldOmitSamplingParameters) { + delete typedData.temperature; + delete typedData.topP; + delete additionalFields.temperature; + delete additionalFields.topP; + delete additionalFields.top_p; + delete additionalFields.topK; + delete additionalFields.top_k; } /** Default promptCache for claude and nova models, if not defined */ diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index fae3bcd83b..b0d2217968 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1542,6 +1542,7 @@ const sharedOpenAIModels = [ ]; const sharedAnthropicModels = [ + 'claude-opus-4-8', 'claude-opus-4-7', 'claude-sonnet-4-6', 'claude-opus-4-6', @@ -1565,6 +1566,7 @@ const sharedAnthropicModels = [ ]; export const bedrockModels = [ + 'anthropic.claude-opus-4-8', 'anthropic.claude-opus-4-7', 'anthropic.claude-sonnet-4-6', 'anthropic.claude-opus-4-6-v1', diff --git a/packages/data-schemas/src/methods/tx.spec.ts b/packages/data-schemas/src/methods/tx.spec.ts index 9ca5e2a710..95882c9f80 100644 --- a/packages/data-schemas/src/methods/tx.spec.ts +++ b/packages/data-schemas/src/methods/tx.spec.ts @@ -2337,6 +2337,46 @@ describe('Claude Model Tests', () => { cacheTokenValues['claude-opus-4-7'].read, ); }); + + it('should return correct prompt and completion rates for Claude Opus 4.8', () => { + expect(getMultiplier({ model: 'claude-opus-4-8', tokenType: 'prompt' })).toBe( + tokenValues['claude-opus-4-8'].prompt, + ); + expect(getMultiplier({ model: 'claude-opus-4-8', tokenType: 'completion' })).toBe( + tokenValues['claude-opus-4-8'].completion, + ); + }); + + it('should handle Claude Opus 4.8 model name variations', () => { + const modelVariations = [ + 'claude-opus-4-8', + 'claude-opus-4-8-20260528', + 'claude-opus-4-8-latest', + 'anthropic/claude-opus-4-8', + 'claude-opus-4-8/anthropic', + 'claude-opus-4-8-preview', + ]; + + modelVariations.forEach((model) => { + const valueKey = getValueKey(model); + expect(valueKey).toBe('claude-opus-4-8'); + expect(getMultiplier({ model, tokenType: 'prompt' })).toBe( + tokenValues['claude-opus-4-8'].prompt, + ); + expect(getMultiplier({ model, tokenType: 'completion' })).toBe( + tokenValues['claude-opus-4-8'].completion, + ); + }); + }); + + it('should return correct cache rates for Claude Opus 4.8', () => { + expect(getCacheMultiplier({ model: 'claude-opus-4-8', cacheType: 'write' })).toBe( + cacheTokenValues['claude-opus-4-8'].write, + ); + expect(getCacheMultiplier({ model: 'claude-opus-4-8', cacheType: 'read' })).toBe( + cacheTokenValues['claude-opus-4-8'].read, + ); + }); }); describe('Premium Token Pricing', () => { @@ -2357,7 +2397,12 @@ describe('Premium Token Pricing', () => { }); it('should not apply premium pricing to Claude 1M GA models', () => { - const claudeModels = ['claude-opus-4-6', 'claude-opus-4-7', 'claude-sonnet-4-6']; + const claudeModels = [ + 'claude-opus-4-6', + 'claude-opus-4-7', + 'claude-opus-4-8', + 'claude-sonnet-4-6', + ]; claudeModels.forEach((model) => { expect(premiumTokenValues[model]).toBeUndefined(); expect(getPremiumRate(model, 'prompt', wellAboveThreshold)).toBeNull(); diff --git a/packages/data-schemas/src/methods/tx.ts b/packages/data-schemas/src/methods/tx.ts index db7d539230..fdd2ace8c3 100644 --- a/packages/data-schemas/src/methods/tx.ts +++ b/packages/data-schemas/src/methods/tx.ts @@ -158,6 +158,7 @@ export const tokenValues: Record 'claude-opus-4-5': { prompt: 5, completion: 25 }, 'claude-opus-4-6': { prompt: 5, completion: 25 }, 'claude-opus-4-7': { prompt: 5, completion: 25 }, + 'claude-opus-4-8': { prompt: 5, completion: 25 }, 'claude-sonnet-4': { prompt: 3, completion: 15 }, 'claude-sonnet-4-5': { prompt: 3, completion: 15 }, 'claude-sonnet-4-6': { prompt: 3, completion: 15 }, @@ -294,6 +295,7 @@ export const cacheTokenValues: Record = 'claude-opus-4-5': { write: 6.25, read: 0.5 }, 'claude-opus-4-6': { write: 6.25, read: 0.5 }, 'claude-opus-4-7': { write: 6.25, read: 0.5 }, + 'claude-opus-4-8': { write: 6.25, read: 0.5 }, 'gpt-4o': { write: 2.5, read: 1.25 }, 'gpt-4o-mini': { write: 0.15, read: 0.075 }, 'gpt-4.1': { write: 2, read: 0.5 },