Skip to content

Commit 3ad0291

Browse files
committed
Adds Mistral support
1 parent 3a9158b commit 3ad0291

File tree

4 files changed

+185
-8
lines changed

4 files changed

+185
-8
lines changed

docs/telemetry-events.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
'failed.reason': 'user-declined' | 'user-cancelled' | 'error',
124124
'input.length': number,
125125
'model.id': string,
126-
'model.provider.id': 'anthropic' | 'azure' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'ollama' | 'openai' | 'openaicompatible' | 'openrouter' | 'vscode' | 'xai',
126+
'model.provider.id': 'anthropic' | 'azure' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'mistral' | 'ollama' | 'openai' | 'openaicompatible' | 'openrouter' | 'vscode' | 'xai',
127127
'model.provider.name': string,
128128
'output.length': number,
129129
'retry.count': number,
@@ -155,7 +155,7 @@
155155
'failed.reason': 'user-declined' | 'user-cancelled' | 'error',
156156
'input.length': number,
157157
'model.id': string,
158-
'model.provider.id': 'anthropic' | 'azure' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'ollama' | 'openai' | 'openaicompatible' | 'openrouter' | 'vscode' | 'xai',
158+
'model.provider.id': 'anthropic' | 'azure' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'mistral' | 'ollama' | 'openai' | 'openaicompatible' | 'openrouter' | 'vscode' | 'xai',
159159
'model.provider.name': string,
160160
'output.length': number,
161161
'retry.count': number,
@@ -186,7 +186,7 @@ or
186186
'failed.reason': 'user-declined' | 'user-cancelled' | 'error',
187187
'input.length': number,
188188
'model.id': string,
189-
'model.provider.id': 'anthropic' | 'azure' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'ollama' | 'openai' | 'openaicompatible' | 'openrouter' | 'vscode' | 'xai',
189+
'model.provider.id': 'anthropic' | 'azure' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'mistral' | 'ollama' | 'openai' | 'openaicompatible' | 'openrouter' | 'vscode' | 'xai',
190190
'model.provider.name': string,
191191
'output.length': number,
192192
'retry.count': number,
@@ -216,7 +216,7 @@ or
216216
'failed.reason': 'user-declined' | 'user-cancelled' | 'error',
217217
'input.length': number,
218218
'model.id': string,
219-
'model.provider.id': 'anthropic' | 'azure' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'ollama' | 'openai' | 'openaicompatible' | 'openrouter' | 'vscode' | 'xai',
219+
'model.provider.id': 'anthropic' | 'azure' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'mistral' | 'ollama' | 'openai' | 'openaicompatible' | 'openrouter' | 'vscode' | 'xai',
220220
'model.provider.name': string,
221221
'output.length': number,
222222
'retry.count': number,
@@ -246,7 +246,7 @@ or
246246
'failed.reason': 'user-declined' | 'user-cancelled' | 'error',
247247
'input.length': number,
248248
'model.id': string,
249-
'model.provider.id': 'anthropic' | 'azure' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'ollama' | 'openai' | 'openaicompatible' | 'openrouter' | 'vscode' | 'xai',
249+
'model.provider.id': 'anthropic' | 'azure' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'mistral' | 'ollama' | 'openai' | 'openaicompatible' | 'openrouter' | 'vscode' | 'xai',
250250
'model.provider.name': string,
251251
'output.length': number,
252252
'retry.count': number,
@@ -276,7 +276,7 @@ or
276276
'failed.reason': 'user-declined' | 'user-cancelled' | 'error',
277277
'input.length': number,
278278
'model.id': string,
279-
'model.provider.id': 'anthropic' | 'azure' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'ollama' | 'openai' | 'openaicompatible' | 'openrouter' | 'vscode' | 'xai',
279+
'model.provider.id': 'anthropic' | 'azure' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'mistral' | 'ollama' | 'openai' | 'openaicompatible' | 'openrouter' | 'vscode' | 'xai',
280280
'model.provider.name': string,
281281
'output.length': number,
282282
'retry.count': number,
@@ -306,7 +306,7 @@ or
306306
'failed.reason': 'user-declined' | 'user-cancelled' | 'error',
307307
'input.length': number,
308308
'model.id': string,
309-
'model.provider.id': 'anthropic' | 'azure' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'ollama' | 'openai' | 'openaicompatible' | 'openrouter' | 'vscode' | 'xai',
309+
'model.provider.id': 'anthropic' | 'azure' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'mistral' | 'ollama' | 'openai' | 'openaicompatible' | 'openrouter' | 'vscode' | 'xai',
310310
'model.provider.name': string,
311311
'output.length': number,
312312
'retry.count': number,
@@ -329,7 +329,7 @@ or
329329
```typescript
330330
{
331331
'model.id': string,
332-
'model.provider.id': 'anthropic' | 'azure' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'ollama' | 'openai' | 'openaicompatible' | 'openrouter' | 'vscode' | 'xai',
332+
'model.provider.id': 'anthropic' | 'azure' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'mistral' | 'ollama' | 'openai' | 'openaicompatible' | 'openrouter' | 'vscode' | 'xai',
333333
'model.provider.name': string
334334
}
335335
```

src/constants.ai.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type AIProviders =
88
| 'github'
99
| 'gitkraken'
1010
| 'huggingface'
11+
| 'mistral'
1112
| 'ollama'
1213
| 'openai'
1314
| 'openaicompatible'
@@ -69,6 +70,13 @@ export const geminiProviderDescriptor: AIProviderDescriptor<'gemini'> = {
6970
requiresAccount: true,
7071
requiresUserKey: true,
7172
} as const;
73+
export const mistralProviderDescriptor: AIProviderDescriptor<'mistral'> = {
74+
id: 'mistral',
75+
name: 'Mistral',
76+
primary: false,
77+
requiresAccount: true,
78+
requiresUserKey: true,
79+
} as const;
7280
export const deepSeekProviderDescriptor: AIProviderDescriptor<'deepseek'> = {
7381
id: 'deepseek',
7482
name: 'DeepSeek',

src/plus/ai/aiProviderService.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
githubProviderDescriptor,
1010
gitKrakenProviderDescriptor,
1111
huggingFaceProviderDescriptor,
12+
mistralProviderDescriptor,
1213
ollamaProviderDescriptor,
1314
openAICompatibleProviderDescriptor,
1415
openAIProviderDescriptor,
@@ -168,6 +169,13 @@ const supportedAIProviders = new Map<AIProviders, AIProviderDescriptorWithType>(
168169
type: lazy(async () => (await import(/* webpackChunkName: "ai" */ './azureProvider')).AzureProvider),
169170
},
170171
],
172+
[
173+
'mistral',
174+
{
175+
...mistralProviderDescriptor,
176+
type: lazy(async () => (await import(/* webpackChunkName: "ai" */ './mistralProvider')).MistralProvider),
177+
},
178+
],
171179
[
172180
'openaicompatible',
173181
{

src/plus/ai/mistralProvider.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import type { CancellationToken } from 'vscode';
2+
import type { Response } from '@env/fetch';
3+
import { mistralProviderDescriptor as provider } from '../../constants.ai';
4+
import { AIError, AIErrorReason } from '../../errors';
5+
import type { AIActionType, AIModel } from './models/model';
6+
import { OpenAICompatibleProviderBase } from './openAICompatibleProviderBase';
7+
8+
type MistralModel = AIModel<typeof provider.id>;
9+
const models: MistralModel[] = [
10+
{
11+
id: 'mistral-medium-latest',
12+
name: 'Mistral Medium',
13+
maxTokens: { input: 131072, output: 4096 },
14+
provider: provider,
15+
},
16+
{
17+
id: 'mistral-medium-2505',
18+
name: 'Mistral Medium',
19+
maxTokens: { input: 131072, output: 4096 },
20+
provider: provider,
21+
hidden: true,
22+
},
23+
{
24+
id: 'codestral-latest',
25+
name: 'Codestral',
26+
maxTokens: { input: 262144, output: 4096 },
27+
provider: provider,
28+
default: true,
29+
},
30+
{
31+
id: 'codestral-2501',
32+
name: 'Codestral',
33+
maxTokens: { input: 262144, output: 4096 },
34+
provider: provider,
35+
hidden: true,
36+
},
37+
{
38+
id: 'mistral-large-latest',
39+
name: 'Mistral Large',
40+
maxTokens: { input: 131072, output: 4096 },
41+
provider: provider,
42+
},
43+
{
44+
id: 'mistral-large-2411',
45+
name: 'Mistral Large',
46+
maxTokens: { input: 131072, output: 4096 },
47+
provider: provider,
48+
hidden: true,
49+
},
50+
{
51+
id: 'devstral-small-latest',
52+
name: 'Devstral Small',
53+
maxTokens: { input: 131072, output: 4096 },
54+
provider: provider,
55+
},
56+
{
57+
id: 'devstral-small-2505',
58+
name: 'Devstral Small',
59+
maxTokens: { input: 131072, output: 4096 },
60+
provider: provider,
61+
hidden: true,
62+
},
63+
{
64+
id: 'mistral-small-latest',
65+
name: 'Mistral Small',
66+
maxTokens: { input: 131072, output: 4096 },
67+
provider: provider,
68+
},
69+
{
70+
id: 'mistral-small-2503',
71+
name: 'Mistral Small',
72+
maxTokens: { input: 131072, output: 4096 },
73+
provider: provider,
74+
hidden: true,
75+
},
76+
];
77+
78+
export class MistralProvider extends OpenAICompatibleProviderBase<typeof provider.id> {
79+
readonly id = provider.id;
80+
readonly name = provider.name;
81+
protected readonly descriptor = provider;
82+
protected readonly config = {
83+
keyUrl: 'https://console.mistral.ai/api-keys',
84+
keyValidator: /^[a-zA-Z0-9]{32}$/,
85+
};
86+
87+
getModels(): Promise<readonly AIModel<typeof provider.id>[]> {
88+
return Promise.resolve(models);
89+
}
90+
91+
protected getUrl(_model: AIModel<typeof provider.id>): string {
92+
return 'https://api.mistral.ai/v1/chat/completions';
93+
}
94+
95+
protected override fetchCore<TAction extends AIActionType>(
96+
action: TAction,
97+
model: AIModel<typeof provider.id>,
98+
apiKey: string,
99+
request: object,
100+
cancellation: CancellationToken | undefined,
101+
): Promise<Response> {
102+
if ('max_completion_tokens' in request) {
103+
const { max_completion_tokens: max, ...rest } = request;
104+
request = max ? { max_tokens: max, ...rest } : rest;
105+
}
106+
return super.fetchCore(action, model, apiKey, request, cancellation);
107+
}
108+
109+
protected override async handleFetchFailure<TAction extends AIActionType>(
110+
rsp: Response,
111+
action: TAction,
112+
model: AIModel<typeof provider.id>,
113+
retries: number,
114+
maxInputTokens: number,
115+
): Promise<{ retry: true; maxInputTokens: number }> {
116+
if (rsp.status !== 404 && rsp.status !== 429) {
117+
let json;
118+
try {
119+
json = (await rsp.json()) as MistralError | undefined;
120+
} catch {}
121+
122+
debugger;
123+
124+
const message = `${json?.type}: ${json?.message?.detail
125+
?.map(d => `${d.msg} (${d.type} @ ${d.loc.join(',')})`)
126+
.join(', ')}`;
127+
128+
if (json?.type === 'invalid_request_error') {
129+
if (message?.includes('prompt is too long')) {
130+
if (retries < 2) {
131+
return { retry: true, maxInputTokens: maxInputTokens - 200 * (retries || 1) };
132+
}
133+
134+
throw new AIError(
135+
AIErrorReason.RequestTooLarge,
136+
new Error(`(${this.name}) ${rsp.status}: ${message || rsp.statusText}`),
137+
);
138+
}
139+
}
140+
141+
throw new Error(`(${this.name}) ${rsp.status}: ${message || rsp.statusText}`);
142+
}
143+
144+
return super.handleFetchFailure(rsp, action, model, retries, maxInputTokens);
145+
}
146+
}
147+
148+
interface MistralError {
149+
object: 'error';
150+
type: 'invalid_request_error' | string;
151+
message: {
152+
detail: {
153+
type: string;
154+
msg: string;
155+
loc: string[];
156+
input: number;
157+
}[];
158+
};
159+
code: unknown | null;
160+
param: unknown | null;
161+
}

0 commit comments

Comments
 (0)