From 2080a0e597a604e098daaa2b7fe622f0131c04af Mon Sep 17 00:00:00 2001
From: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com>
Date: Tue, 5 Aug 2025 15:45:50 -0700
Subject: [PATCH 01/10] Updating the model of listAvailableModels (#2064)
---
.../src/client/token/bearer-token-service.json | 4 ++++
.../src/client/token/codewhispererbearertokenclient.d.ts | 5 +++++
.../src/shared/codeWhispererService.ts | 7 +++++++
3 files changed, 16 insertions(+)
diff --git a/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json b/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json
index 2120382d25..957627d234 100644
--- a/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json
+++ b/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json
@@ -3573,6 +3573,10 @@
"shape": "Models",
"documentation": "
List of available models
"
},
+ "defaultModel": {
+ "shape": "Model",
+ "documentation": "Default model set by the client
"
+ },
"nextToken": {
"shape": "Base64EncodedPaginationToken",
"documentation": "Token for retrieving the next page of results
"
diff --git a/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts b/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts
index 2189944d4f..ac0739bc05 100644
--- a/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts
+++ b/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts
@@ -3,6 +3,7 @@
* THIS FILE IS AUTOGENERATED BY 'generateServiceClient.ts'.
* DO NOT EDIT BY HAND.
*/
+
import {Request} from 'aws-sdk/lib/request';
import {Response} from 'aws-sdk/lib/response';
import {AWSError} from 'aws-sdk/lib/error';
@@ -1118,6 +1119,10 @@ declare namespace CodeWhispererBearerTokenClient {
* List of available models
*/
models: Models;
+ /**
+ * Default model set by the client
+ */
+ defaultModel?: Model;
/**
* Token for retrieving the next page of results
*/
diff --git a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts
index b7ef57ee66..d1db107014 100644
--- a/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts
+++ b/server/aws-lsp-codewhisperer/src/shared/codeWhispererService.ts
@@ -459,6 +459,13 @@ export class CodeWhispererServiceToken extends CodeWhispererServiceBase {
return this.client.listAvailableProfiles(request).promise()
}
+ /**
+ * @description Get list of available models
+ */
+ async listAvailableModels(request: CodeWhispererTokenClient.ListAvailableModelsRequest) {
+ return this.client.listAvailableModels(request).promise()
+ }
+
/**
* @description send telemetry event to code whisperer data warehouse
*/
From c3751f251c4a4d8395c120283105a97458e7b2d7 Mon Sep 17 00:00:00 2001
From: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com>
Date: Tue, 5 Aug 2025 18:58:42 -0700
Subject: [PATCH 02/10] fix(amazonq): fix flickering issue for model selection
dropdown and agenticCoding toggle (#2065)
* fix(amazonq): fix flickering issue for modelId and agenticCoding
* fix(amazonq): Fixing flaky tests
---
chat-client/src/client/mynahUi.test.ts | 4 +++-
chat-client/src/client/mynahUi.ts | 23 +++++++++++++++++++----
2 files changed, 22 insertions(+), 5 deletions(-)
diff --git a/chat-client/src/client/mynahUi.test.ts b/chat-client/src/client/mynahUi.test.ts
index 31e64f0e76..727805ebbc 100644
--- a/chat-client/src/client/mynahUi.test.ts
+++ b/chat-client/src/client/mynahUi.test.ts
@@ -260,7 +260,8 @@ describe('MynahUI', () => {
sinon.assert.calledThrice(updateStoreSpy)
})
- it('should create a new tab if current tab is loading', () => {
+ it('should create a new tab if current tab is loading', function (done) {
+ this.timeout(8000)
// clear create tab stub since set up process calls it twice
createTabStub.resetHistory()
getAllTabsStub.returns({ 'tab-1': { store: { loadingChat: true } } })
@@ -274,6 +275,7 @@ describe('MynahUI', () => {
sinon.assert.calledOnceWithExactly(createTabStub, false)
sinon.assert.calledThrice(updateStoreSpy)
+ done()
})
it('should not create a new tab if one exists already', () => {
diff --git a/chat-client/src/client/mynahUi.ts b/chat-client/src/client/mynahUi.ts
index 3b42249331..525a9382e1 100644
--- a/chat-client/src/client/mynahUi.ts
+++ b/chat-client/src/client/mynahUi.ts
@@ -151,11 +151,20 @@ export const handlePromptInputChange = (mynahUi: MynahUI, tabId: string, options
}
}
+ const updatedPromptInputOptions = promptInputOptions?.map(option => {
+ option.value = optionsValues[option.id]
+ return option
+ })
+
mynahUi.updateStore(tabId, {
- promptInputOptions: promptInputOptions?.map(option => {
- option.value = optionsValues[option.id]
- return option
- }),
+ promptInputOptions: updatedPromptInputOptions,
+ })
+
+ // Store the updated values in tab defaults for new tabs
+ mynahUi.updateTabDefaults({
+ store: {
+ promptInputOptions: updatedPromptInputOptions,
+ },
})
}
@@ -414,6 +423,12 @@ export const createMynahUi = (
}
const tabStore = mynahUi.getTabData(tabId).getStore()
+ const storedPromptInputOptions = mynahUi.getTabDefaults().store?.promptInputOptions
+
+ // Retrieve stored model selection and pair programming mode from defaults
+ if (storedPromptInputOptions) {
+ defaultTabConfig.promptInputOptions = storedPromptInputOptions
+ }
// Tabs can be opened through different methods, including server-initiated 'openTab' requests.
// The 'openTab' request is specifically used for loading historical chat sessions with pre-existing messages.
From 837798d576bb57a8dab2d44438c17d5143779575 Mon Sep 17 00:00:00 2001
From: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com>
Date: Fri, 8 Aug 2025 09:49:49 -0700
Subject: [PATCH 03/10] feat(amazonq): Fetching models from backend and adding
cache implementation. (#2075)
* fix: removing and refactoring legacy code before implementing model selection
* feat(amazonq): adding cache implementation and fetching models from listAvailableModels api
* feat(amazonq): adding selected model in error case
* feat(amazonq): adding test cases
* fix: addressing comments
* fix: fixing test cases and adding modelName to models
* fix: minor edits
* fix: minor edits
* fix: minor modifications in logs
* fix: adding default model if api throws any errors
* fix: refactoring code
---
chat-client/src/client/chat.ts | 15 -
.../src/client/tabs/tabFactory.test.ts | 7 +-
chat-client/src/client/tabs/tabFactory.ts | 7 +-
.../src/client/texts/modelSelection.test.ts | 54 +--
.../src/client/texts/modelSelection.ts | 36 +-
.../sigv4/codewhisperersigv4client.d.ts | 2 +-
.../client/token/bearer-token-service.json | 11 +
.../token/codewhispererbearertokenclient.d.ts | 5 +
.../agenticChat/agenticChatController.test.ts | 316 ++++++++++++------
.../agenticChat/agenticChatController.ts | 150 ++++++---
.../constants/modelSelection.test.ts | 67 +---
.../agenticChat/constants/modelSelection.ts | 16 +-
.../agenticChat/tools/chatDb/chatDb.ts | 34 +-
.../agenticChat/tools/chatDb/util.ts | 12 +
.../utils/agenticChatControllerHelper.ts | 16 -
.../src/shared/constants.ts | 2 +
16 files changed, 412 insertions(+), 338 deletions(-)
delete mode 100644 server/aws-lsp-codewhisperer/src/language-server/agenticChat/utils/agenticChatControllerHelper.ts
diff --git a/chat-client/src/client/chat.ts b/chat-client/src/client/chat.ts
index d090a33f70..58519d96ef 100644
--- a/chat-client/src/client/chat.ts
+++ b/chat-client/src/client/chat.ts
@@ -116,7 +116,6 @@ import { InboundChatApi, createMynahUi } from './mynahUi'
import { TabFactory } from './tabs/tabFactory'
import { ChatClientAdapter } from '../contracts/chatClientAdapter'
import { toMynahContextCommand, toMynahIcon } from './utils'
-import { modelSelectionForRegion } from './texts/modelSelection'
const getDefaultTabConfig = (agenticMode?: boolean) => {
return {
@@ -264,20 +263,6 @@ export const createChat = (
return option
}),
})
- } else if (message.params.region) {
- // TODO: This can be removed after all clients support aws/chat/listAvailableModels
- // get all tabs and update region
- const allExistingTabs: MynahUITabStoreModel = mynahUi.getAllTabs()
- for (const tabId in allExistingTabs) {
- const options = mynahUi.getTabData(tabId).getStore()?.promptInputOptions
- mynahUi.updateStore(tabId, {
- promptInputOptions: options?.map(option =>
- option.id === 'model-selection'
- ? modelSelectionForRegion[message.params.region]
- : option
- ),
- })
- }
} else {
tabFactory.setInfoMessages((message.params as ChatOptionsUpdateParams).chatNotifications)
}
diff --git a/chat-client/src/client/tabs/tabFactory.test.ts b/chat-client/src/client/tabs/tabFactory.test.ts
index c398c709eb..815e81a22e 100644
--- a/chat-client/src/client/tabs/tabFactory.test.ts
+++ b/chat-client/src/client/tabs/tabFactory.test.ts
@@ -2,7 +2,7 @@ import { ChatHistory } from '../features/history'
import { TabFactory } from './tabFactory'
import * as assert from 'assert'
import { pairProgrammingPromptInput } from '../texts/pairProgramming'
-import { modelSelectionForRegion } from '../texts/modelSelection'
+import { modelSelection } from '../texts/modelSelection'
describe('tabFactory', () => {
describe('getDefaultTabData', () => {
@@ -92,10 +92,7 @@ describe('tabFactory', () => {
const result = tabFactory.createTab(false)
- assert.deepStrictEqual(result.promptInputOptions, [
- pairProgrammingPromptInput,
- modelSelectionForRegion['us-east-1'],
- ])
+ assert.deepStrictEqual(result.promptInputOptions, [pairProgrammingPromptInput, modelSelection])
})
it('should not include model selection when only agentic mode is enabled', () => {
diff --git a/chat-client/src/client/tabs/tabFactory.ts b/chat-client/src/client/tabs/tabFactory.ts
index af1af21e7d..c0ab3ebac9 100644
--- a/chat-client/src/client/tabs/tabFactory.ts
+++ b/chat-client/src/client/tabs/tabFactory.ts
@@ -11,7 +11,7 @@ import { disclaimerCard } from '../texts/disclaimer'
import { ChatMessage } from '@aws/language-server-runtimes-types'
import { ChatHistory } from '../features/history'
import { pairProgrammingPromptInput, programmerModeCard } from '../texts/pairProgramming'
-import { modelSelectionForRegion } from '../texts/modelSelection'
+import { modelSelection } from '../texts/modelSelection'
export type DefaultTabData = MynahUIDataModel
@@ -52,10 +52,7 @@ export class TabFactory {
...this.getDefaultTabData(),
...(disclaimerCardActive ? { promptInputStickyCard: disclaimerCard } : {}),
promptInputOptions: this.agenticMode
- ? [
- pairProgrammingPromptInput,
- ...(this.modelSelectionEnabled ? [modelSelectionForRegion['us-east-1']] : []),
- ]
+ ? [pairProgrammingPromptInput, ...(this.modelSelectionEnabled ? [modelSelection] : [])]
: [],
cancelButtonWhenLoading: this.agenticMode, // supported for agentic chat only
}
diff --git a/chat-client/src/client/texts/modelSelection.test.ts b/chat-client/src/client/texts/modelSelection.test.ts
index 762eb6f818..abd010436e 100644
--- a/chat-client/src/client/texts/modelSelection.test.ts
+++ b/chat-client/src/client/texts/modelSelection.test.ts
@@ -1,63 +1,11 @@
import * as assert from 'assert'
-import {
- BedrockModel,
- modelSelectionForRegion,
- getModelSelectionChatItem,
- modelUnavailableBanner,
- modelThrottledBanner,
-} from './modelSelection'
+import { getModelSelectionChatItem, modelUnavailableBanner, modelThrottledBanner } from './modelSelection'
import { ChatItemType } from '@aws/mynah-ui'
/**
* Tests for modelSelection functionality
- *
- * Note: Some tests are for deprecated code (marked with 'legacy') that is maintained
- * for backward compatibility with older clients. These should be removed once
- * all clients have been updated to use the new API (aws/chat/listAvailableModels).
*/
describe('modelSelection', () => {
- describe('BedrockModel enum (legacy)', () => {
- it('should have the correct model IDs', () => {
- assert.strictEqual(BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0, 'CLAUDE_3_7_SONNET_20250219_V1_0')
- assert.strictEqual(BedrockModel.CLAUDE_SONNET_4_20250514_V1_0, 'CLAUDE_SONNET_4_20250514_V1_0')
- })
- })
-
- describe('modelSelectionForRegion (legacy)', () => {
- it('should provide all models for us-east-1 region', () => {
- const usEast1ModelSelection = modelSelectionForRegion['us-east-1']
- assert.ok(usEast1ModelSelection, 'usEast1ModelSelection should exist')
- assert.ok(usEast1ModelSelection.type === 'select', 'usEast1ModelSelection should be type select')
- assert.ok(Array.isArray(usEast1ModelSelection.options), 'options should be an array')
- assert.strictEqual(usEast1ModelSelection.options.length, 2, 'should have 2 options')
-
- const modelIds = usEast1ModelSelection.options.map(option => option.value)
- assert.ok(modelIds.includes(BedrockModel.CLAUDE_SONNET_4_20250514_V1_0), 'should include Claude Sonnet 4')
- assert.ok(
- modelIds.includes(BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0),
- 'should include Claude Sonnet 3.7'
- )
- })
-
- it('should provide limited models for eu-central-1 region', () => {
- const euCentral1ModelSelection = modelSelectionForRegion['eu-central-1']
- assert.ok(euCentral1ModelSelection, 'euCentral1ModelSelection should exist')
- assert.ok(euCentral1ModelSelection.type === 'select', 'euCentral1ModelSelection should be type select')
- assert.ok(Array.isArray(euCentral1ModelSelection.options), 'options should be an array')
- assert.strictEqual(euCentral1ModelSelection.options.length, 1, 'should have 1 option')
-
- const modelIds = euCentral1ModelSelection.options.map(option => option.value)
- assert.ok(
- !modelIds.includes(BedrockModel.CLAUDE_SONNET_4_20250514_V1_0),
- 'should not include Claude Sonnet 4'
- )
- assert.ok(
- modelIds.includes(BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0),
- 'should include Claude Sonnet 3.7'
- )
- })
- })
-
describe('getModelSelectionChatItem', () => {
it('should return a chat item with the correct model name', () => {
const modelName = 'Claude Sonnet 4'
diff --git a/chat-client/src/client/texts/modelSelection.ts b/chat-client/src/client/texts/modelSelection.ts
index 28fe969bc9..4dcaad95ba 100644
--- a/chat-client/src/client/texts/modelSelection.ts
+++ b/chat-client/src/client/texts/modelSelection.ts
@@ -1,48 +1,14 @@
import { ChatItem, ChatItemFormItem, ChatItemType } from '@aws/mynah-ui'
-/**
- * @deprecated use aws/chat/listAvailableModels server request instead
- */
-export enum BedrockModel {
- CLAUDE_SONNET_4_20250514_V1_0 = 'CLAUDE_SONNET_4_20250514_V1_0',
- CLAUDE_3_7_SONNET_20250219_V1_0 = 'CLAUDE_3_7_SONNET_20250219_V1_0',
-}
-
-type ModelDetails = {
- label: string
-}
-
-const modelRecord: Record = {
- [BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0]: { label: 'Claude Sonnet 3.7' },
- [BedrockModel.CLAUDE_SONNET_4_20250514_V1_0]: { label: 'Claude Sonnet 4' },
-}
-
-const modelOptions = Object.entries(modelRecord).map(([value, { label }]) => ({
- value,
- label,
-}))
-
-const modelSelection: ChatItemFormItem = {
+export const modelSelection: ChatItemFormItem = {
type: 'select',
id: 'model-selection',
- options: modelOptions,
mandatory: true,
hideMandatoryIcon: true,
border: false,
autoWidth: true,
}
-/**
- * @deprecated use aws/chat/listAvailableModels server request instead
- */
-export const modelSelectionForRegion: Record = {
- 'us-east-1': modelSelection,
- 'eu-central-1': {
- ...modelSelection,
- options: modelOptions.filter(option => option.value !== BedrockModel.CLAUDE_SONNET_4_20250514_V1_0),
- },
-}
-
export const getModelSelectionChatItem = (modelName: string): ChatItem => ({
type: ChatItemType.DIRECTIVE,
contentHorizontalAlignment: 'center',
diff --git a/server/aws-lsp-codewhisperer/src/client/sigv4/codewhisperersigv4client.d.ts b/server/aws-lsp-codewhisperer/src/client/sigv4/codewhisperersigv4client.d.ts
index 308ead42ee..6cb67d3ef8 100644
--- a/server/aws-lsp-codewhisperer/src/client/sigv4/codewhisperersigv4client.d.ts
+++ b/server/aws-lsp-codewhisperer/src/client/sigv4/codewhisperersigv4client.d.ts
@@ -3,7 +3,7 @@
* THIS FILE IS AUTOGENERATED BY 'generateServiceClient.ts'.
* DO NOT EDIT BY HAND.
*/
-
+
import {Request} from 'aws-sdk/lib/request';
import {Response} from 'aws-sdk/lib/response';
import {AWSError} from 'aws-sdk/lib/error';
diff --git a/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json b/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json
index 957627d234..88e6e8d177 100644
--- a/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json
+++ b/server/aws-lsp-codewhisperer/src/client/token/bearer-token-service.json
@@ -3901,6 +3901,10 @@
"shape": "ModelId",
"documentation": "Unique identifier for the model
"
},
+ "modelName": {
+ "shape": "ModelName",
+ "documentation": "User-facing display name
"
+ },
"description": {
"shape": "Description",
"documentation": "Description of the model
"
@@ -3918,6 +3922,13 @@
"min": 1,
"pattern": "[a-zA-Z0-9_:.-]+"
},
+ "ModelName": {
+ "type": "string",
+ "documentation": "Identifier for the model Name
",
+ "max": 1024,
+ "min": 1,
+ "pattern": "[a-zA-Z0-9-_.]+"
+ },
"ModelMetadata": {
"type": "structure",
"members": {
diff --git a/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts b/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts
index ac0739bc05..86798cce75 100644
--- a/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts
+++ b/server/aws-lsp-codewhisperer/src/client/token/codewhispererbearertokenclient.d.ts
@@ -1228,6 +1228,10 @@ declare namespace CodeWhispererBearerTokenClient {
* Unique identifier for the model
*/
modelId: ModelId;
+ /**
+ * User-facing display name
+ */
+ modelName?: ModelName;
/**
* Description of the model
*/
@@ -1238,6 +1242,7 @@ declare namespace CodeWhispererBearerTokenClient {
modelMetadata?: ModelMetadata;
}
export type ModelId = string;
+ export type ModelName = string;
export interface ModelMetadata {
/**
* Maximum number of input tokens the model can process
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts
index 3ee23089b2..ede7ae1fad 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts
@@ -32,6 +32,7 @@ import {
InlineChatResult,
CancellationTokenSource,
ContextCommand,
+ ChatUpdateParams,
} from '@aws/language-server-runtimes/server-interface'
import { TestFeatures } from '@aws/language-server-runtimes/testing'
import * as assert from 'assert'
@@ -56,7 +57,7 @@ import { LocalProjectContextController } from '../../shared/localProjectContextC
import { CancellationError } from '@aws/lsp-core'
import { ToolApprovalException } from './tools/toolShared'
import * as constants from './constants/constants'
-import { GENERATE_ASSISTANT_RESPONSE_INPUT_LIMIT, GENERIC_ERROR_MS } from './constants/constants'
+import { DEFAULT_MODEL_ID, GENERATE_ASSISTANT_RESPONSE_INPUT_LIMIT, GENERIC_ERROR_MS } from './constants/constants'
import { MISSING_BEARER_TOKEN_ERROR } from '../../shared/constants'
import {
AmazonQError,
@@ -367,17 +368,6 @@ describe('AgenticChatController', () => {
sinon.assert.calledWithExactly(activeTabSpy.set, mockTabId)
})
- it('onTabAdd updates model ID in chat options and session', () => {
- const modelId = 'test-model-id'
- sinon.stub(ChatDatabase.prototype, 'getModelId').returns(modelId)
- chatController.onTabAdd({ tabId: mockTabId })
-
- sinon.assert.calledWithExactly(testFeatures.chat.chatOptionsUpdate, { modelId, tabId: mockTabId })
-
- const session = chatSessionManagementService.getSession(mockTabId).data
- assert.strictEqual(session!.modelId, modelId)
- })
-
it('onTabChange sets active tab id in telemetryController and emits metrics', () => {
chatController.onTabChange({ tabId: mockTabId })
@@ -2993,133 +2983,251 @@ ${' '.repeat(8)}}
})
describe('onListAvailableModels', () => {
- let tokenServiceManagerStub: sinon.SinonStub
+ let isCachedModelsValidStub: sinon.SinonStub
+ let getCachedModelsStub: sinon.SinonStub
+ let setCachedModelsStub: sinon.SinonStub
+ let getConnectionTypeStub: sinon.SinonStub
+ let getActiveProfileArnStub: sinon.SinonStub
+ let getCodewhispererServiceStub: sinon.SinonStub
+ let listAvailableModelsStub: sinon.SinonStub
beforeEach(() => {
- // Create a session with a model ID
+ // Create a session
chatController.onTabAdd({ tabId: mockTabId })
- const session = chatSessionManagementService.getSession(mockTabId).data!
- session.modelId = 'CLAUDE_3_7_SONNET_20250219_V1_0'
- // Stub the getRegion method
- tokenServiceManagerStub = sinon.stub(AmazonQTokenServiceManager.prototype, 'getRegion')
+ // Stub ChatDatabase methods
+ isCachedModelsValidStub = sinon.stub(ChatDatabase.prototype, 'isCachedModelsValid')
+ getCachedModelsStub = sinon.stub(ChatDatabase.prototype, 'getCachedModels')
+ setCachedModelsStub = sinon.stub(ChatDatabase.prototype, 'setCachedModels')
+
+ // Stub AmazonQTokenServiceManager methods
+ getConnectionTypeStub = sinon.stub(AmazonQTokenServiceManager.prototype, 'getConnectionType')
+ getActiveProfileArnStub = sinon.stub(AmazonQTokenServiceManager.prototype, 'getActiveProfileArn')
+ getCodewhispererServiceStub = sinon.stub(AmazonQTokenServiceManager.prototype, 'getCodewhispererService')
+
+ // Mock listAvailableModels method
+ listAvailableModelsStub = sinon.stub()
+ getCodewhispererServiceStub.returns({
+ listAvailableModels: listAvailableModelsStub,
+ })
})
afterEach(() => {
- tokenServiceManagerStub.restore()
- })
+ isCachedModelsValidStub.restore()
+ getCachedModelsStub.restore()
+ setCachedModelsStub.restore()
+ getConnectionTypeStub.restore()
+ getActiveProfileArnStub.restore()
+ getCodewhispererServiceStub.restore()
+ })
+
+ describe('ListAvailableModels Cache scenarios', () => {
+ it('should return cached models when cache is valid', async () => {
+ // Setup valid cache
+ isCachedModelsValidStub.returns(true)
+ const cachedData = {
+ models: [
+ { id: 'model1', name: 'Model 1' },
+ { id: 'model2', name: 'Model 2' },
+ ],
+ defaultModelId: 'model1',
+ timestamp: Date.now(),
+ }
+ getCachedModelsStub.returns(cachedData)
- it('should return all available models for us-east-1 region', async () => {
- // Set up the region to be us-east-1
- tokenServiceManagerStub.returns('us-east-1')
+ const session = chatSessionManagementService.getSession(mockTabId).data!
+ session.modelId = 'model1'
- // Call the method
- const params = { tabId: mockTabId }
- const result = await chatController.onListAvailableModels(params)
+ const result = await chatController.onListAvailableModels({ tabId: mockTabId })
- // Verify the result
- assert.strictEqual(result.tabId, mockTabId)
- assert.strictEqual(result.models.length, 2)
- assert.strictEqual(result.selectedModelId, 'CLAUDE_SONNET_4_20250514_V1_0')
+ // Verify cached data is used
+ assert.strictEqual(result.tabId, mockTabId)
+ assert.deepStrictEqual(result.models, cachedData.models)
+ assert.strictEqual(result.selectedModelId, 'model1')
- // Check that the models include both Claude versions
- const modelIds = result.models.map(model => model.id)
- assert.ok(modelIds.includes('CLAUDE_SONNET_4_20250514_V1_0'))
- assert.ok(modelIds.includes('CLAUDE_3_7_SONNET_20250219_V1_0'))
- })
+ // Verify API was not called
+ sinon.assert.notCalled(listAvailableModelsStub)
+ sinon.assert.notCalled(setCachedModelsStub)
+ })
- it('should return limited models for eu-central-1 region', async () => {
- // Set up the region to be eu-central-1
- tokenServiceManagerStub.returns('eu-central-1')
+ it('should return cached models when cache is valid but has empty models array', async () => {
+ // Setup cache with empty models
+ isCachedModelsValidStub.returns(true)
+ const cachedData = {
+ models: [],
+ defaultModelId: undefined,
+ timestamp: Date.now(),
+ }
+ getCachedModelsStub.returns(cachedData)
- // Call the method
- const params = { tabId: mockTabId }
- const result = await chatController.onListAvailableModels(params)
+ // Should fall back to API call since models array is empty
+ getConnectionTypeStub.returns('builderId')
+ getActiveProfileArnStub.returns('test-arn')
+ listAvailableModelsStub.resolves({
+ models: {
+ model1: { modelId: 'model1' },
+ model2: { modelId: 'model2' },
+ },
+ defaultModel: { modelId: 'model1' },
+ })
- // Verify the result
- assert.strictEqual(result.tabId, mockTabId)
- assert.strictEqual(result.models.length, 1)
- assert.strictEqual(result.selectedModelId, 'CLAUDE_3_7_SONNET_20250219_V1_0')
+ await chatController.onListAvailableModels({ tabId: mockTabId })
- // Check that the models only include Claude 3.7
- const modelIds = result.models.map(model => model.id)
- assert.ok(!modelIds.includes('CLAUDE_SONNET_4_20250514_V1_0'))
- assert.ok(modelIds.includes('CLAUDE_3_7_SONNET_20250219_V1_0'))
- })
+ // Verify API was called due to empty cached models
+ sinon.assert.calledOnce(listAvailableModelsStub)
+ sinon.assert.calledOnce(setCachedModelsStub)
+ })
- it('should return all models when region is unknown', async () => {
- // Set up the region to be unknown
- tokenServiceManagerStub.returns('unknown-region')
+ it('should return cached models when cache is valid but cachedData is null', async () => {
+ // Setup cache as valid but returns null
+ isCachedModelsValidStub.returns(true)
+ getCachedModelsStub.returns(null)
- // Call the method
- const params = { tabId: mockTabId }
- const result = await chatController.onListAvailableModels(params)
+ // Should fall back to API call
+ getConnectionTypeStub.returns('builderId')
+ getActiveProfileArnStub.returns('test-arn')
+ listAvailableModelsStub.resolves({
+ models: {
+ model1: { modelId: 'model1' },
+ },
+ defaultModel: { modelId: 'model1' },
+ })
- // Verify the result
- assert.strictEqual(result.tabId, mockTabId)
- assert.strictEqual(result.models.length, 2)
- assert.strictEqual(result.selectedModelId, 'CLAUDE_3_7_SONNET_20250219_V1_0')
+ await chatController.onListAvailableModels({ tabId: mockTabId })
+
+ // Verify API was called
+ sinon.assert.calledOnce(listAvailableModelsStub)
+ })
})
- it('should return undefined for selectedModelId when no session data exists', async () => {
- // Set up the session to return no session (failure case)
- const getSessionStub = sinon.stub(chatSessionManagementService, 'getSession')
- getSessionStub.returns({
- data: undefined,
- success: false,
- error: 'error',
+ describe('ListAvailableModels API call scenarios', () => {
+ beforeEach(() => {
+ // Setup invalid cache to force API call
+ isCachedModelsValidStub.returns(false)
})
- // Call the method
- const params = { tabId: 'non-existent-tab' }
- const result = await chatController.onListAvailableModels(params)
+ it('should fetch models from API when cache is invalid', async () => {
+ getConnectionTypeStub.returns('builderId')
+ getActiveProfileArnStub.returns('test-profile-arn')
- // Verify the result
- assert.strictEqual(result.tabId, 'non-existent-tab')
- assert.strictEqual(result.models.length, 2)
- assert.strictEqual(result.selectedModelId, undefined)
+ const mockApiResponse = {
+ models: {
+ 'claude-3-sonnet': { modelId: 'claude-3-sonnet' },
+ 'claude-4-sonnet': { modelId: 'claude-4-sonnet' },
+ },
+ defaultModel: { modelId: 'claude-3-sonnet' },
+ }
+ listAvailableModelsStub.resolves(mockApiResponse)
- getSessionStub.restore()
- })
+ const result = await chatController.onListAvailableModels({ tabId: mockTabId })
+
+ // Verify API call was made with correct parameters
+ sinon.assert.calledOnceWithExactly(listAvailableModelsStub, {
+ origin: 'IDE',
+ profileArn: 'test-profile-arn',
+ })
+
+ // Verify result structure
+ assert.strictEqual(result.tabId, mockTabId)
+ assert.strictEqual(result.models.length, 2)
+ assert.deepStrictEqual(result.models, [
+ { id: 'claude-3-sonnet', name: 'claude-3-sonnet' },
+ { id: 'claude-4-sonnet', name: 'claude-4-sonnet' },
+ ])
+
+ // Verify cache was updated
+ sinon.assert.calledOnceWithExactly(setCachedModelsStub, result.models, 'claude-3-sonnet')
+ })
+
+ it('should fall back to hardcoded models when API call fails', async () => {
+ getConnectionTypeStub.returns('builderId')
+ listAvailableModelsStub.rejects(new Error('API Error'))
- it('should fallback to latest available model when saved model is not available in current region', async () => {
- // Set up the region to be eu-central-1 (which only has Claude 3.7)
- tokenServiceManagerStub.returns('eu-central-1')
+ const result = await chatController.onListAvailableModels({ tabId: mockTabId })
- // Mock database to return Claude Sonnet 4 (not available in eu-central-1)
- const getModelIdStub = sinon.stub(ChatDatabase.prototype, 'getModelId')
- getModelIdStub.returns('CLAUDE_SONNET_4_20250514_V1_0')
+ // Verify fallback to FALLBACK_MODEL_OPTIONS
+ assert.strictEqual(result.tabId, mockTabId)
+ assert.strictEqual(result.models.length, 2) // FALLBACK_MODEL_OPTIONS length
- // Call the method
- const params = { tabId: mockTabId }
- const result = await chatController.onListAvailableModels(params)
+ // Verify cache was not updated due to error
+ sinon.assert.notCalled(setCachedModelsStub)
+ })
- // Verify the result falls back to available model
- assert.strictEqual(result.tabId, mockTabId)
- assert.strictEqual(result.models.length, 1)
- assert.strictEqual(result.selectedModelId, 'CLAUDE_3_7_SONNET_20250219_V1_0')
+ it('should handle API response with no defaultModel', async () => {
+ getConnectionTypeStub.returns('builderId')
- getModelIdStub.restore()
+ const mockApiResponse = {
+ models: {
+ model1: { modelId: 'model1' },
+ },
+ defaultModel: undefined, // No default model
+ }
+ listAvailableModelsStub.resolves(mockApiResponse)
+
+ const result = await chatController.onListAvailableModels({ tabId: mockTabId })
+
+ // Verify cache was updated with undefined defaultModelId
+ sinon.assert.calledOnceWithExactly(setCachedModelsStub, result.models, undefined)
+ })
})
- it('should use saved model when it is available in current region', async () => {
- // Set up the region to be us-east-1 (which has both models)
- tokenServiceManagerStub.returns('us-east-1')
+ describe('Session and model selection scenarios', () => {
+ beforeEach(() => {
+ // Setup cache to avoid API calls in these tests
+ isCachedModelsValidStub.returns(true)
+ getCachedModelsStub.returns({
+ models: [
+ { id: 'model1', name: 'Model 1' },
+ { id: 'model2', name: 'Model 2' },
+ ],
+ defaultModelId: 'model1',
+ timestamp: Date.now(),
+ })
+ })
+
+ it('should return default model when session fails to load', async () => {
+ const getSessionStub = sinon.stub(chatSessionManagementService, 'getSession')
+ getSessionStub.returns({
+ data: undefined,
+ success: false,
+ error: 'Session not found',
+ })
+
+ const result = await chatController.onListAvailableModels({ tabId: 'invalid-tab' })
- // Mock database to return Claude 3.7 (available in us-east-1)
- const getModelIdStub = sinon.stub(ChatDatabase.prototype, 'getModelId')
- getModelIdStub.returns('CLAUDE_3_7_SONNET_20250219_V1_0')
+ assert.strictEqual(result.tabId, 'invalid-tab')
+ assert.strictEqual(result.selectedModelId, DEFAULT_MODEL_ID)
- // Call the method
- const params = { tabId: mockTabId }
- const result = await chatController.onListAvailableModels(params)
+ getSessionStub.restore()
+ })
+
+ it('should use defaultModelId from cache when session has no modelId', async () => {
+ const session = chatSessionManagementService.getSession(mockTabId).data!
+ session.modelId = undefined
- // Verify the result uses the saved model
- assert.strictEqual(result.tabId, mockTabId)
- assert.strictEqual(result.models.length, 2)
- assert.strictEqual(result.selectedModelId, 'CLAUDE_3_7_SONNET_20250219_V1_0')
+ const result = await chatController.onListAvailableModels({ tabId: mockTabId })
- getModelIdStub.restore()
+ assert.strictEqual(result.selectedModelId, 'model1') // defaultModelId from cache
+ // Verify session modelId is updated
+ assert.strictEqual(session.modelId, 'model1')
+ })
+
+ it('should fall back to default model when session has no modelId and no defaultModelId in cache', async () => {
+ getCachedModelsStub.returns({
+ models: [{ id: 'model1', name: 'Model 1' }],
+ defaultModelId: undefined, // No default model
+ timestamp: Date.now(),
+ })
+
+ const session = chatSessionManagementService.getSession(mockTabId).data!
+ session.modelId = undefined
+
+ const result = await chatController.onListAvailableModels({ tabId: mockTabId })
+
+ assert.strictEqual(result.selectedModelId, 'claude-4-sonnet') // MODEL_RECORD[DEFAULT_MODEL_ID].label
+ // Verify session modelId is updated
+ assert.strictEqual(session.modelId, 'claude-4-sonnet')
+ })
})
})
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
index 34d153c6e0..af2a154a70 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
@@ -24,7 +24,6 @@ import {
GREP_SEARCH,
FILE_SEARCH,
EXECUTE_BASH,
- CODE_REVIEW,
BUTTON_RUN_SHELL_COMMAND,
BUTTON_REJECT_SHELL_COMMAND,
BUTTON_REJECT_MCP_TOOL,
@@ -67,8 +66,6 @@ import {
ListAvailableModelsResult,
OpenFileDialogParams,
OpenFileDialogResult,
-} from '@aws/language-server-runtimes/protocol'
-import {
ApplyWorkspaceEditParams,
ErrorCodes,
FeedbackParams,
@@ -83,6 +80,7 @@ import {
TabBarActionParams,
CreatePromptParams,
FileClickParams,
+ Model,
} from '@aws/language-server-runtimes/protocol'
import {
CancellationToken,
@@ -222,14 +220,14 @@ import {
Message as DbMessage,
messageToStreamingMessage,
} from './tools/chatDb/util'
-import { MODEL_OPTIONS, MODEL_OPTIONS_FOR_REGION } from './constants/modelSelection'
+import { FALLBACK_MODEL_OPTIONS, MODEL_RECORD } from './constants/modelSelection'
import { DEFAULT_IMAGE_VERIFICATION_OPTIONS, verifyServerImage } from '../../shared/imageVerification'
import { sanitize } from '@aws/lsp-core/out/util/path'
-import { getLatestAvailableModel } from './utils/agenticChatControllerHelper'
import { ActiveUserTracker } from '../../shared/activeUserTracker'
import { UserContext } from '../../client/token/codewhispererbearertokenclient'
import { CodeWhispererServiceToken } from '../../shared/codeWhispererService'
import { DisplayFindings } from './tools/qCodeAnalysis/displayFindings'
+import { IDE } from '../../shared/constants'
type ChatHandlers = Omit<
LspHandlers,
@@ -680,25 +678,122 @@ export class AgenticChatController implements ChatHandlers {
return this.#mcpEventHandler.onMcpServerClick(params)
}
+ /**
+ * Fetches available models either from cache or API
+ * If cache is valid (less than 5 minutes old), returns cached models
+ * If cache is invalid or empty, makes an API call and stores results in cache
+ * If the API throws errors (e.g., throttling), falls back to default models
+ */
+ async #fetchModelsWithCache(): Promise<{ models: Model[]; defaultModelId?: string; errorFromAPI: boolean }> {
+ let models: Model[] = []
+ let defaultModelId: string | undefined
+ let errorFromAPI = false
+
+ // Check if cache is valid (less than 5 minutes old)
+ if (this.#chatHistoryDb.isCachedModelsValid()) {
+ const cachedData = this.#chatHistoryDb.getCachedModels()
+ if (cachedData && cachedData.models && cachedData.models.length > 0) {
+ this.#log('Using cached models, last updated at:', new Date(cachedData.timestamp).toISOString())
+ return {
+ models: cachedData.models,
+ defaultModelId: cachedData.defaultModelId,
+ errorFromAPI: false,
+ }
+ }
+ }
+
+ // If cache is invalid or empty, make an API call
+ this.#log('Cache miss or expired, fetching models from API')
+ try {
+ const client = AmazonQTokenServiceManager.getInstance().getCodewhispererService()
+ const responseResult = await client.listAvailableModels({
+ origin: IDE,
+ profileArn: AmazonQTokenServiceManager.getInstance().getConnectionType()
+ ? AmazonQTokenServiceManager.getInstance().getActiveProfileArn()
+ : undefined,
+ })
+
+ // Wait for the response to be completed before proceeding
+ this.#log('Model Response', responseResult.toString())
+ models = Object.values(responseResult.models).map(({ modelId, modelName }) => ({
+ id: modelId,
+ name: modelName ?? modelId,
+ }))
+ defaultModelId = responseResult.defaultModel?.modelId
+
+ // Cache the models with defaultModelId
+ this.#chatHistoryDb.setCachedModels(models, defaultModelId)
+ } catch (err) {
+ // In case of API throttling or other errors, fall back to hardcoded models
+ this.#log('Error fetching models from API, using fallback models:', fmtError(err))
+ errorFromAPI = true
+ models = FALLBACK_MODEL_OPTIONS
+ }
+
+ return {
+ models,
+ defaultModelId,
+ errorFromAPI,
+ }
+ }
+
+ /**
+ * This function handles the model selection process for the chat interface.
+ * It first attempts to retrieve models from cache or API, then determines the appropriate model to select
+ * based on the following priority:
+ * 1. When errors occur or session is invalid: Use the default model as a fallback
+ * 2. When user has previously selected a model: Use that model (or its mapped version if the model ID has changed)
+ * 3. When there's a default model from the API: Use the server-recommended default model
+ * 4. Last resort: Use the newest model defined in modelSelection constants
+ *
+ * This ensures users maintain consistent model selection across sessions while also handling
+ * API failures and model ID migrations gracefully.
+ */
async onListAvailableModels(params: ListAvailableModelsParams): Promise {
- const region = AmazonQTokenServiceManager.getInstance().getRegion()
- const models = region && MODEL_OPTIONS_FOR_REGION[region] ? MODEL_OPTIONS_FOR_REGION[region] : MODEL_OPTIONS
+ // Get models from cache or API
+ const { models, defaultModelId, errorFromAPI } = await this.#fetchModelsWithCache()
+
+ // Get the first fallback model option as default
+ const defaultModelOption = FALLBACK_MODEL_OPTIONS[1]
+ const DEFAULT_MODEL_ID = defaultModelOption?.id || Object.keys(MODEL_RECORD)[1]
const sessionResult = this.#chatSessionManagementService.getSession(params.tabId)
const { data: session, success } = sessionResult
- if (!success) {
+
+ // Handle error cases by returning default model
+ if (!success || errorFromAPI) {
return {
tabId: params.tabId,
models: models,
+ selectedModelId: DEFAULT_MODEL_ID,
+ }
+ }
+
+ // Determine which model ID to select based on priority.
+ let selectedModelId: string
+ let modelId = this.#chatHistoryDb.getModelId()
+
+ if (modelId) {
+ // Case 1: User has previously selected a model - use that model or its mapped version
+ // Check if modelId is a key in MODEL_RECORD
+ if (modelId in MODEL_RECORD) {
+ // If it's a valid model key, use its mapped label
+ selectedModelId = MODEL_RECORD[modelId as keyof typeof MODEL_RECORD]?.label || modelId
+ } else {
+ // Otherwise use as is (might be a direct model ID from the API)
+ selectedModelId = modelId
}
+ } else if (defaultModelId) {
+ // Case 2: No user selection, but API provided a default model - use server recommendation
+ selectedModelId = defaultModelId
+ } else {
+ // Case 3: Last resort - use default model's label
+ selectedModelId = MODEL_RECORD[DEFAULT_MODEL_ID as keyof typeof MODEL_RECORD]?.label || DEFAULT_MODEL_ID
}
- const savedModelId = this.#chatHistoryDb.getModelId()
- const selectedModelId =
- savedModelId && models.some(model => model.id === savedModelId)
- ? savedModelId
- : getLatestAvailableModel(region).id
+ // Store the selected model in the session
session.modelId = selectedModelId
+
return {
tabId: params.tabId,
models: models,
@@ -3490,25 +3585,6 @@ export class AgenticChatController implements ChatHandlers {
onSourceLinkClick() {}
- /**
- * @deprecated use aws/chat/listAvailableModels server request instead
- */
- #legacySetModelId(tabId: string, session: ChatSessionService) {
- // Since model selection is mandatory, the only time modelId is not set is when the chat history is empty.
- // In that case, we use the default modelId.
- let modelId = this.#chatHistoryDb.getModelId() ?? DEFAULT_MODEL_ID
-
- const region = AmazonQTokenServiceManager.getInstance().getRegion()
- if (region === 'eu-central-1') {
- // Only 3.7 Sonnet is available in eu-central-1 for now
- modelId = 'CLAUDE_3_7_SONNET_20250219_V1_0'
- // @ts-ignore
- this.#features.chat.chatOptionsUpdate({ region })
- }
- this.#features.chat.chatOptionsUpdate({ modelId: modelId, tabId: tabId })
- session.modelId = modelId
- }
-
onTabAdd(params: TabAddParams) {
this.#telemetryController.activeTabId = params.tabId
@@ -3521,11 +3597,14 @@ export class AgenticChatController implements ChatHandlers {
if (!success) {
return new ResponseError(ErrorCodes.InternalError, sessionResult.error)
}
- this.#legacySetModelId(params.tabId, session)
// Get the saved pair programming mode from the database or default to true if not found
const savedPairProgrammingMode = this.#chatHistoryDb.getPairProgrammingMode()
session.pairProgrammingMode = savedPairProgrammingMode !== undefined ? savedPairProgrammingMode : true
+ if (session) {
+ // Set the logging object on the session
+ session.setLogging(this.#features.logging)
+ }
// Update the client with the initial pair programming mode
this.#features.chat.chatOptionsUpdate({
@@ -3533,11 +3612,6 @@ export class AgenticChatController implements ChatHandlers {
// Type assertion to support pairProgrammingMode
...(session.pairProgrammingMode !== undefined ? { pairProgrammingMode: session.pairProgrammingMode } : {}),
} as ChatUpdateParams)
-
- if (success && session) {
- // Set the logging object on the session
- session.setLogging(this.#features.logging)
- }
this.setPaidTierMode(params.tabId)
}
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.test.ts
index 0fc768d88e..dbf694b876 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.test.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.test.ts
@@ -1,19 +1,19 @@
import * as assert from 'assert'
-import { MODEL_OPTIONS, MODEL_OPTIONS_FOR_REGION } from './modelSelection'
+import { FALLBACK_MODEL_OPTIONS } from './modelSelection'
describe('modelSelection', () => {
describe('modelOptions', () => {
it('should contain the correct model options', () => {
- assert.ok(Array.isArray(MODEL_OPTIONS), 'modelOptions should be an array')
- assert.strictEqual(MODEL_OPTIONS.length, 2, 'modelOptions should have 2 items')
+ assert.ok(Array.isArray(FALLBACK_MODEL_OPTIONS), 'modelOptions should be an array')
+ assert.strictEqual(FALLBACK_MODEL_OPTIONS.length, 2, 'modelOptions should have 2 items')
// Check that the array contains the expected models
- const modelIds = MODEL_OPTIONS.map(model => model.id)
- assert.ok(modelIds.includes('CLAUDE_SONNET_4_20250514_V1_0'), 'Should include Claude Sonnet 4')
- assert.ok(modelIds.includes('CLAUDE_3_7_SONNET_20250219_V1_0'), 'Should include Claude Sonnet 3.7')
+ const modelIds = FALLBACK_MODEL_OPTIONS.map(model => model.id)
+ assert.ok(modelIds.includes('CLAUDE_SONNET_4_20250514_V1_0'), 'Should include claude-4-sonnet')
+ assert.ok(modelIds.includes('CLAUDE_3_7_SONNET_20250219_V1_0'), 'Should include claude-3.7-sonnet')
// Check that each model has the required properties
- MODEL_OPTIONS.forEach(model => {
+ FALLBACK_MODEL_OPTIONS.forEach(model => {
assert.ok('id' in model, 'Model should have id property')
assert.ok('name' in model, 'Model should have name property')
assert.strictEqual(typeof model.id, 'string', 'Model id should be a string')
@@ -21,56 +21,11 @@ describe('modelSelection', () => {
})
// Check specific model names
- const claudeSonnet4 = MODEL_OPTIONS.find(model => model.id === 'CLAUDE_SONNET_4_20250514_V1_0')
- const claudeSonnet37 = MODEL_OPTIONS.find(model => model.id === 'CLAUDE_3_7_SONNET_20250219_V1_0')
+ const claudeSonnet4 = FALLBACK_MODEL_OPTIONS.find(model => model.id === 'CLAUDE_SONNET_4_20250514_V1_0')
+ const claudeSonnet37 = FALLBACK_MODEL_OPTIONS.find(model => model.id === 'CLAUDE_3_7_SONNET_20250219_V1_0')
- assert.strictEqual(claudeSonnet4?.name, 'Claude Sonnet 4', 'Claude Sonnet 4 should have correct name')
- assert.strictEqual(claudeSonnet37?.name, 'Claude Sonnet 3.7', 'Claude Sonnet 3.7 should have correct name')
- })
- })
-
- describe('modelOptionsForRegion', () => {
- it('should provide all models for us-east-1 region', () => {
- const usEast1Models = MODEL_OPTIONS_FOR_REGION['us-east-1']
- assert.deepStrictEqual(usEast1Models, MODEL_OPTIONS, 'us-east-1 should have all models')
- assert.strictEqual(usEast1Models.length, 2, 'us-east-1 should have 2 models')
-
- const modelIds = usEast1Models.map(model => model.id)
- assert.ok(modelIds.includes('CLAUDE_SONNET_4_20250514_V1_0'), 'us-east-1 should include Claude Sonnet 4')
- assert.ok(
- modelIds.includes('CLAUDE_3_7_SONNET_20250219_V1_0'),
- 'us-east-1 should include Claude Sonnet 3.7'
- )
- })
-
- it('should provide limited models for eu-central-1 region', () => {
- const euCentral1Models = MODEL_OPTIONS_FOR_REGION['eu-central-1']
- assert.ok(Array.isArray(euCentral1Models), 'eu-central-1 models should be an array')
- assert.strictEqual(euCentral1Models.length, 1, 'eu-central-1 should have 1 model')
-
- const modelIds = euCentral1Models.map(model => model.id)
- assert.ok(
- !modelIds.includes('CLAUDE_SONNET_4_20250514_V1_0'),
- 'eu-central-1 should not include Claude Sonnet 4'
- )
- assert.ok(
- modelIds.includes('CLAUDE_3_7_SONNET_20250219_V1_0'),
- 'eu-central-1 should include Claude Sonnet 3.7'
- )
- })
-
- it('should filter out Claude Sonnet 4 for eu-central-1 region', () => {
- const euCentral1Models = MODEL_OPTIONS_FOR_REGION['eu-central-1']
- const claudeSonnet4 = euCentral1Models.find(model => model.id === 'CLAUDE_SONNET_4_20250514_V1_0')
- assert.strictEqual(claudeSonnet4, undefined, 'Claude Sonnet 4 should be filtered out for eu-central-1')
- })
-
- it('should fall back to all models for unknown regions', () => {
- // Test with a region that doesn't exist in the modelOptionsForRegion map
- const unknownRegionModels = MODEL_OPTIONS_FOR_REGION['unknown-region']
-
- // Should be undefined since the region doesn't exist in the map
- assert.strictEqual(unknownRegionModels, undefined, 'Unknown region should return undefined')
+ assert.strictEqual(claudeSonnet4?.name, 'claude-4-sonnet', 'claude-4-sonnet should have correct name')
+ assert.strictEqual(claudeSonnet37?.name, 'claude-3.7-sonnet', 'claude-3.7-sonnet should have correct name')
})
})
})
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.ts
index d7ee53cf75..b36b2345f6 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.ts
@@ -1,5 +1,8 @@
import { ListAvailableModelsResult } from '@aws/language-server-runtimes/protocol'
+/**
+ * @deprecated Do not add new models to the enum.
+ */
export enum BedrockModel {
CLAUDE_SONNET_4_20250514_V1_0 = 'CLAUDE_SONNET_4_20250514_V1_0',
CLAUDE_3_7_SONNET_20250219_V1_0 = 'CLAUDE_3_7_SONNET_20250219_V1_0',
@@ -9,19 +12,14 @@ type ModelDetails = {
label: string
}
-const MODEL_RECORD: Record = {
- [BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0]: { label: 'Claude Sonnet 3.7' },
- [BedrockModel.CLAUDE_SONNET_4_20250514_V1_0]: { label: 'Claude Sonnet 4' },
+export const MODEL_RECORD: Record = {
+ [BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0]: { label: 'claude-3.7-sonnet' },
+ [BedrockModel.CLAUDE_SONNET_4_20250514_V1_0]: { label: 'claude-4-sonnet' },
}
-export const MODEL_OPTIONS: ListAvailableModelsResult['models'] = Object.entries(MODEL_RECORD).map(
+export const FALLBACK_MODEL_OPTIONS: ListAvailableModelsResult['models'] = Object.entries(MODEL_RECORD).map(
([value, { label }]) => ({
id: value,
name: label,
})
)
-
-export const MODEL_OPTIONS_FOR_REGION: Record = {
- 'us-east-1': MODEL_OPTIONS,
- 'eu-central-1': MODEL_OPTIONS.filter(option => option.id !== BedrockModel.CLAUDE_SONNET_4_20250514_V1_0),
-}
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts
index dfcf21308c..4b169c8a69 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts
@@ -24,11 +24,12 @@ import {
getMd5WorkspaceId,
MessagesWithCharacterCount,
estimateCharacterCountFromImageBlock,
+ isCachedValid,
} from './util'
import * as crypto from 'crypto'
import * as path from 'path'
import { Features } from '@aws/language-server-runtimes/server-interface/server'
-import { ContextCommand, ConversationItemGroup } from '@aws/language-server-runtimes/protocol'
+import { ContextCommand, ConversationItemGroup, Model } from '@aws/language-server-runtimes/protocol'
import { ChatMessage, ToolResultStatus } from '@amzn/codewhisperer-streaming'
import { ChatItemType } from '@aws/mynah-ui'
import { getUserHomeDir } from '@aws/lsp-core/out/util/path'
@@ -1084,6 +1085,37 @@ export class ChatDatabase {
this.updateSettings({ modelId: modelId === '' ? undefined : modelId })
}
+ getCachedModels(): { models: Model[]; defaultModelId?: string; timestamp: number } | undefined {
+ const settings = this.getSettings()
+ if (settings?.cachedModels && settings?.modelCacheTimestamp) {
+ return {
+ models: settings.cachedModels,
+ defaultModelId: settings.cachedDefaultModelId,
+ timestamp: settings.modelCacheTimestamp,
+ }
+ }
+ return undefined
+ }
+
+ setCachedModels(models: Model[], defaultModelId?: string): void {
+ const currentTimestamp = Date.now()
+ // Get existing settings to preserve fields like modelId
+ const existingSettings = this.getSettings() || { modelId: undefined }
+ this.updateSettings({
+ ...existingSettings,
+ cachedModels: models,
+ cachedDefaultModelId: defaultModelId,
+ modelCacheTimestamp: currentTimestamp,
+ })
+ this.#features.logging.log(`Models cached at timestamp: ${currentTimestamp}`)
+ }
+
+ isCachedModelsValid(): boolean {
+ const cachedData = this.getCachedModels()
+ if (!cachedData) return false
+ return isCachedValid(cachedData.timestamp)
+ }
+
getPairProgrammingMode(): boolean | undefined {
const settings = this.getSettings()
return settings?.pairProgrammingMode
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts
index 8f3f46b9f4..5a2f989f0c 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts
@@ -10,6 +10,7 @@ import {
ConversationItem,
ConversationItemGroup,
IconType,
+ Model,
ReferenceTrackerInformation,
} from '@aws/language-server-runtimes/server-interface'
import {
@@ -84,6 +85,9 @@ export type Rules = {
export type Settings = {
modelId: string | undefined
pairProgrammingMode?: boolean
+ cachedModels?: Model[]
+ cachedDefaultModelId?: string
+ modelCacheTimestamp?: number
}
export type Conversation = {
@@ -131,6 +135,14 @@ export type MessagesWithCharacterCount = {
currentCount: number
}
+export function isCachedValid(timestamp: number): boolean {
+ const currentTime = Date.now()
+ const cacheAge = currentTime - timestamp
+ const CACHE_TTL = 5 * 60 * 1000 // 5 minutes in milliseconds
+
+ return cacheAge < CACHE_TTL
+}
+
/**
* Converts Message to codewhisperer-streaming ChatMessage
*/
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/utils/agenticChatControllerHelper.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/utils/agenticChatControllerHelper.ts
deleted file mode 100644
index 55ac7873e2..0000000000
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/utils/agenticChatControllerHelper.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { ListAvailableModelsResult } from '@aws/language-server-runtimes/protocol'
-import { MODEL_OPTIONS, MODEL_OPTIONS_FOR_REGION } from '../constants/modelSelection'
-
-/**
- * Gets the latest available model for a region, optionally excluding a specific model
- * @param region The AWS region
- * @param exclude Optional model ID to exclude
- * @returns The latest available model
- */
-export function getLatestAvailableModel(
- region: string | undefined,
- exclude?: string
-): ListAvailableModelsResult['models'][0] {
- const models = region && MODEL_OPTIONS_FOR_REGION[region] ? MODEL_OPTIONS_FOR_REGION[region] : MODEL_OPTIONS
- return models.reverse().find(model => model.id !== exclude) ?? models[models.length - 1]
-}
diff --git a/server/aws-lsp-codewhisperer/src/shared/constants.ts b/server/aws-lsp-codewhisperer/src/shared/constants.ts
index cd453a11ba..33f61a079f 100644
--- a/server/aws-lsp-codewhisperer/src/shared/constants.ts
+++ b/server/aws-lsp-codewhisperer/src/shared/constants.ts
@@ -15,6 +15,8 @@ export const AWS_Q_ENDPOINTS = new Map([
export const AWS_Q_REGION_ENV_VAR = 'AWS_Q_REGION'
export const AWS_Q_ENDPOINT_URL_ENV_VAR = 'AWS_Q_ENDPOINT_URL'
+export const IDE = 'IDE'
+
export const Q_CONFIGURATION_SECTION = 'aws.q'
export const CODE_WHISPERER_CONFIGURATION_SECTION = 'aws.codeWhisperer'
From f3600e362c18ba8f798becfb5d635139525a3896 Mon Sep 17 00:00:00 2001
From: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com>
Date: Fri, 8 Aug 2025 14:08:36 -0700
Subject: [PATCH 04/10] fix: Improve model selection fallback logic when
user's preferred model is unavailable (#2089)
* fix: if user preferred model does not exist, fall back to default model
* fix: minor test changes
---
.../agenticChat/agenticChatController.test.ts | 2 +-
.../agenticChat/agenticChatController.ts | 33 +++++++++++--------
.../agenticChat/constants/modelSelection.ts | 4 +--
3 files changed, 22 insertions(+), 17 deletions(-)
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts
index ede7ae1fad..a7fc385a5a 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts
@@ -3224,7 +3224,7 @@ ${' '.repeat(8)}}
const result = await chatController.onListAvailableModels({ tabId: mockTabId })
- assert.strictEqual(result.selectedModelId, 'claude-4-sonnet') // MODEL_RECORD[DEFAULT_MODEL_ID].label
+ assert.strictEqual(result.selectedModelId, 'claude-4-sonnet') // FALLBACK_MODEL_RECORD[DEFAULT_MODEL_ID].label
// Verify session modelId is updated
assert.strictEqual(session.modelId, 'claude-4-sonnet')
})
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
index af2a154a70..959941ea24 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
@@ -220,7 +220,7 @@ import {
Message as DbMessage,
messageToStreamingMessage,
} from './tools/chatDb/util'
-import { FALLBACK_MODEL_OPTIONS, MODEL_RECORD } from './constants/modelSelection'
+import { FALLBACK_MODEL_OPTIONS, FALLBACK_MODEL_RECORD } from './constants/modelSelection'
import { DEFAULT_IMAGE_VERIFICATION_OPTIONS, verifyServerImage } from '../../shared/imageVerification'
import { sanitize } from '@aws/lsp-core/out/util/path'
import { ActiveUserTracker } from '../../shared/activeUserTracker'
@@ -755,7 +755,7 @@ export class AgenticChatController implements ChatHandlers {
// Get the first fallback model option as default
const defaultModelOption = FALLBACK_MODEL_OPTIONS[1]
- const DEFAULT_MODEL_ID = defaultModelOption?.id || Object.keys(MODEL_RECORD)[1]
+ const DEFAULT_MODEL_ID = defaultModelOption?.id || Object.keys(FALLBACK_MODEL_RECORD)[1]
const sessionResult = this.#chatSessionManagementService.getSession(params.tabId)
const { data: session, success } = sessionResult
@@ -773,22 +773,27 @@ export class AgenticChatController implements ChatHandlers {
let selectedModelId: string
let modelId = this.#chatHistoryDb.getModelId()
+ // Helper function to get model label from FALLBACK_MODEL_RECORD
+ const getModelLabel = (modelKey: string) =>
+ FALLBACK_MODEL_RECORD[modelKey as keyof typeof FALLBACK_MODEL_RECORD]?.label || modelKey
+
+ // Determine selected model ID based on priority
if (modelId) {
- // Case 1: User has previously selected a model - use that model or its mapped version
- // Check if modelId is a key in MODEL_RECORD
- if (modelId in MODEL_RECORD) {
- // If it's a valid model key, use its mapped label
- selectedModelId = MODEL_RECORD[modelId as keyof typeof MODEL_RECORD]?.label || modelId
- } else {
- // Otherwise use as is (might be a direct model ID from the API)
+ // Priority 1: Use modelId if it exists in available models from backend
+ if (models.some(model => model.id === modelId)) {
selectedModelId = modelId
}
- } else if (defaultModelId) {
- // Case 2: No user selection, but API provided a default model - use server recommendation
- selectedModelId = defaultModelId
+ // Priority 2: Use mapped version if modelId exists in FALLBACK_MODEL_RECORD
+ else if (modelId in FALLBACK_MODEL_RECORD) {
+ selectedModelId = getModelLabel(modelId)
+ }
+ // Priority 3: Fall back to default or system default
+ else {
+ selectedModelId = defaultModelId || getModelLabel(DEFAULT_MODEL_ID)
+ }
} else {
- // Case 3: Last resort - use default model's label
- selectedModelId = MODEL_RECORD[DEFAULT_MODEL_ID as keyof typeof MODEL_RECORD]?.label || DEFAULT_MODEL_ID
+ // No user-selected model - use API default or system default
+ selectedModelId = defaultModelId || getModelLabel(DEFAULT_MODEL_ID)
}
// Store the selected model in the session
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.ts
index b36b2345f6..0f3a14ff56 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.ts
@@ -12,12 +12,12 @@ type ModelDetails = {
label: string
}
-export const MODEL_RECORD: Record = {
+export const FALLBACK_MODEL_RECORD: Record = {
[BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0]: { label: 'claude-3.7-sonnet' },
[BedrockModel.CLAUDE_SONNET_4_20250514_V1_0]: { label: 'claude-4-sonnet' },
}
-export const FALLBACK_MODEL_OPTIONS: ListAvailableModelsResult['models'] = Object.entries(MODEL_RECORD).map(
+export const FALLBACK_MODEL_OPTIONS: ListAvailableModelsResult['models'] = Object.entries(FALLBACK_MODEL_RECORD).map(
([value, { label }]) => ({
id: value,
name: label,
From 05bcc24ab1f5de2266f50c63e583c4cc24888e6e Mon Sep 17 00:00:00 2001
From: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com>
Date: Fri, 8 Aug 2025 17:51:45 -0700
Subject: [PATCH 05/10] fix: to support backward compatibility for vs and
eclipse, adding back modelSelection in chat-client (#2095)
---
.../src/client/texts/modelSelection.ts | 23 +++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/chat-client/src/client/texts/modelSelection.ts b/chat-client/src/client/texts/modelSelection.ts
index 4dcaad95ba..813d9e688f 100644
--- a/chat-client/src/client/texts/modelSelection.ts
+++ b/chat-client/src/client/texts/modelSelection.ts
@@ -1,10 +1,33 @@
import { ChatItem, ChatItemFormItem, ChatItemType } from '@aws/mynah-ui'
+/**
+ * @deprecated use aws/chat/listAvailableModels server request instead
+ */
+export enum BedrockModel {
+ CLAUDE_SONNET_4_20250514_V1_0 = 'CLAUDE_SONNET_4_20250514_V1_0',
+ CLAUDE_3_7_SONNET_20250219_V1_0 = 'CLAUDE_3_7_SONNET_20250219_V1_0',
+}
+
+type ModelDetails = {
+ label: string
+}
+
+const modelRecord: Record = {
+ [BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0]: { label: 'claude-3.7-sonnet' },
+ [BedrockModel.CLAUDE_SONNET_4_20250514_V1_0]: { label: 'claude-4-sonnet' },
+}
+
+const modelOptions = Object.entries(modelRecord).map(([value, { label }]) => ({
+ value,
+ label,
+}))
+
export const modelSelection: ChatItemFormItem = {
type: 'select',
id: 'model-selection',
mandatory: true,
hideMandatoryIcon: true,
+ options: modelOptions,
border: false,
autoWidth: true,
}
From 331eb2fcf5fce1571f1f59ea468129796478508c Mon Sep 17 00:00:00 2001
From: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com>
Date: Mon, 11 Aug 2025 11:42:01 -0700
Subject: [PATCH 06/10] fix: check available models from backend before
selecting default model from fallback models (#2102)
---
.../src/language-server/agenticChat/agenticChatController.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
index 959941ea24..e58831c4fa 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
@@ -783,8 +783,8 @@ export class AgenticChatController implements ChatHandlers {
if (models.some(model => model.id === modelId)) {
selectedModelId = modelId
}
- // Priority 2: Use mapped version if modelId exists in FALLBACK_MODEL_RECORD
- else if (modelId in FALLBACK_MODEL_RECORD) {
+ // Priority 2: Use mapped version if modelId exists in FALLBACK_MODEL_RECORD and no backend models available
+ else if (models.length === 0 && modelId in FALLBACK_MODEL_RECORD) {
selectedModelId = getModelLabel(modelId)
}
// Priority 3: Fall back to default or system default
From 13ab1c858e17e6416215aaeb1eb428d3ee9ebc13 Mon Sep 17 00:00:00 2001
From: Tai Lai
Date: Wed, 13 Aug 2025 20:43:31 -0700
Subject: [PATCH 07/10] feat(amazonq): use model display names (#2123)
---
chat-client/src/client/texts/modelSelection.ts | 4 ++--
.../agenticChat/agenticChatController.test.ts | 4 ++--
.../agenticChat/agenticChatController.ts | 18 ++++++++++++------
.../constants/modelSelection.test.ts | 6 +++---
.../agenticChat/constants/modelSelection.ts | 9 +++++++--
5 files changed, 26 insertions(+), 15 deletions(-)
diff --git a/chat-client/src/client/texts/modelSelection.ts b/chat-client/src/client/texts/modelSelection.ts
index 813d9e688f..a1d0247875 100644
--- a/chat-client/src/client/texts/modelSelection.ts
+++ b/chat-client/src/client/texts/modelSelection.ts
@@ -13,8 +13,8 @@ type ModelDetails = {
}
const modelRecord: Record = {
- [BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0]: { label: 'claude-3.7-sonnet' },
- [BedrockModel.CLAUDE_SONNET_4_20250514_V1_0]: { label: 'claude-4-sonnet' },
+ [BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0]: { label: 'Claude 3.7 Sonnet' },
+ [BedrockModel.CLAUDE_SONNET_4_20250514_V1_0]: { label: 'Claude Sonnet 4' },
}
const modelOptions = Object.entries(modelRecord).map(([value, { label }]) => ({
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts
index a7fc385a5a..0d071a4dac 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts
@@ -3224,9 +3224,9 @@ ${' '.repeat(8)}}
const result = await chatController.onListAvailableModels({ tabId: mockTabId })
- assert.strictEqual(result.selectedModelId, 'claude-4-sonnet') // FALLBACK_MODEL_RECORD[DEFAULT_MODEL_ID].label
+ assert.strictEqual(result.selectedModelId, 'claude-sonnet-4') // FALLBACK_MODEL_RECORD[DEFAULT_MODEL_ID].label
// Verify session modelId is updated
- assert.strictEqual(session.modelId, 'claude-4-sonnet')
+ assert.strictEqual(session.modelId, 'claude-sonnet-4')
})
})
})
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
index e58831c4fa..73ef27192e 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
@@ -220,7 +220,7 @@ import {
Message as DbMessage,
messageToStreamingMessage,
} from './tools/chatDb/util'
-import { FALLBACK_MODEL_OPTIONS, FALLBACK_MODEL_RECORD } from './constants/modelSelection'
+import { FALLBACK_MODEL_OPTIONS, FALLBACK_MODEL_RECORD, BEDROCK_MODEL_TO_MODEL_ID } from './constants/modelSelection'
import { DEFAULT_IMAGE_VERIFICATION_OPTIONS, verifyServerImage } from '../../shared/imageVerification'
import { sanitize } from '@aws/lsp-core/out/util/path'
import { ActiveUserTracker } from '../../shared/activeUserTracker'
@@ -777,11 +777,17 @@ export class AgenticChatController implements ChatHandlers {
const getModelLabel = (modelKey: string) =>
FALLBACK_MODEL_RECORD[modelKey as keyof typeof FALLBACK_MODEL_RECORD]?.label || modelKey
+ // Helper function to map enum model ID to API model ID
+ const getMappedModelId = (modelKey: string) =>
+ BEDROCK_MODEL_TO_MODEL_ID[modelKey as keyof typeof BEDROCK_MODEL_TO_MODEL_ID] || modelKey
+
// Determine selected model ID based on priority
if (modelId) {
- // Priority 1: Use modelId if it exists in available models from backend
- if (models.some(model => model.id === modelId)) {
- selectedModelId = modelId
+ const mappedModelId = getMappedModelId(modelId)
+
+ // Priority 1: Use mapped modelId if it exists in available models from backend
+ if (models.some(model => model.id === mappedModelId)) {
+ selectedModelId = mappedModelId
}
// Priority 2: Use mapped version if modelId exists in FALLBACK_MODEL_RECORD and no backend models available
else if (models.length === 0 && modelId in FALLBACK_MODEL_RECORD) {
@@ -789,11 +795,11 @@ export class AgenticChatController implements ChatHandlers {
}
// Priority 3: Fall back to default or system default
else {
- selectedModelId = defaultModelId || getModelLabel(DEFAULT_MODEL_ID)
+ selectedModelId = defaultModelId || getMappedModelId(DEFAULT_MODEL_ID)
}
} else {
// No user-selected model - use API default or system default
- selectedModelId = defaultModelId || getModelLabel(DEFAULT_MODEL_ID)
+ selectedModelId = defaultModelId || getMappedModelId(DEFAULT_MODEL_ID)
}
// Store the selected model in the session
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.test.ts
index dbf694b876..f037470bec 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.test.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.test.ts
@@ -9,7 +9,7 @@ describe('modelSelection', () => {
// Check that the array contains the expected models
const modelIds = FALLBACK_MODEL_OPTIONS.map(model => model.id)
- assert.ok(modelIds.includes('CLAUDE_SONNET_4_20250514_V1_0'), 'Should include claude-4-sonnet')
+ assert.ok(modelIds.includes('CLAUDE_SONNET_4_20250514_V1_0'), 'Should include claude-sonnet-4')
assert.ok(modelIds.includes('CLAUDE_3_7_SONNET_20250219_V1_0'), 'Should include claude-3.7-sonnet')
// Check that each model has the required properties
@@ -24,8 +24,8 @@ describe('modelSelection', () => {
const claudeSonnet4 = FALLBACK_MODEL_OPTIONS.find(model => model.id === 'CLAUDE_SONNET_4_20250514_V1_0')
const claudeSonnet37 = FALLBACK_MODEL_OPTIONS.find(model => model.id === 'CLAUDE_3_7_SONNET_20250219_V1_0')
- assert.strictEqual(claudeSonnet4?.name, 'claude-4-sonnet', 'claude-4-sonnet should have correct name')
- assert.strictEqual(claudeSonnet37?.name, 'claude-3.7-sonnet', 'claude-3.7-sonnet should have correct name')
+ assert.strictEqual(claudeSonnet4?.name, 'Claude Sonnet 4', 'claude-sonnet-4 should have correct name')
+ assert.strictEqual(claudeSonnet37?.name, 'Claude 3.7 Sonnet', 'claude-3.7-sonnet should have correct name')
})
})
})
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.ts
index 0f3a14ff56..46c5446c8c 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/constants/modelSelection.ts
@@ -13,8 +13,13 @@ type ModelDetails = {
}
export const FALLBACK_MODEL_RECORD: Record = {
- [BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0]: { label: 'claude-3.7-sonnet' },
- [BedrockModel.CLAUDE_SONNET_4_20250514_V1_0]: { label: 'claude-4-sonnet' },
+ [BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0]: { label: 'Claude 3.7 Sonnet' },
+ [BedrockModel.CLAUDE_SONNET_4_20250514_V1_0]: { label: 'Claude Sonnet 4' },
+}
+
+export const BEDROCK_MODEL_TO_MODEL_ID: Record = {
+ [BedrockModel.CLAUDE_3_7_SONNET_20250219_V1_0]: 'claude-3.7-sonnet',
+ [BedrockModel.CLAUDE_SONNET_4_20250514_V1_0]: 'claude-sonnet-4',
}
export const FALLBACK_MODEL_OPTIONS: ListAvailableModelsResult['models'] = Object.entries(FALLBACK_MODEL_RECORD).map(
From 18111b079f9051d2b8cbfba7a60da685d0e41b8a Mon Sep 17 00:00:00 2001
From: Tai Lai
Date: Mon, 18 Aug 2025 11:21:10 -0700
Subject: [PATCH 08/10] fix: cached model list should be invalidated on sign
out (#2131)
* fix: cached model list should be invalidated on sign out
* fix test
* avoid throwing error
---
.../agenticChat/agenticChatController.ts | 2 +-
.../agenticChat/tools/chatDb/chatDb.test.ts | 69 ++++++++++++++++++-
.../agenticChat/tools/chatDb/chatDb.ts | 17 +++++
.../AmazonQTokenServiceManager.ts | 5 ++
4 files changed, 91 insertions(+), 2 deletions(-)
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
index 73ef27192e..abcd8ab8ed 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
@@ -349,7 +349,7 @@ export class AgenticChatController implements ChatHandlers {
// @ts-ignore
this.#features.chat.chatOptionsUpdate({ region })
})
- this.#chatHistoryDb = new ChatDatabase(features)
+ this.#chatHistoryDb = ChatDatabase.getInstance(features)
this.#tabBarController = new TabBarController(
features,
this.#chatHistoryDb,
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts
index f4f61f955b..aed5a30643 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.test.ts
@@ -57,7 +57,7 @@ describe('ChatDatabase', () => {
},
} as unknown as Features
- chatDb = new ChatDatabase(mockFeatures)
+ chatDb = ChatDatabase.getInstance(mockFeatures)
})
afterEach(() => {
@@ -665,6 +665,73 @@ describe('ChatDatabase', () => {
)
})
})
+
+ describe('Model Cache Management', () => {
+ beforeEach(async () => {
+ await chatDb.databaseInitialize(0)
+ })
+
+ it('should cache and retrieve models', () => {
+ const models = [{ id: 'model-1', name: 'Test Model' }]
+ const defaultModelId = 'model-1'
+
+ chatDb.setCachedModels(models, defaultModelId)
+ const cached = chatDb.getCachedModels()
+
+ assert.ok(cached, 'Should return cached data')
+ assert.deepStrictEqual(cached.models, models)
+ assert.strictEqual(cached.defaultModelId, defaultModelId)
+ assert.ok(cached.timestamp > 0, 'Should have timestamp')
+ })
+
+ it('should validate cache expiry', () => {
+ const models = [{ id: 'model-1', name: 'Test Model' }]
+ chatDb.setCachedModels(models)
+
+ // Mock isCachedValid to return false (expired)
+ const isCachedValidStub = sinon.stub(util, 'isCachedValid').returns(false)
+
+ assert.strictEqual(chatDb.isCachedModelsValid(), false)
+
+ isCachedValidStub.restore()
+ })
+
+ it('should clear cached models', () => {
+ const models = [{ id: 'model-1', name: 'Test Model' }]
+ chatDb.setCachedModels(models)
+
+ // Verify cache exists
+ assert.ok(chatDb.getCachedModels(), 'Cache should exist before clearing')
+
+ chatDb.clearCachedModels()
+
+ // Verify cache is cleared
+ assert.strictEqual(chatDb.getCachedModels(), undefined, 'Cache should be cleared')
+ })
+
+ it('should clear model cache via static method when instance exists', () => {
+ const models = [{ id: 'model-1', name: 'Test Model' }]
+ chatDb.setCachedModels(models)
+
+ // Verify cache exists
+ assert.ok(chatDb.getCachedModels(), 'Cache should exist before clearing')
+
+ ChatDatabase.clearModelCache()
+
+ // Verify cache is cleared
+ assert.strictEqual(chatDb.getCachedModels(), undefined, 'Cache should be cleared via static method')
+ })
+
+ it('should handle static clearModelCache when no instance exists', () => {
+ // Close current instance
+ chatDb.close()
+
+ // Should not throw when no instance exists
+ assert.doesNotThrow(() => {
+ ChatDatabase.clearModelCache()
+ }, 'Should not throw when no instance exists')
+ })
+ })
})
function uuid(): `${string}-${string}-${string}-${string}-${string}` {
throw new Error('Function not implemented.')
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts
index 4b169c8a69..2e4f7a099d 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/chatDb.ts
@@ -123,6 +123,12 @@ export class ChatDatabase {
return ChatDatabase.#instance
}
+ public static clearModelCache(): void {
+ if (ChatDatabase.#instance) {
+ ChatDatabase.#instance.clearCachedModels()
+ }
+ }
+
public close() {
this.#db.close()
ChatDatabase.#instance = undefined
@@ -1116,6 +1122,17 @@ export class ChatDatabase {
return isCachedValid(cachedData.timestamp)
}
+ clearCachedModels(): void {
+ const existingSettings = this.getSettings() || { modelId: undefined }
+ this.updateSettings({
+ ...existingSettings,
+ cachedModels: undefined,
+ cachedDefaultModelId: undefined,
+ modelCacheTimestamp: undefined,
+ })
+ this.#features.logging.log('Model cache cleared')
+ }
+
getPairProgrammingMode(): boolean | undefined {
const settings = this.getSettings()
return settings?.pairProgrammingMode
diff --git a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts
index 8db60a69ab..c418fe29ca 100644
--- a/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts
+++ b/server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts
@@ -33,6 +33,7 @@ import { getAmazonQRegionAndEndpoint } from './configurationUtils'
import { getUserAgent } from '../telemetryUtils'
import { StreamingClientServiceToken } from '../streamingClientService'
import { parse } from '@aws-sdk/util-arn-parser'
+import { ChatDatabase } from '../../language-server/agenticChat/tools/chatDb/chatDb'
/**
* AmazonQTokenServiceManager manages state and provides centralized access to
@@ -147,6 +148,10 @@ export class AmazonQTokenServiceManager extends BaseAmazonQServiceManager<
if (type === 'iam') {
return
}
+
+ // Clear model cache when credentials are deleted
+ ChatDatabase.clearModelCache()
+
this.cancelActiveProfileChangeToken()
this.resetCodewhispererService()
From 447f6cb44a8a0b486be544786eb201a0140146be Mon Sep 17 00:00:00 2001
From: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com>
Date: Tue, 26 Aug 2025 08:42:18 -0700
Subject: [PATCH 09/10] fix: adding default modelId and setting cache ttl to 30
minutes (#2161)
* fix: adding defaultmodelId and setting cache ttl to 30 minutes
* fix: fixing tests
* fix: updating comments
---
.../agenticChat/agenticChatController.test.ts | 2 +-
.../language-server/agenticChat/agenticChatController.ts | 6 +++---
.../src/language-server/agenticChat/tools/chatDb/util.ts | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts
index 0d071a4dac..a0f5d8a3a8 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.test.ts
@@ -3196,7 +3196,7 @@ ${' '.repeat(8)}}
const result = await chatController.onListAvailableModels({ tabId: 'invalid-tab' })
assert.strictEqual(result.tabId, 'invalid-tab')
- assert.strictEqual(result.selectedModelId, DEFAULT_MODEL_ID)
+ assert.strictEqual(result.selectedModelId, 'model1')
getSessionStub.restore()
})
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
index abcd8ab8ed..52ae5324f2 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts
@@ -714,7 +714,7 @@ export class AgenticChatController implements ChatHandlers {
})
// Wait for the response to be completed before proceeding
- this.#log('Model Response', responseResult.toString())
+ this.#log('Model Response: ', JSON.stringify(responseResult, null, 2))
models = Object.values(responseResult.models).map(({ modelId, modelName }) => ({
id: modelId,
name: modelName ?? modelId,
@@ -755,7 +755,7 @@ export class AgenticChatController implements ChatHandlers {
// Get the first fallback model option as default
const defaultModelOption = FALLBACK_MODEL_OPTIONS[1]
- const DEFAULT_MODEL_ID = defaultModelOption?.id || Object.keys(FALLBACK_MODEL_RECORD)[1]
+ const DEFAULT_MODEL_ID = defaultModelId || defaultModelOption?.id
const sessionResult = this.#chatSessionManagementService.getSession(params.tabId)
const { data: session, success } = sessionResult
@@ -769,7 +769,7 @@ export class AgenticChatController implements ChatHandlers {
}
}
- // Determine which model ID to select based on priority.
+ // Determine selected model ID based on priority
let selectedModelId: string
let modelId = this.#chatHistoryDb.getModelId()
diff --git a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts
index 5a2f989f0c..77496cd96b 100644
--- a/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts
+++ b/server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/chatDb/util.ts
@@ -138,7 +138,7 @@ export type MessagesWithCharacterCount = {
export function isCachedValid(timestamp: number): boolean {
const currentTime = Date.now()
const cacheAge = currentTime - timestamp
- const CACHE_TTL = 5 * 60 * 1000 // 5 minutes in milliseconds
+ const CACHE_TTL = 30 * 60 * 1000 // 30 minutes in milliseconds
return cacheAge < CACHE_TTL
}
From 433d9caea2022b7d94464611217047c456a5590c Mon Sep 17 00:00:00 2001
From: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com>
Date: Tue, 26 Aug 2025 13:04:59 -0700
Subject: [PATCH 10/10] fix: Merge from main (#2169)
* refactor: split edit from completion code path (#2058)
* feat(amazonq): add two more tips for the did you know section (#2063)
* fix(amazonq): make display findings tool run more often (#2067)
Co-authored-by: Blake Lazarine
* fix(amazonq): fix to add disable/enable feature back to mcp servers (#2052)
* fix(amazonq): fix to add disable/enable feature back to mcp servers
* fix(amazonq): fix to add unit tests
* fix: fix to add unit test coverage for mcpUtils
* feat(amazonq): enable sonnet 4 for fra region (#2069)
* chore(release): release packages from branch main (#2050)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(amazonq): emit failed status for amazonq_invokeLLM (#2071)
* chore: generate completion log string (#2072)
* fix(amazonq): fix fallout of #2051 (#2057)
* fix(amazonq): fix fallout of #2051
avoid buggy global states
* fix(amazonq): fix tests
fix should report UTD test case
* chore: merge agentic version 1.26.0 (#2076)
* Bump agentic version: 1.26.0
* chore: format version json
---------
Co-authored-by: aws-toolkit-automation <>
* fix(amazonq): bundle corect registry.node for windows (#2061)
* chore: bump runtimes to 0.2.123 (#2082)
* fix: remove edit cache logic (#2079)
* fix(amazonq): add fallback classpath generation (#2077)
* fix(amazonq): skips continuous monitoring when WCS sees workspace as idle (#2066)
* fix(amazonq): skips continuous monitoring when WCS sees workspace as idle
* fix(amazonq): skips creating remote workspace at the start
---------
Co-authored-by: Jiatong Li
* fix: sessionManager misused because there are 2 types of manager now (#2090)
* chore: mapping adt plugin to abap to let inline suggestion work (#2085)
* chore: support inline suggestion in adt plugin for eclipse
* fix: add import URI
* chore: add detailed error description for uri is empty for language id
* feat(amazonq): read tool ui revamp
* feat(amazonq): read tool message revamp (#2049)
* feat(amazonq): read tool message revamp
* fix tests
* feat: file search ui (#2078)
* feat: file search ui
* fix tests
* fix integration tests
* remove unnecessary type check
* fix: use quotes instead of backticks
* fix: creating a new sesion for Edits trigger with next token (#2094)
* chore: bump @aws/mynah-ui to 4.36.4 (#2096)
* fix(amazonq): leverage lcs to find the chars added and removed (#2092)
* fix: update client name to support Sagemaker AI origin for agentic chat (#2093)
Co-authored-by: chungjac
* chore(release): release packages from branch main (#2073)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* chore: format version.json after incrementing (#2068)
* chore: merge agentic version 1.27.0 (#2107)
* Bump agentic version: 1.27.0
* Revert "feat(amazonq): read tool ui revamp"
This reverts commit c65428bab2cf5e47badf1e3a9453babcf881e60c.
* fix: the style in version json (#2106)
---------
Co-authored-by: aws-toolkit-automation <>
Co-authored-by: Tai Lai
Co-authored-by: Christopher Christou <39839589+awschristou@users.noreply.github.com>
* fix(amazonq): persist mcp configs in agent json on start-up (#2112)
* feat(amazonq): added mcp admin level configuration with GetProfile (#2000)
* feat(amazonq): added mcp admin level configuration with GetProfile
* feat(amazonq): added UX message for mcp admin control
* test: add unit tests for ProfileStatusMonitor static functionality
* test: add comprehensive unit tests for MCP admin control features
* fix: fix to wait for serviceManager is initialzied to initialize mcp managers
* fix: fix for unit test failures
* fix: fix for UI changes
* fix(amazonq): fix to to rename mcp enabled function and max time limit of 10 seconds
* fix: fix to add async initialization for mcp manager
* fix: fix to move action buttons to mcpEventHandler for listMcpServers
* fix: added try and catch block for mcp initialization
* fix: fix for merge conflicts
* fix: fix for test failure
* fix: remove the unnecessary feature flag
* fix: fix for mynah test failure
* fix: fix to retry function to common util
* fix: fix to add retryUtils
* fix: Use file context override in the inline completion params for Jupyter Notebook (#2114)
* feat(amazonq): read tool ui revamp (#2113) (#2121)
* feat(amazonq): read tool ui revamp
* feat(amazonq): read tool message revamp (#2049)
* feat(amazonq): read tool message revamp
* fix tests
* feat: file search ui (#2078)
* feat: file search ui
* fix tests
* fix integration tests
* remove unnecessary type check
* fix: use quotes instead of backticks
* fix header update issue
* fix integration test
* refactor: improve generateCompletion logging format (#2125)
* fix(amazonq): handle case where multiple rules are provided with the same name (#2118)
* fix(amazonq): handle case where multiple rules are provided with the same name
* fix(amazonq): add unit test for duplicate custom guidelines
* fix(amazonq): add unit test for processToolUses
* fix(amazonq): set context type for AmazonQ.md to rule
* fix(amazonq): add README.md back as rule context type
---------
Co-authored-by: Blake Lazarine
* perf: remove edit completion retry mechanism on document change (#2124)
* ci: remove need for custom PAT for release branch workflow (#2126)
The token from GitHub Actions is sufficient
* fix: proper path handling for additional context (#2129)
* fix: proper pathing for additonal context
* fix: update existing tests to also mock path.join()
* fix(amazonq): fix regression of mcp config in agent config (#2101)
* fix(amazonq): update process-permission-updates to be the same as previous behavior
* fix(chat-client): update package.json chat-client to prod
* fix(amazonq): retain mcp permissions after disable/enable server
* fix: dont call initOneServer on Built-in
* fix: deny permission does not persist after restart IDE
---------
Co-authored-by: Boyu
* feat: remove project type validation from LSP layer (#2103)
* feat: remove project type validation from LSP layer
* refactor: remove unused validation functions from LSP layer
* fix: fix for button text and remove profilearn caching (#2137)
* chore: bump runtimes to 0.2.126 (#2138)
* fix: fix to add disk caching for mcp admin state (#2139)
* fix: fix to add disk caching for mcp admin state
* fix: fix to add logging
* fix: fix to initialize mcpManager in any case and discover servers based on mcpState
* fix: fix for unit test failure
* refactor: cleanup old nep code path which is no longered being used in codewhispererServer.ts (#2141)
* fix(amazonq): add server side control for WCS features (#2128)
Co-authored-by: Jiatong Li
* chore: update STE userContext version metadata (#2142)
* fix: fix to turn on and off MCP servers incase of error based on last state (#2143)
Co-authored-by: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com>
* fix: empty userTriggerDecision not being sent for NEP code path (#2140)
* refactor: clean up old NEP code path which is no longer used
* fix: empty userTriggerDecision not being sent for NEP code path
* test: disable telemetry test temporarily
* fix: patch
* revert: a
* revert: a
* chore(release): release packages from branch main (#2117)
* fix(amazonq): don't let flare send discard for the still valid suggestion in JB (#2145)
* fix(amazonq): don't let flare send discard for the still valid suggestion in JB
if a valid sessionId is returned back to JB, JB will eventually
send a decision for it, but when user types a character to reject
the current suggestion, on JB side it will 1) send reject for the current
one 2) send a new trigger for the latest context. 2) will happen before
1) which will discard the current active session on flare's end.
We don't want flare to do that for JB.
* fix(amazonq): createSession doesn't need to close the previous session
it should be handled together with telemetry reporting
* fix(amazonq): fix test
* fix(amazonq): test fix attempt
* fix(amazonq): test fix attempt
* chore(release): release packages from branch main (#2149)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* chore: merge agentic version 1.29.0 (#2151)
* chore: bump agentic version: 1.29.0
* chore: empty commit to start Github Action (#2150)
---------
Co-authored-by: aws-toolkit-automation <>
* feat: add basic OAuth client for remote MCP (#2136)
* feat: add basic mcp oauth client
* fix: relax condition for triggering oauth flow; add resilience checks for as availability
* feat: add unit test for auth client
* fix: multiple fixes for remote mcp error and timeout
* fix: adding streakTracker to track streakLength across Completions and Edits (#2147)
* feat: disable pkce flow during plugin load (#2153)
* chore: bump runtimes to 0.2.127 (#2156)
* fix: multiple fixes on auth flow edge cases (#2155)
* feat: update MCP manager and utilities (#2158)
Co-authored-by: Boyu
* fix(amazonq): disable typewriter animation (#2160)
* ci: generation of builds action can be triggered manually (#2157)
Co-authored-by: Laxman Reddy <141967714+laileni-aws@users.noreply.github.com>
* fix(amazonq): fix for mcp servers operations to edit server config only (#2165)
* fix(amazonq): fix for mcp servers operations to edit server specific config
* fix(amazonq): additional mcp server config fixes
* fix: resolve test failures
* fix: update MCP manager configuration
* fix: allow ci to run on release branches (#2159)
Currently release/agentic/* branches used in our release process have rules that require CI to be run. However the corresponding workflows do not actually trigger for release branches. This change ensures no manual overrides are required in the release process and CI mapped with the branch actually runs.
* Updating the model of listAvailableModels (#2064)
* fix(amazonq): fix flickering issue for model selection dropdown and agenticCoding toggle (#2065)
* fix(amazonq): fix flickering issue for modelId and agenticCoding
* fix(amazonq): Fixing flaky tests
* feat(amazonq): Fetching models from backend and adding cache implementation. (#2075)
* fix: removing and refactoring legacy code before implementing model selection
* feat(amazonq): adding cache implementation and fetching models from listAvailableModels api
* feat(amazonq): adding selected model in error case
* feat(amazonq): adding test cases
* fix: addressing comments
* fix: fixing test cases and adding modelName to models
* fix: minor edits
* fix: minor edits
* fix: minor modifications in logs
* fix: adding default model if api throws any errors
* fix: refactoring code
* fix: Improve model selection fallback logic when user's preferred model is unavailable (#2089)
* fix: if user preferred model does not exist, fall back to default model
* fix: minor test changes
* fix: to support backward compatibility for vs and eclipse, adding back modelSelection in chat-client (#2095)
* fix: check available models from backend before selecting default model from fallback models (#2102)
* feat(amazonq): use model display names (#2123)
* fix: cached model list should be invalidated on sign out (#2131)
* fix: cached model list should be invalidated on sign out
* fix test
* avoid throwing error
* fix: adding default modelId and setting cache ttl to 30 minutes (#2161)
* fix: adding defaultmodelId and setting cache ttl to 30 minutes
* fix: fixing tests
* fix: updating comments
* fix: lint issue while resolving merge conflicts
---------
Co-authored-by: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com>
Co-authored-by: abhraina-aws
Co-authored-by: BlakeLazarine
Co-authored-by: Blake Lazarine
Co-authored-by: invictus <149003065+ashishrp-aws@users.noreply.github.com>
Co-authored-by: Tai Lai
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Na Yue
Co-authored-by: andrewyuq <89420755+andrewyuq@users.noreply.github.com>
Co-authored-by: Sherry Lu <75588211+XiaoxuanLu@users.noreply.github.com>
Co-authored-by: tsmithsz <84354541+tsmithsz@users.noreply.github.com>
Co-authored-by: liumofei-amazon <98127670+liumofei-amazon@users.noreply.github.com>
Co-authored-by: Jiatong Li
Co-authored-by: Jiatong Li
Co-authored-by: atontb <104926752+atonaamz@users.noreply.github.com>
Co-authored-by: Jayakrishna P
Co-authored-by: chungjac
Co-authored-by: Christopher Christou <39839589+awschristou@users.noreply.github.com>
Co-authored-by: Tai Lai
Co-authored-by: Dung Dong
Co-authored-by: Lei Gao <97199248+leigaol@users.noreply.github.com>
Co-authored-by: Richard Li <742829+rli@users.noreply.github.com>
Co-authored-by: Boyu
Co-authored-by: Rajanna-Karthik
Co-authored-by: manodnyab <66754471+manodnyab@users.noreply.github.com>
Co-authored-by: Shruti Sinha <44882001+shruti0085@users.noreply.github.com>
---
.github/workflows/create-agent-standalone.yml | 2 +
.../create-release-candidate-branch.yml | 21 +-
.github/workflows/lsp-ci.yaml | 4 +-
.github/workflows/npm-packaging.yaml | 4 +-
.release-please-manifest.json | 14 +-
app/aws-lsp-antlr4-runtimes/package.json | 2 +-
app/aws-lsp-buildspec-runtimes/package.json | 2 +-
.../package.json | 2 +-
app/aws-lsp-codewhisperer-runtimes/README.md | 11 +
.../registry-js/win32-x64/registry.node | Bin 0 -> 696320 bytes
.../package.json | 5 +-
.../src/version.json | 2 +-
app/aws-lsp-identity-runtimes/package.json | 2 +-
app/aws-lsp-json-runtimes/package.json | 2 +-
.../package.json | 2 +-
app/aws-lsp-partiql-runtimes/package.json | 2 +-
app/aws-lsp-s3-runtimes/package.json | 2 +-
app/aws-lsp-yaml-json-webworker/package.json | 2 +-
app/aws-lsp-yaml-runtimes/package.json | 2 +-
app/hello-world-lsp-runtimes/package.json | 2 +-
chat-client/CHANGELOG.md | 34 ++
chat-client/package.json | 6 +-
chat-client/src/client/mcpMynahUi.test.ts | 11 +-
chat-client/src/client/mcpMynahUi.ts | 38 +-
chat-client/src/client/mynahUi.ts | 17 +-
chat-client/src/client/tabs/tabFactory.ts | 2 +
client/vscode/package.json | 2 +-
core/aws-lsp-core/CHANGELOG.md | 12 +
core/aws-lsp-core/package.json | 4 +-
core/aws-lsp-core/src/index.ts | 1 +
core/aws-lsp-core/src/util/retryUtils.test.ts | 120 ++++
core/aws-lsp-core/src/util/retryUtils.ts | 77 +++
.../q-agentic-chat-server/package.json | 2 +-
.../src/tests/agenticChatInteg.test.ts | 13 +-
package-lock.json | 128 ++---
server/aws-lsp-antlr4/CHANGELOG.md | 14 +
server/aws-lsp-antlr4/package.json | 6 +-
server/aws-lsp-buildspec/package.json | 2 +-
server/aws-lsp-cloudformation/package.json | 2 +-
server/aws-lsp-codewhisperer/CHANGELOG.md | 79 +++
server/aws-lsp-codewhisperer/package.json | 6 +-
.../client/token/bearer-token-service.json | 85 +++
.../token/codewhispererbearertokenclient.d.ts | 26 +
.../agenticChat/agenticChatController.test.ts | 102 +++-
.../agenticChat/agenticChatController.ts | 240 +++++---
.../agenticChat/agenticChatResultStream.ts | 63 +-
.../agenticChat/constants/constants.ts | 2 +
.../context/additionalContextProvider.test.ts | 117 ++++
.../context/additionalContextProvider.ts | 2 +-
.../src/language-server/agenticChat/errors.ts | 1 +
.../agenticChat/qAgenticChatServer.ts | 2 +-
.../agenticChat/tools/fileSearch.ts | 4 +
.../tools/mcp/mcpEventHandler.test.ts | 106 ++++
.../agenticChat/tools/mcp/mcpEventHandler.ts | 261 +++++----
.../agenticChat/tools/mcp/mcpManager.test.ts | 232 +++++++-
.../agenticChat/tools/mcp/mcpManager.ts | 406 +++++++++----
.../tools/mcp/mcpOauthClient.test.ts | 122 ++++
.../agenticChat/tools/mcp/mcpOauthClient.ts | 491 ++++++++++++++++
.../agenticChat/tools/mcp/mcpTypes.ts | 1 +
.../agenticChat/tools/mcp/mcpUtils.test.ts | 321 +++++++++++
.../agenticChat/tools/mcp/mcpUtils.ts | 219 ++++---
.../tools/mcp/profileStatusMonitor.test.ts | 182 ++++++
.../tools/mcp/profileStatusMonitor.ts | 182 ++++++
.../tools/qCodeAnalysis/codeReview.test.ts | 28 +
.../tools/qCodeAnalysis/codeReview.ts | 5 +
.../qCodeAnalysis/displayFindingsConstants.ts | 1 +
.../agenticChat/tools/toolServer.ts | 166 +++++-
.../src/language-server/chat/qChatServer.ts | 4 +-
.../inline-completion/codePercentage.ts | 14 +-
.../codeWhispererServer.nep.test.ts | 2 +
.../codeWhispererServer.test.ts | 167 +++---
.../inline-completion/codeWhispererServer.ts | 537 ++++++++----------
.../inline-completion/constants.ts | 27 +
.../inline-completion/diffUtils.ts | 67 +++
.../documentChangedListener.ts | 19 +
.../editCompletionHandler.ts | 430 ++++++++++++++
.../session/sessionManager.test.ts | 70 +--
.../session/sessionManager.ts | 59 +-
.../inline-completion/telemetry.ts | 48 +-
.../tracker/streakTracker.test.ts | 85 +++
.../tracker/streakTracker.ts | 42 ++
.../inline-completion/trigger.ts | 57 ++
.../userTriggerDecision.test.ts | 36 +-
.../resources/SupportedProjects.ts | 3 +
.../netTransform/tests/validation.test.ts | 120 +---
.../netTransform/transformHandler.ts | 12 -
.../netTransform/validation.ts | 30 +-
.../IdleWorkspaceManager.test.ts | 75 +++
.../workspaceContext/IdleWorkspaceManager.ts | 42 ++
.../workspaceContext/artifactManager.ts | 15 +
.../dependency/dependencyDiscoverer.ts | 3 +-
.../workspaceContextServer.ts | 17 +-
.../workspaceFolderManager.test.ts | 163 ++++--
.../workspaceFolderManager.ts | 248 +++++---
.../AmazonQTokenServiceManager.ts | 19 +
.../BaseAmazonQServiceManager.ts | 5 +
.../src/shared/codeWhispererService.test.ts | 26 +
.../src/shared/codeWhispererService.ts | 208 ++++++-
.../shared/telemetry/telemetryService.test.ts | 12 +-
.../src/shared/telemetry/telemetryService.ts | 17 +-
.../src/shared/telemetryUtils.test.ts | 30 +-
.../src/shared/telemetryUtils.ts | 13 +-
.../src/shared/utils.test.ts | 18 +-
.../aws-lsp-codewhisperer/src/shared/utils.ts | 7 +-
server/aws-lsp-identity/package.json | 2 +-
server/aws-lsp-json/CHANGELOG.md | 14 +
server/aws-lsp-json/package.json | 6 +-
server/aws-lsp-notification/package.json | 2 +-
server/aws-lsp-partiql/CHANGELOG.md | 7 +
server/aws-lsp-partiql/package.json | 4 +-
server/aws-lsp-s3/package.json | 2 +-
server/aws-lsp-yaml/CHANGELOG.md | 14 +
server/aws-lsp-yaml/package.json | 6 +-
server/device-sso-auth-lsp/package.json | 2 +-
server/hello-world-lsp/package.json | 2 +-
115 files changed, 5309 insertions(+), 1527 deletions(-)
create mode 100644 app/aws-lsp-codewhisperer-runtimes/_bundle-assets/registry-js/win32-x64/registry.node
create mode 100644 core/aws-lsp-core/src/util/retryUtils.test.ts
create mode 100644 core/aws-lsp-core/src/util/retryUtils.ts
create mode 100644 server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.test.ts
create mode 100644 server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpOauthClient.ts
create mode 100644 server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.test.ts
create mode 100644 server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/profileStatusMonitor.ts
create mode 100644 server/aws-lsp-codewhisperer/src/language-server/inline-completion/documentChangedListener.ts
create mode 100644 server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts
create mode 100644 server/aws-lsp-codewhisperer/src/language-server/inline-completion/tracker/streakTracker.test.ts
create mode 100644 server/aws-lsp-codewhisperer/src/language-server/inline-completion/tracker/streakTracker.ts
create mode 100644 server/aws-lsp-codewhisperer/src/language-server/inline-completion/trigger.ts
create mode 100644 server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.test.ts
create mode 100644 server/aws-lsp-codewhisperer/src/language-server/workspaceContext/IdleWorkspaceManager.ts
diff --git a/.github/workflows/create-agent-standalone.yml b/.github/workflows/create-agent-standalone.yml
index 2807a50aea..f50b5b43b0 100644
--- a/.github/workflows/create-agent-standalone.yml
+++ b/.github/workflows/create-agent-standalone.yml
@@ -3,10 +3,12 @@ name: Create agent-standalone bundles
on:
push:
branches: [main, feature/*, release/agentic/*]
+ workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
+ if: github.event_name == 'push' || github.actor_id == github.repository_owner_id
steps:
- name: Checkout repository
diff --git a/.github/workflows/create-release-candidate-branch.yml b/.github/workflows/create-release-candidate-branch.yml
index aa37873d2a..b447f11803 100644
--- a/.github/workflows/create-release-candidate-branch.yml
+++ b/.github/workflows/create-release-candidate-branch.yml
@@ -28,14 +28,15 @@ jobs:
setupRcBranch:
name: Set up a Release Candidate Branch
runs-on: ubuntu-latest
+ permissions:
+ contents: write
steps:
- name: Sync code
uses: actions/checkout@v4
with:
ref: ${{ inputs.commitId }}
- # Use RELEASE_CANDIDATE_BRANCH_CREATION_PAT to ensure workflow triggering works
- token: ${{ secrets.RELEASE_CANDIDATE_BRANCH_CREATION_PAT }}
+ token: ${{ secrets.GITHUB_TOKEN }}
persist-credentials: true
- name: Setup Node.js
@@ -44,6 +45,10 @@ jobs:
node-version: '20'
cache: 'npm'
+ # Needed to format the json file being checked in
+ - name: Install dependencies
+ run: npm ci
+
- name: Calculate Release Version
id: release-version
env:
@@ -87,6 +92,9 @@ jobs:
git add "$VERSION_FILE"
+ # Ensure the file does not cause issues when merged to main
+ npm run format-staged
+
- name: Create Release Candidate Branch
id: release-branch
env:
@@ -102,15 +110,8 @@ jobs:
env:
BRANCH_NAME: ${{ steps.release-branch.outputs.BRANCH_NAME }}
RELEASE_VERSION: ${{ steps.release-version.outputs.RELEASE_VERSION }}
- # We use the toolkit-automation account, basically something that
- # isn't the default GitHub Token, because you cannot chain actions with that.
- # In our case, after pushing a commit (below), we want create-agent-standalone.yml
- # to start automatically.
- REPO_PAT: ${{ secrets.RELEASE_CANDIDATE_BRANCH_CREATION_PAT }}
run: |
git config --global user.email "<>"
git config --global user.name "aws-toolkit-automation"
- # Configure git to use the PAT token for authentication
- git remote set-url origin "https://x-access-token:${REPO_PAT}@github.com/${{ github.repository }}.git"
- git commit -m "Bump agentic version: $RELEASE_VERSION"
+ git commit --no-verify -m "chore: bump agentic version: $RELEASE_VERSION"
git push --set-upstream origin "$BRANCH_NAME"
diff --git a/.github/workflows/lsp-ci.yaml b/.github/workflows/lsp-ci.yaml
index bda44ec74a..15f87d2428 100644
--- a/.github/workflows/lsp-ci.yaml
+++ b/.github/workflows/lsp-ci.yaml
@@ -1,9 +1,9 @@
name: Language Server CI
on:
push:
- branches: [main, dev, feature/*]
+ branches: [main, dev, feature/*, release/agentic/*]
pull_request:
- branches: [main, dev, feature/*]
+ branches: [main, dev, feature/*, release/agentic/*]
jobs:
test:
diff --git a/.github/workflows/npm-packaging.yaml b/.github/workflows/npm-packaging.yaml
index 724c9a0c05..d4ce77e07c 100644
--- a/.github/workflows/npm-packaging.yaml
+++ b/.github/workflows/npm-packaging.yaml
@@ -1,9 +1,9 @@
name: NPM Packaging
on:
push:
- branches: [main, dev, feature/*]
+ branches: [main, dev, feature/*, release/agentic/*]
pull_request:
- branches: [main, dev, feature/*]
+ branches: [main, dev, feature/*, release/agentic/*]
jobs:
build:
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 61dc88f5e1..49f118f219 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,9 +1,9 @@
{
- "chat-client": "0.1.30",
- "core/aws-lsp-core": "0.0.13",
- "server/aws-lsp-antlr4": "0.1.17",
- "server/aws-lsp-codewhisperer": "0.0.71",
- "server/aws-lsp-json": "0.1.17",
- "server/aws-lsp-partiql": "0.0.16",
- "server/aws-lsp-yaml": "0.1.17"
+ "chat-client": "0.1.33",
+ "core/aws-lsp-core": "0.0.14",
+ "server/aws-lsp-antlr4": "0.1.18",
+ "server/aws-lsp-codewhisperer": "0.0.75",
+ "server/aws-lsp-json": "0.1.18",
+ "server/aws-lsp-partiql": "0.0.17",
+ "server/aws-lsp-yaml": "0.1.18"
}
diff --git a/app/aws-lsp-antlr4-runtimes/package.json b/app/aws-lsp-antlr4-runtimes/package.json
index e283033afb..89fa95dcb3 100644
--- a/app/aws-lsp-antlr4-runtimes/package.json
+++ b/app/aws-lsp-antlr4-runtimes/package.json
@@ -12,7 +12,7 @@
"webpack": "webpack"
},
"dependencies": {
- "@aws/language-server-runtimes": "^0.2.121",
+ "@aws/language-server-runtimes": "^0.2.127",
"@aws/lsp-antlr4": "*",
"antlr4-c3": "^3.4.1",
"antlr4ng": "^3.0.4"
diff --git a/app/aws-lsp-buildspec-runtimes/package.json b/app/aws-lsp-buildspec-runtimes/package.json
index 0fa2217def..0ad07ddb8f 100644
--- a/app/aws-lsp-buildspec-runtimes/package.json
+++ b/app/aws-lsp-buildspec-runtimes/package.json
@@ -7,7 +7,7 @@
"compile": "tsc --build"
},
"dependencies": {
- "@aws/language-server-runtimes": "^0.2.121",
+ "@aws/language-server-runtimes": "^0.2.127",
"@aws/lsp-buildspec": "^0.0.1"
}
}
diff --git a/app/aws-lsp-cloudformation-runtimes/package.json b/app/aws-lsp-cloudformation-runtimes/package.json
index 336e9b48ae..d211149d0d 100644
--- a/app/aws-lsp-cloudformation-runtimes/package.json
+++ b/app/aws-lsp-cloudformation-runtimes/package.json
@@ -7,7 +7,7 @@
"compile": "tsc --build"
},
"dependencies": {
- "@aws/language-server-runtimes": "^0.2.121",
+ "@aws/language-server-runtimes": "^0.2.127",
"@aws/lsp-cloudformation": "^0.0.1"
}
}
diff --git a/app/aws-lsp-codewhisperer-runtimes/README.md b/app/aws-lsp-codewhisperer-runtimes/README.md
index 977d514bd6..71fbcf0456 100644
--- a/app/aws-lsp-codewhisperer-runtimes/README.md
+++ b/app/aws-lsp-codewhisperer-runtimes/README.md
@@ -76,6 +76,17 @@ The server is managed via scripts/dev-server.js, which ensures:
**NOTE**: Tests are currently disabled for Windows as we currently face issues with automatically shutting down devserver and cleaning resources after tests are executed.
+## Binary Dependencies
+
+### registry.node
+The file `_bundle-assets/registry-js/win32-x64/registry.node` is a precompiled binary downloaded from the [registry-js](https://github.com/desktop/registry-js) project.
+
+- **Current version**: v1.16.1 (released May 21, 2024)
+- **Source**: https://github.com/desktop/registry-js/releases
+- **Purpose**: Provides Windows registry access functionality
+
+**To update**: Download the latest `registry.node` binary for win32-x64 from the registry-js releases page and replace the existing file.
+
#### Tests configuration
- Test settings are defined in `wdio.conf.ts`
- The actual test implementation is in the `test/e2e` folder
diff --git a/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/registry-js/win32-x64/registry.node b/app/aws-lsp-codewhisperer-runtimes/_bundle-assets/registry-js/win32-x64/registry.node
new file mode 100644
index 0000000000000000000000000000000000000000..d7b5091a2cf02da166341422c54ec9520c2c1b31
GIT binary patch
literal 696320
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