Skip to content

Commit 949367e

Browse files
authored
[AXON-1796] Add agent model selection to Rovo Dev chat (#1660)
1 parent 30dd6c9 commit 949367e

24 files changed

+900
-18
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
### [Report an Issue](https://github.com/atlassian/atlascode/issues)
22

3+
4+
## What's new in 4.0.22
5+
6+
### Features
7+
8+
- Rovo Dev: Agent model selection both via /models command, and dedicated drop-down menu
9+
310
## What's new in 4.0.21
411

512
### Bug fixes

src/rovo-dev/client/responseParser.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,5 +700,79 @@ describe('RovoDevResponseParser', () => {
700700
expect(toolReturnEvent.toolCallMessage).toBeDefined();
701701
expect(toolReturnEvent.toolCallMessage.tool_name).toBe('get_weather');
702702
});
703+
704+
describe('models responses', () => {
705+
it('should parse a models response with model change message', () => {
706+
const input =
707+
'event: models\ndata: {"model_name": "GPT-4", "model_id": "gpt-4", "message": "Agent model changed to GPT-4"}\n\n';
708+
709+
const results = Array.from(parser.parse(input));
710+
711+
expect(results).toHaveLength(1);
712+
expect(results[0]).toEqual({
713+
event_kind: 'models',
714+
data: {
715+
model_name: 'GPT-4',
716+
model_id: 'gpt-4',
717+
message: 'Agent model changed to GPT-4',
718+
},
719+
});
720+
});
721+
722+
it('should parse a models response with available models list', () => {
723+
const input = `event: models\ndata: {"models": [{"name": "GPT-4", "model_id": "gpt-4", "description": "Most capable", "credit_multiplier": "1.5"}, {"name": "GPT-3.5", "model_id": "gpt-3.5-turbo", "description": "Fast", "credit_multiplier": "1.0"}]}\n\n`;
724+
725+
const results = Array.from(parser.parse(input));
726+
727+
expect(results).toHaveLength(1);
728+
expect(results[0].event_kind).toBe('models');
729+
const modelsResponse = results[0] as any;
730+
expect(modelsResponse.data.models).toHaveLength(2);
731+
expect(modelsResponse.data.models[0]).toEqual({
732+
name: 'GPT-4',
733+
model_id: 'gpt-4',
734+
description: 'Most capable',
735+
credit_multiplier: '1.5',
736+
});
737+
});
738+
739+
it('should parse models response with empty models array', () => {
740+
const input = 'event: models\ndata: {"models": []}\n\n';
741+
742+
const results = Array.from(parser.parse(input));
743+
744+
expect(results).toHaveLength(1);
745+
expect(results[0].event_kind).toBe('models');
746+
const modelsResponse = results[0] as any;
747+
expect(modelsResponse.data.models).toEqual([]);
748+
});
749+
750+
it('should handle models event as a non-bufferable generic event', () => {
751+
const allResults: RovoDevResponse[] = [];
752+
753+
// Parse a models event
754+
let input = 'event: models\ndata: {"model_id": "gpt-4", "message": "Model changed"}\n\n';
755+
allResults.push(...Array.from(parser.parse(input)));
756+
757+
// Parse another generic event to ensure models is not buffered
758+
input = 'event: status\ndata: {}\n\n';
759+
allResults.push(...Array.from(parser.parse(input)));
760+
761+
expect(allResults).toHaveLength(2);
762+
expect(allResults[0].event_kind).toBe('models');
763+
expect(allResults[1].event_kind).toBe('status');
764+
});
765+
766+
it('should parse models event with both message and models array', () => {
767+
const input = `event: models\ndata: {"message": "Available models", "models": [{"name": "GPT-4", "model_id": "gpt-4", "description": "Test", "credit_multiplier": "1.0"}]}\n\n`;
768+
769+
const results = Array.from(parser.parse(input));
770+
771+
expect(results).toHaveLength(1);
772+
const modelsResponse = results[0] as any;
773+
expect(modelsResponse.data.message).toBe('Available models');
774+
expect(modelsResponse.data.models).toHaveLength(1);
775+
});
776+
});
703777
});
704778
});

src/rovo-dev/client/responseParser.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import {
44
RovoDevClearResponse,
55
RovoDevExceptionResponse,
6+
RovoDevModelsResponse,
67
RovoDevOnCallToolStartResponse,
78
RovoDevParsingError,
89
RovoDevPromptsResponse,
@@ -190,6 +191,7 @@ type RovoDevSingleChunk =
190191
| RovoDevStatusChunk
191192
| RovoDevUsageChunk
192193
| RovoDevPromptsChunk
194+
| RovoDevModelsResponse
193195
| RovoDevCloseChunk
194196
| RovoDevReplayEndChunk
195197
| RovoDevRequestUsageChunk;
@@ -536,6 +538,7 @@ export class RovoDevResponseParser {
536538
case 'status':
537539
case 'usage':
538540
case 'prompts':
541+
case 'models':
539542
return buffer
540543
? generateError(Error(`Rovo Dev parser error: ${chunk.event_kind} seem to be split`))
541544
: chunk;

src/rovo-dev/client/responseParserInterfaces.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,21 @@ export interface RovoDevPromptsResponse {
137137
};
138138
}
139139

140+
export interface RovoDevModelsResponse {
141+
event_kind: 'models';
142+
data: {
143+
model_name?: string;
144+
model_id?: string;
145+
message?: string;
146+
models?: {
147+
name: string;
148+
model_id: string;
149+
description: string;
150+
credit_multiplier: string;
151+
}[];
152+
};
153+
}
154+
140155
export type RovoDevResponse =
141156
| RovoDevParsingError
142157
| RovoDevUserPromptResponse
@@ -152,6 +167,7 @@ export type RovoDevResponse =
152167
| RovoDevStatusResponse
153168
| RovoDevUsageResponse
154169
| RovoDevPromptsResponse
170+
| RovoDevModelsResponse
155171
| RovoDevCloseResponse
156172
| RovoDevReplayEndResponse
157173
| RovoDevIgnoredResponse;

src/rovo-dev/client/rovoDevApiClient.test.ts

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1874,4 +1874,219 @@ describe('RovoDevApiClient', () => {
18741874
);
18751875
});
18761876
});
1877+
1878+
describe('getAgentModel method', () => {
1879+
it('should return current agent model successfully', async () => {
1880+
const mockGetAgentModelResponse = {
1881+
model_name: 'GPT-4',
1882+
model_id: 'gpt-4',
1883+
credit_multiplier: '1.5',
1884+
message: 'Current model is GPT-4',
1885+
};
1886+
const mockResponse = {
1887+
status: 200,
1888+
json: jest.fn().mockResolvedValue(mockGetAgentModelResponse),
1889+
headers: mockStandardResponseHeaders(),
1890+
} as unknown as Response;
1891+
1892+
mockFetch.mockResolvedValue(mockResponse);
1893+
1894+
const result = await client.getAgentModel();
1895+
1896+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:8080/v3/agent-model', {
1897+
method: 'GET',
1898+
headers: {
1899+
accept: 'text/event-stream',
1900+
'Content-Type': 'application/json',
1901+
Authorization: 'Bearer sessionToken',
1902+
},
1903+
body: undefined,
1904+
});
1905+
expect(result).toEqual(mockGetAgentModelResponse);
1906+
expect(result.model_id).toBe('gpt-4');
1907+
expect(result.model_name).toBe('GPT-4');
1908+
expect(result.credit_multiplier).toBe('1.5');
1909+
});
1910+
1911+
it('should throw error when API call fails', async () => {
1912+
const mockResponse = {
1913+
status: 500,
1914+
statusText: 'Internal Server Error',
1915+
headers: mockStandardResponseHeaders(),
1916+
} as Response;
1917+
1918+
mockFetch.mockResolvedValue(mockResponse);
1919+
1920+
await expect(client.getAgentModel()).rejects.toThrow("Failed to fetch '/v3/agent-model API: HTTP 500");
1921+
});
1922+
});
1923+
1924+
describe('setAgentModel method', () => {
1925+
it('should set agent model successfully', async () => {
1926+
const mockSetAgentModelResponse = {
1927+
model_name: 'GPT-4',
1928+
model_id: 'gpt-4',
1929+
message: 'Agent model set to GPT-4',
1930+
};
1931+
const mockResponse = {
1932+
status: 200,
1933+
json: jest.fn().mockResolvedValue(mockSetAgentModelResponse),
1934+
headers: mockStandardResponseHeaders(),
1935+
} as unknown as Response;
1936+
1937+
mockFetch.mockResolvedValue(mockResponse);
1938+
1939+
const result = await client.setAgentModel('gpt-4');
1940+
1941+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:8080/v3/agent-model', {
1942+
method: 'PUT',
1943+
headers: {
1944+
accept: 'text/event-stream',
1945+
'Content-Type': 'application/json',
1946+
Authorization: 'Bearer sessionToken',
1947+
},
1948+
body: JSON.stringify({ model_id: 'gpt-4' }),
1949+
});
1950+
expect(result.model_id).toBe('gpt-4');
1951+
expect(result.message).toBe('Agent model set to GPT-4');
1952+
});
1953+
1954+
it('should handle different model IDs', async () => {
1955+
const mockSetAgentModelResponse = {
1956+
model_name: 'Claude 3',
1957+
model_id: 'claude-3',
1958+
message: 'Agent model set to Claude 3',
1959+
};
1960+
const mockResponse = {
1961+
status: 200,
1962+
json: jest.fn().mockResolvedValue(mockSetAgentModelResponse),
1963+
headers: mockStandardResponseHeaders(),
1964+
} as unknown as Response;
1965+
1966+
mockFetch.mockResolvedValue(mockResponse);
1967+
1968+
const result = await client.setAgentModel('claude-3');
1969+
1970+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:8080/v3/agent-model', {
1971+
method: 'PUT',
1972+
headers: {
1973+
accept: 'text/event-stream',
1974+
'Content-Type': 'application/json',
1975+
Authorization: 'Bearer sessionToken',
1976+
},
1977+
body: JSON.stringify({ model_id: 'claude-3' }),
1978+
});
1979+
expect(result.model_id).toBe('claude-3');
1980+
});
1981+
1982+
it('should throw error when API call fails', async () => {
1983+
const mockResponse = {
1984+
status: 500,
1985+
statusText: 'Internal Server Error',
1986+
headers: mockStandardResponseHeaders(),
1987+
} as Response;
1988+
1989+
mockFetch.mockResolvedValue(mockResponse);
1990+
1991+
await expect(client.setAgentModel('gpt-4')).rejects.toThrow(
1992+
"Failed to fetch '/v3/agent-model API: HTTP 500",
1993+
);
1994+
});
1995+
1996+
it('should throw error when API returns 400 (invalid model)', async () => {
1997+
const mockResponse = {
1998+
status: 400,
1999+
statusText: 'Bad Request',
2000+
headers: mockStandardResponseHeaders(),
2001+
} as Response;
2002+
2003+
mockFetch.mockResolvedValue(mockResponse);
2004+
2005+
await expect(client.setAgentModel('invalid-model')).rejects.toThrow(
2006+
"Failed to fetch '/v3/agent-model API: HTTP 400",
2007+
);
2008+
});
2009+
});
2010+
2011+
describe('getAvailableAgentModels method', () => {
2012+
it('should return list of available agent models successfully', async () => {
2013+
const mockAvailableModelsResponse = {
2014+
models: [
2015+
{
2016+
name: 'GPT-4',
2017+
model_id: 'gpt-4',
2018+
description: 'Most capable model',
2019+
credit_multiplier: '1.5',
2020+
},
2021+
{
2022+
name: 'GPT-3.5 Turbo',
2023+
model_id: 'gpt-3.5-turbo',
2024+
description: 'Fast and efficient',
2025+
credit_multiplier: '1.0',
2026+
},
2027+
{
2028+
name: 'Claude 3',
2029+
model_id: 'claude-3',
2030+
description: 'Anthropic model',
2031+
credit_multiplier: '2.0',
2032+
},
2033+
],
2034+
};
2035+
const mockResponse = {
2036+
status: 200,
2037+
json: jest.fn().mockResolvedValue(mockAvailableModelsResponse),
2038+
headers: mockStandardResponseHeaders(),
2039+
} as unknown as Response;
2040+
2041+
mockFetch.mockResolvedValue(mockResponse);
2042+
2043+
const result = await client.getAvailableAgentModels();
2044+
2045+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:8080/v3/agent-models', {
2046+
method: 'GET',
2047+
headers: {
2048+
accept: 'text/event-stream',
2049+
'Content-Type': 'application/json',
2050+
Authorization: 'Bearer sessionToken',
2051+
},
2052+
body: undefined,
2053+
});
2054+
expect(result).toEqual(mockAvailableModelsResponse);
2055+
expect(result.models).toHaveLength(3);
2056+
expect(result.models[0].model_id).toBe('gpt-4');
2057+
expect(result.models[0].name).toBe('GPT-4');
2058+
expect(result.models[0].credit_multiplier).toBe('1.5');
2059+
});
2060+
2061+
it('should handle empty models list', async () => {
2062+
const mockAvailableModelsResponse = {
2063+
models: [],
2064+
};
2065+
const mockResponse = {
2066+
status: 200,
2067+
json: jest.fn().mockResolvedValue(mockAvailableModelsResponse),
2068+
headers: mockStandardResponseHeaders(),
2069+
} as unknown as Response;
2070+
2071+
mockFetch.mockResolvedValue(mockResponse);
2072+
2073+
const result = await client.getAvailableAgentModels();
2074+
2075+
expect(result.models).toHaveLength(0);
2076+
});
2077+
2078+
it('should throw error when API call fails', async () => {
2079+
const mockResponse = {
2080+
status: 500,
2081+
statusText: 'Internal Server Error',
2082+
headers: mockStandardResponseHeaders(),
2083+
} as Response;
2084+
2085+
mockFetch.mockResolvedValue(mockResponse);
2086+
2087+
await expect(client.getAvailableAgentModels()).rejects.toThrow(
2088+
"Failed to fetch '/v3/agent-models API: HTTP 500",
2089+
);
2090+
});
2091+
});
18772092
});

0 commit comments

Comments
 (0)