Skip to content

Commit 11dd873

Browse files
committed
Adds o3-mini & o1 models
Adds dynamic model loading for GitHub Models Adds dynamic model loading for Hugging Face Updates model ordering in the picker
1 parent 945cc86 commit 11dd873

11 files changed

+185
-222
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
1010

1111
- Adds and expands AI model support for GitLens' AI features
1212
- Adds DeepSeek V3 and R1 models — closes [#3943](https://github.com/gitkraken/vscode-gitlens/issues/3943)
13+
- Adds o3-mini and o1 OpenAI models
1314
- Adds the latest experimental Gemini 2.0 Flash Thinking model
14-
- Adds a `gitlens.ai.modelOptions.temperature` setting to specify the temperature (randomness) for AI models
15+
- Adds dynamic model loading for GitHub Models and HuggingFace models
16+
- Adds a `gitlens.ai.modelOptions.temperature` setting to specify the temperature (randomness) for AI models that support it
1517
- Adds a _Switch Model_ button to the AI confirmation prompts
1618

1719
### Changed

docs/telemetry-events.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
'failed.error': string,
115115
'failed.reason': 'user-declined' | 'user-cancelled' | 'error',
116116
'input.length': number,
117-
'model.id': 'claude-3-5-sonnet-latest' | 'claude-3-5-sonnet-20241022' | 'claude-3-5-sonnet-20240620' | 'claude-3-5-haiku-20241022' | 'claude-3-5-haiku-latest' | 'claude-3-opus-latest' | 'claude-3-opus-20240229' | 'claude-3-sonnet-20240229' | 'claude-3-haiku-20240307' | 'claude-2.1' | 'deepseek-chat' | 'deepseek-reasoner' | 'gemini-2.0-flash-exp' | 'gemini-2.0-flash-thinking-exp-01-21' | 'gemini-exp-1206' | 'gemini-exp-1121' | 'gemini-1.5-pro-latest' | 'gemini-1.5-flash-latest' | 'gemini-1.5-flash-8b' | 'gemini-1.0-pro' | 'gpt-4o' | 'gpt-4o-mini' | 'o1-preview' | 'o1-mini' | 'Phi-3.5-MoE-instruct' | 'Phi-3.5-mini-instruct' | 'AI21-Jamba-1.5-Large' | 'AI21-Jamba-1.5-Mini' | 'meta-llama/Llama-3.2-11B-Vision-Instruct' | 'Qwen/Qwen2.5-72B-Instruct' | 'NousResearch/Hermes-3-Llama-3.1-8B' | 'mistralai/Mistral-Nemo-Instruct-2407' | 'microsoft/Phi-3.5-mini-instruct' | 'o1-preview-2024-09-12' | 'o1-mini-2024-09-12' | 'gpt-4o-2024-08-06' | 'gpt-4o-2024-05-13' | 'chatgpt-4o-latest' | 'gpt-4o-mini-2024-07-18' | 'gpt-4-turbo' | 'gpt-4-turbo-2024-04-09' | 'gpt-4-turbo-preview' | 'gpt-4-0125-preview' | 'gpt-4-1106-preview' | 'gpt-4' | 'gpt-4-0613' | 'gpt-4-32k' | 'gpt-4-32k-0613' | 'gpt-3.5-turbo' | 'gpt-3.5-turbo-0125' | 'gpt-3.5-turbo-1106' | 'gpt-3.5-turbo-16k' | `${string}:${string}` | 'grok-beta',
117+
'model.id': string,
118118
'model.provider.id': 'anthropic' | 'deepseek' | 'gemini' | 'github' | 'huggingface' | 'openai' | 'vscode' | 'xai',
119119
'model.provider.name': string,
120120
'output.length': number,
@@ -133,7 +133,7 @@
133133
'failed.error': string,
134134
'failed.reason': 'user-declined' | 'user-cancelled' | 'error',
135135
'input.length': number,
136-
'model.id': 'claude-3-5-sonnet-latest' | 'claude-3-5-sonnet-20241022' | 'claude-3-5-sonnet-20240620' | 'claude-3-5-haiku-20241022' | 'claude-3-5-haiku-latest' | 'claude-3-opus-latest' | 'claude-3-opus-20240229' | 'claude-3-sonnet-20240229' | 'claude-3-haiku-20240307' | 'claude-2.1' | 'deepseek-chat' | 'deepseek-reasoner' | 'gemini-2.0-flash-exp' | 'gemini-2.0-flash-thinking-exp-01-21' | 'gemini-exp-1206' | 'gemini-exp-1121' | 'gemini-1.5-pro-latest' | 'gemini-1.5-flash-latest' | 'gemini-1.5-flash-8b' | 'gemini-1.0-pro' | 'gpt-4o' | 'gpt-4o-mini' | 'o1-preview' | 'o1-mini' | 'Phi-3.5-MoE-instruct' | 'Phi-3.5-mini-instruct' | 'AI21-Jamba-1.5-Large' | 'AI21-Jamba-1.5-Mini' | 'meta-llama/Llama-3.2-11B-Vision-Instruct' | 'Qwen/Qwen2.5-72B-Instruct' | 'NousResearch/Hermes-3-Llama-3.1-8B' | 'mistralai/Mistral-Nemo-Instruct-2407' | 'microsoft/Phi-3.5-mini-instruct' | 'o1-preview-2024-09-12' | 'o1-mini-2024-09-12' | 'gpt-4o-2024-08-06' | 'gpt-4o-2024-05-13' | 'chatgpt-4o-latest' | 'gpt-4o-mini-2024-07-18' | 'gpt-4-turbo' | 'gpt-4-turbo-2024-04-09' | 'gpt-4-turbo-preview' | 'gpt-4-0125-preview' | 'gpt-4-1106-preview' | 'gpt-4' | 'gpt-4-0613' | 'gpt-4-32k' | 'gpt-4-32k-0613' | 'gpt-3.5-turbo' | 'gpt-3.5-turbo-0125' | 'gpt-3.5-turbo-1106' | 'gpt-3.5-turbo-16k' | `${string}:${string}` | 'grok-beta',
136+
'model.id': string,
137137
'model.provider.id': 'anthropic' | 'deepseek' | 'gemini' | 'github' | 'huggingface' | 'openai' | 'vscode' | 'xai',
138138
'model.provider.name': string,
139139
'output.length': number,
@@ -151,7 +151,7 @@ or
151151
'failed.error': string,
152152
'failed.reason': 'user-declined' | 'user-cancelled' | 'error',
153153
'input.length': number,
154-
'model.id': 'claude-3-5-sonnet-latest' | 'claude-3-5-sonnet-20241022' | 'claude-3-5-sonnet-20240620' | 'claude-3-5-haiku-20241022' | 'claude-3-5-haiku-latest' | 'claude-3-opus-latest' | 'claude-3-opus-20240229' | 'claude-3-sonnet-20240229' | 'claude-3-haiku-20240307' | 'claude-2.1' | 'deepseek-chat' | 'deepseek-reasoner' | 'gemini-2.0-flash-exp' | 'gemini-2.0-flash-thinking-exp-01-21' | 'gemini-exp-1206' | 'gemini-exp-1121' | 'gemini-1.5-pro-latest' | 'gemini-1.5-flash-latest' | 'gemini-1.5-flash-8b' | 'gemini-1.0-pro' | 'gpt-4o' | 'gpt-4o-mini' | 'o1-preview' | 'o1-mini' | 'Phi-3.5-MoE-instruct' | 'Phi-3.5-mini-instruct' | 'AI21-Jamba-1.5-Large' | 'AI21-Jamba-1.5-Mini' | 'meta-llama/Llama-3.2-11B-Vision-Instruct' | 'Qwen/Qwen2.5-72B-Instruct' | 'NousResearch/Hermes-3-Llama-3.1-8B' | 'mistralai/Mistral-Nemo-Instruct-2407' | 'microsoft/Phi-3.5-mini-instruct' | 'o1-preview-2024-09-12' | 'o1-mini-2024-09-12' | 'gpt-4o-2024-08-06' | 'gpt-4o-2024-05-13' | 'chatgpt-4o-latest' | 'gpt-4o-mini-2024-07-18' | 'gpt-4-turbo' | 'gpt-4-turbo-2024-04-09' | 'gpt-4-turbo-preview' | 'gpt-4-0125-preview' | 'gpt-4-1106-preview' | 'gpt-4' | 'gpt-4-0613' | 'gpt-4-32k' | 'gpt-4-32k-0613' | 'gpt-3.5-turbo' | 'gpt-3.5-turbo-0125' | 'gpt-3.5-turbo-1106' | 'gpt-3.5-turbo-16k' | `${string}:${string}` | 'grok-beta',
154+
'model.id': string,
155155
'model.provider.id': 'anthropic' | 'deepseek' | 'gemini' | 'github' | 'huggingface' | 'openai' | 'vscode' | 'xai',
156156
'model.provider.name': string,
157157
'output.length': number,

src/ai/aiProviderService.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { CancellationToken, Disposable, MessageItem, ProgressOptions, QuickInputButton } from 'vscode';
22
import { env, ThemeIcon, Uri, window } from 'vscode';
3-
import type { AIModels, AIProviders, SupportedAIModels, VSCodeAIModels } from '../constants.ai';
3+
import type { AIProviders, SupportedAIModels, VSCodeAIModels } from '../constants.ai';
44
import type { AIGenerateDraftEventData, Sources, TelemetryEvents } from '../constants.telemetry';
55
import type { Container } from '../container';
66
import { CancellationError } from '../errors';
@@ -33,10 +33,7 @@ export interface AIResult {
3333
body: string;
3434
}
3535

36-
export interface AIModel<
37-
Provider extends AIProviders = AIProviders,
38-
Model extends AIModels<Provider> = AIModels<Provider>,
39-
> {
36+
export interface AIModel<Provider extends AIProviders = AIProviders, Model extends string = string> {
4037
readonly id: Model;
4138
readonly name: string;
4239
readonly maxTokens: { input: number; output: number };
@@ -55,38 +52,39 @@ interface AIProviderConstructor<Provider extends AIProviders = AIProviders> {
5552
new (container: Container): AIProvider<Provider>;
5653
}
5754

55+
// Order matters for sorting the picker
5856
const _supportedProviderTypes = new Map<AIProviders, AIProviderConstructor>([
5957
...(supportedInVSCodeVersion('language-models') ? [['vscode', VSCodeAIProvider]] : ([] as any)),
6058
['openai', OpenAIProvider],
6159
['anthropic', AnthropicProvider],
62-
['deepseek', DeepSeekProvider],
6360
['gemini', GeminiProvider],
61+
['deepseek', DeepSeekProvider],
62+
['xai', xAIProvider],
6463
['github', GitHubModelsProvider],
6564
['huggingface', HuggingFaceProvider],
66-
['xai', xAIProvider],
6765
]);
6866

6967
export interface AIProvider<Provider extends AIProviders = AIProviders> extends Disposable {
7068
readonly id: Provider;
7169
readonly name: string;
7270

73-
getModels(): Promise<readonly AIModel<Provider, AIModels<Provider>>[]>;
71+
getModels(): Promise<readonly AIModel<Provider>[]>;
7472

7573
explainChanges(
76-
model: AIModel<Provider, AIModels<Provider>>,
74+
model: AIModel<Provider>,
7775
message: string,
7876
diff: string,
7977
reporting: TelemetryEvents['ai/explain'],
8078
options?: { cancellation?: CancellationToken },
8179
): Promise<string | undefined>;
8280
generateCommitMessage(
83-
model: AIModel<Provider, AIModels<Provider>>,
81+
model: AIModel<Provider>,
8482
diff: string,
8583
reporting: TelemetryEvents['ai/generate'],
8684
options?: { cancellation?: CancellationToken; context?: string },
8785
): Promise<string | undefined>;
8886
generateDraftMessage(
89-
model: AIModel<Provider, AIModels<Provider>>,
87+
model: AIModel<Provider>,
9088
diff: string,
9189
reporting: TelemetryEvents['ai/generate'],
9290
options?: { cancellation?: CancellationToken; context?: string; codeSuggestion?: boolean },
@@ -107,10 +105,10 @@ export class AIProviderService implements Disposable {
107105
return this._provider?.id;
108106
}
109107

110-
private getConfiguredModel(): { provider: AIProviders; model: AIModels } | undefined {
108+
private getConfiguredModel(): { provider: AIProviders; model: string } | undefined {
111109
const qualifiedModelId = configuration.get('ai.model') ?? undefined;
112110
if (qualifiedModelId != null) {
113-
let [providerId, modelId] = qualifiedModelId.split(':') as [AIProviders, AIModels];
111+
let [providerId, modelId] = qualifiedModelId.split(':') as [AIProviders, string];
114112
if (providerId != null && this.supports(providerId)) {
115113
if (modelId != null) {
116114
return { provider: providerId, model: modelId };
@@ -150,10 +148,10 @@ export class AIProviderService implements Disposable {
150148
}
151149

152150
private getOrUpdateModel(model: AIModel): Promise<AIModel | undefined>;
153-
private getOrUpdateModel<T extends AIProviders>(providerId: T, modelId: AIModels<T>): Promise<AIModel | undefined>;
151+
private getOrUpdateModel<T extends AIProviders>(providerId: T, modelId: string): Promise<AIModel | undefined>;
154152
private async getOrUpdateModel(
155153
modelOrProviderId: AIModel | AIProviders,
156-
modelId?: AIModels,
154+
modelId?: string,
157155
): Promise<AIModel | undefined> {
158156
let providerId: AIProviders;
159157
let model: AIModel | undefined;
@@ -552,7 +550,7 @@ export class AIProviderService implements Disposable {
552550

553551
async function confirmAIProviderToS<Provider extends AIProviders>(
554552
service: AIProviderService,
555-
model: AIModel<Provider, AIModels<Provider>>,
553+
model: AIModel<Provider>,
556554
storage: Storage,
557555
): Promise<boolean> {
558556
const confirmed =
@@ -596,9 +594,9 @@ async function confirmAIProviderToS<Provider extends AIProviders>(
596594
return false;
597595
}
598596

599-
export function getMaxCharacters(model: AIModel, outputLength: number): number {
597+
export function getMaxCharacters(model: AIModel, outputLength: number, overrideInputTokens?: number): number {
600598
const tokensPerCharacter = 3.1;
601-
const max = model.maxTokens.input * tokensPerCharacter - outputLength / tokensPerCharacter;
599+
const max = (overrideInputTokens ?? model.maxTokens.input) * tokensPerCharacter - outputLength / tokensPerCharacter;
602600
return Math.floor(max - max * 0.1);
603601
}
604602

src/ai/githubModelsProvider.ts

Lines changed: 63 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,12 @@
1+
import type { Response } from '@env/fetch';
2+
import { fetch } from '@env/fetch';
13
import type { AIModel } from './aiProviderService';
4+
import { getMaxCharacters } from './aiProviderService';
25
import { OpenAICompatibleProvider } from './openAICompatibleProvider';
36

47
const provider = { id: 'github', name: 'GitHub Models' } as const;
58

69
type GitHubModelsModel = AIModel<typeof provider.id>;
7-
const models: GitHubModelsModel[] = [
8-
{
9-
id: 'o1-preview',
10-
name: 'o1 preview',
11-
maxTokens: { input: 128000, output: 32768 },
12-
provider: provider,
13-
},
14-
{
15-
id: 'o1-mini',
16-
name: 'o1 mini',
17-
maxTokens: { input: 128000, output: 65536 },
18-
provider: provider,
19-
},
20-
{
21-
id: 'gpt-4o',
22-
name: 'GPT-4o',
23-
maxTokens: { input: 128000, output: 16384 },
24-
provider: provider,
25-
},
26-
{
27-
id: 'gpt-4o-mini',
28-
name: 'GPT-4o mini',
29-
maxTokens: { input: 128000, output: 16384 },
30-
provider: provider,
31-
},
32-
{
33-
id: 'Phi-3.5-MoE-instruct',
34-
name: 'Phi 3.5 MoE',
35-
maxTokens: { input: 134144, output: 4096 },
36-
provider: provider,
37-
},
38-
{
39-
id: 'Phi-3.5-mini-instruct',
40-
name: 'Phi 3.5 mini',
41-
maxTokens: { input: 134144, output: 4096 },
42-
provider: provider,
43-
},
44-
{
45-
id: 'AI21-Jamba-1.5-Large',
46-
name: 'Jamba 1.5 Large',
47-
maxTokens: { input: 268288, output: 4096 },
48-
provider: provider,
49-
},
50-
{
51-
id: 'AI21-Jamba-1.5-Mini',
52-
name: 'Jamba 1.5 Mini',
53-
maxTokens: { input: 268288, output: 4096 },
54-
provider: provider,
55-
},
56-
];
5710

5811
export class GitHubModelsProvider extends OpenAICompatibleProvider<typeof provider.id> {
5912
readonly id = provider.id;
@@ -63,11 +16,69 @@ export class GitHubModelsProvider extends OpenAICompatibleProvider<typeof provid
6316
keyValidator: /(?:ghp-)?[a-zA-Z0-9]{32,}/,
6417
};
6518

66-
getModels(): Promise<readonly AIModel<typeof provider.id>[]> {
67-
return Promise.resolve(models);
19+
async getModels(): Promise<readonly AIModel<typeof provider.id>[]> {
20+
const rsp = await fetch('https://github.com/marketplace?category=All&task=chat-completion&type=models', {
21+
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
22+
});
23+
24+
interface ModelsResponseResult {
25+
type: 'model';
26+
task: 'chat-completion';
27+
28+
id: string;
29+
name: string;
30+
friendly_name: string;
31+
publisher: string;
32+
model_family: string;
33+
max_input_tokens: number;
34+
max_output_tokens: number;
35+
}
36+
37+
interface ModelsResponse {
38+
results: ModelsResponseResult[];
39+
}
40+
41+
const result: ModelsResponse = await rsp.json();
42+
43+
const models = result.results.map(
44+
r =>
45+
({
46+
id: r.name as any,
47+
name: r.friendly_name,
48+
maxTokens: { input: r.max_input_tokens, output: r.max_output_tokens },
49+
provider: provider,
50+
temperature: null,
51+
}) satisfies GitHubModelsModel,
52+
);
53+
54+
return models;
6855
}
6956

7057
protected getUrl(_model: AIModel<typeof provider.id>): string {
7158
return 'https://models.inference.ai.azure.com/chat/completions';
7259
}
60+
61+
override async handleFetchFailure(
62+
rsp: Response,
63+
model: AIModel<typeof provider.id>,
64+
retries: number,
65+
maxCodeCharacters: number,
66+
): Promise<{ retry: boolean; maxCodeCharacters: number }> {
67+
if (rsp.status !== 404 && rsp.status !== 429) {
68+
let json;
69+
try {
70+
json = (await rsp.json()) as { error?: { code: string; message: string } } | undefined;
71+
} catch {}
72+
73+
if (retries < 2 && json?.error?.code === 'tokens_limit_reached') {
74+
const match = /Max size: (\d+) tokens/.exec(json?.error?.message);
75+
if (match?.[1] != null) {
76+
maxCodeCharacters = getMaxCharacters(model, 2600, parseInt(match[1], 10));
77+
return { retry: true, maxCodeCharacters: maxCodeCharacters };
78+
}
79+
}
80+
}
81+
82+
return super.handleFetchFailure(rsp, model, retries, maxCodeCharacters);
83+
}
7384
}

src/ai/huggingFaceProvider.ts

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,10 @@
1+
import { fetch } from '@env/fetch';
12
import type { AIModel } from './aiProviderService';
23
import { OpenAICompatibleProvider } from './openAICompatibleProvider';
34

45
const provider = { id: 'huggingface', name: 'Hugging Face' } as const;
56

67
type HuggingFaceModel = AIModel<typeof provider.id>;
7-
const models: HuggingFaceModel[] = [
8-
{
9-
id: 'meta-llama/Llama-3.2-11B-Vision-Instruct',
10-
name: 'Meta Llama 3.2 11B Vision',
11-
maxTokens: { input: 131072, output: 4096 },
12-
provider: provider,
13-
},
14-
{
15-
id: 'Qwen/Qwen2.5-72B-Instruct',
16-
name: 'Qwen 2.5 72B',
17-
maxTokens: { input: 131072, output: 4096 },
18-
provider: provider,
19-
},
20-
{
21-
id: 'NousResearch/Hermes-3-Llama-3.1-8B',
22-
name: 'Nous Research Hermes 3',
23-
maxTokens: { input: 131072, output: 4096 },
24-
provider: provider,
25-
},
26-
{
27-
id: 'mistralai/Mistral-Nemo-Instruct-2407',
28-
name: 'Mistral Nemo',
29-
maxTokens: { input: 131072, output: 4096 },
30-
provider: provider,
31-
},
32-
];
338

349
export class HuggingFaceProvider extends OpenAICompatibleProvider<typeof provider.id> {
3510
readonly id = provider.id;
@@ -39,8 +14,44 @@ export class HuggingFaceProvider extends OpenAICompatibleProvider<typeof provide
3914
keyValidator: /(?:hf_)?[a-zA-Z0-9]{32,}/,
4015
};
4116

42-
getModels(): Promise<readonly AIModel<typeof provider.id>[]> {
43-
return Promise.resolve(models);
17+
async getModels(): Promise<readonly AIModel<typeof provider.id>[]> {
18+
const apiKey = await this.getApiKey();
19+
20+
const query = new URLSearchParams({
21+
filter: 'text-generation,conversational',
22+
inference: 'warm',
23+
sort: 'trendingScore',
24+
limit: '30',
25+
});
26+
const rsp = await fetch(`https://huggingface.co/api/models?${query.toString()}`, {
27+
headers: {
28+
Accept: 'application/json',
29+
'Content-Type': 'application/json',
30+
Authorization: apiKey != null ? `Bearer ${apiKey}` : undefined!,
31+
},
32+
method: 'GET',
33+
});
34+
35+
interface ModelsResponseResult {
36+
id: string;
37+
}
38+
39+
type ModelsResponse = ModelsResponseResult[];
40+
41+
const results: ModelsResponse = await rsp.json();
42+
43+
const models = results.map(
44+
r =>
45+
({
46+
id: r.id,
47+
name: r.id.split('/').pop()!,
48+
maxTokens: { input: 4096, output: 4096 },
49+
provider: provider,
50+
temperature: null,
51+
}) satisfies HuggingFaceModel,
52+
);
53+
54+
return models;
4455
}
4556

4657
protected getUrl(model: AIModel<typeof provider.id>): string {

0 commit comments

Comments
 (0)