Path: blob/main/extensions/copilot/src/extension/byok/vscode-node/ollamaProvider.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';5import { IChatModelInformation } from '../../../platform/endpoint/common/endpointProvider';6import { ILogService } from '../../../platform/log/common/logService';7import { IFetcherService } from '../../../platform/networking/common/fetcherService';8import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';9import { ErrorUtils } from '../../../util/common/errors';10import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';11import { byokKnownModelsToAPIInfo, resolveModelInfo } from '../common/byokProvider';12import { OpenAIEndpoint } from '../node/openAIEndpoint';13import { AbstractOpenAICompatibleLMProvider, LanguageModelChatConfiguration, OpenAICompatibleLanguageModelChatInformation } from './abstractLanguageModelChatProvider';14import { IBYOKStorageService } from './byokStorageService';1516interface OllamaModelInfoAPIResponse {17template: string;18capabilities: string[];19details: { family: string };20remote_model?: string;21model_info?: {22'general.basename': string;23'general.architecture': string;24[other: string]: any;25};26}2728interface OllamaVersionResponse {29version: string;30}3132// Minimum supported Ollama version - versions below this may have compatibility issues33const MINIMUM_OLLAMA_VERSION = '0.6.4';3435export interface OllamaConfig extends LanguageModelChatConfiguration {36url: string;37}3839export class OllamaLMProvider extends AbstractOpenAICompatibleLMProvider<OllamaConfig> {40public static readonly providerName = 'Ollama';41private _modelCache = new Map<string, IChatModelInformation>();4243constructor(44byokStorageService: IBYOKStorageService,45@IFetcherService fetcherService: IFetcherService,46@IConfigurationService configurationService: IConfigurationService,47@ILogService logService: ILogService,48@IInstantiationService instantiationService: IInstantiationService,49@IExperimentationService expService: IExperimentationService50) {51super(52OllamaLMProvider.providerName.toLowerCase(),53OllamaLMProvider.providerName,54undefined,55byokStorageService,56fetcherService,57logService,58instantiationService,59configurationService,60expService61);6263this.migrateConfig();64}6566private async migrateConfig(): Promise<void> {67const baseUrl = this.getBaseUrlFromSettings();68if (!baseUrl) {69return;70}71await this.configureDefaultGroupIfExists(this._name, { url: baseUrl });72await this._configurationService.setConfig(ConfigKey.Deprecated.OllamaEndpoint, undefined);73}7475private getBaseUrlFromSettings(): string | undefined {76if (this._configurationService.isConfigured(ConfigKey.Deprecated.OllamaEndpoint)) {77return this._configurationService.getConfig(ConfigKey.Deprecated.OllamaEndpoint);78}79return undefined;80}8182protected override async getAllModels(silent: boolean, apiKey: string | undefined, config: OllamaConfig | undefined): Promise<OpenAICompatibleLanguageModelChatInformation<OllamaConfig>[]> {83if (!config) {84return [];85}8687const ollamaBaseUrl = config.url;8889try {90// Check Ollama server version before proceeding with model operations91await this._checkOllamaVersion(ollamaBaseUrl);9293const response = await this._fetcherService.fetch(`${ollamaBaseUrl}/api/tags`, { method: 'GET', callSite: 'ollama-tags' });94const models = (await response.json()).models;95this._knownModels = {};96for (const model of models) {97let modelInfo = this._modelCache.get(`${ollamaBaseUrl}/${model.model}`);98if (!modelInfo) {99try {100modelInfo = await this._getOllamaModelInfo(ollamaBaseUrl, model.model);101} catch (e) {102const error = ErrorUtils.fromUnknown(e);103this._logService.error(error, 'ollamaProvider: failed to fetch Ollama model info');104this._logService.debug(`[ollamaProvider] Failed model info fetch for model=${model.model}`);105continue; // Skip this model but continue processing others106}107this._modelCache.set(`${ollamaBaseUrl}/${model.model}`, modelInfo);108}109this._knownModels[modelInfo.id] = {110maxInputTokens: modelInfo.capabilities.limits?.max_prompt_tokens ?? 4096,111maxOutputTokens: modelInfo.capabilities.limits?.max_output_tokens ?? 4096,112name: modelInfo.name,113toolCalling: !!modelInfo.capabilities.supports.tool_calls,114vision: !!modelInfo.capabilities.supports.vision115};116}117118return byokKnownModelsToAPIInfo(this._name, this._knownModels).map(model => ({119...model,120url: ollamaBaseUrl121}));122123} catch (e) {124// Check if this is our version check error and preserve it125if (e instanceof Error && e.message.includes('Ollama server version')) {126throw e;127}128throw new Error('Failed to fetch models from Ollama. Please ensure Ollama is running. If ollama is on another host, please configure the `"github.copilot.chat.byok.ollamaEndpoint"` setting.');129}130}131132protected override getModelsBaseUrl(configuration: OllamaConfig | undefined): string {133return configuration?.url ?? 'http://localhost:11434';134}135136protected override async createOpenAIEndPoint(model: OpenAICompatibleLanguageModelChatInformation<OllamaConfig>): Promise<OpenAIEndpoint> {137const modelInfo = this.getModelInfo(model.id, model.url);138const url = `${model.url}/v1/chat/completions`;139return this._instantiationService.createInstance(OpenAIEndpoint, modelInfo, model.configuration?.apiKey ?? '', url);140}141142private async _getOllamaModelInfo(ollamaBaseUrl: string, modelId: string): Promise<IChatModelInformation> {143const modelInfo = await this._fetchOllamaModelInformation(ollamaBaseUrl, modelId);144const contextWindow = modelInfo?.model_info?.[`${modelInfo.model_info['general.architecture']}.context_length`] ?? 32768;145const outputTokens = contextWindow < 4096 ? Math.floor(contextWindow / 2) : 4096;146const modelCapabilities = {147name: modelInfo?.model_info?.['general.basename'] ?? modelInfo.remote_model ?? modelId,148maxOutputTokens: outputTokens,149maxInputTokens: contextWindow - outputTokens,150vision: modelInfo.capabilities.includes('vision'),151toolCalling: modelInfo.capabilities.includes('tools')152};153154return resolveModelInfo(modelId, this._name, this._knownModels, modelCapabilities);155}156157/**158* Compare version strings to check if current version meets minimum requirements159* @param currentVersion Current Ollama server version160* @returns true if version is supported, false otherwise161*/162private _isVersionSupported(currentVersion: string): boolean {163if (currentVersion === '0.0.0') {164// allow all dev versions through165return true;166}167168// Simple version comparison: split by dots and compare numerically169const currentParts = currentVersion.split('.').map(n => parseInt(n, 10));170const minimumParts = MINIMUM_OLLAMA_VERSION.split('.').map(n => parseInt(n, 10));171172for (let i = 0; i < Math.max(currentParts.length, minimumParts.length); i++) {173const current = currentParts[i] || 0;174const minimum = minimumParts[i] || 0;175176if (current > minimum) {177return true;178}179if (current < minimum) {180return false;181}182}183184return true; // versions are equal185}186187private async _fetchOllamaModelInformation(ollamaBaseUrl: string, modelId: string): Promise<OllamaModelInfoAPIResponse> {188const response = await this._fetcherService.fetch(`${ollamaBaseUrl}/api/show`, {189method: 'POST',190callSite: 'ollama-show',191headers: {192'Content-Type': 'application/json'193},194body: JSON.stringify({ model: modelId })195});196return response.json() as unknown as OllamaModelInfoAPIResponse;197}198/**199* Check if the connected Ollama server version meets the minimum requirements200* @throws Error if version is below minimum or version check fails201*/202private async _checkOllamaVersion(ollamaBaseUrl: string): Promise<void> {203try {204const response = await this._fetcherService.fetch(`${ollamaBaseUrl}/api/version`, { method: 'GET', callSite: 'ollama-version' });205const versionInfo = await response.json() as OllamaVersionResponse;206207if (!this._isVersionSupported(versionInfo.version)) {208throw new Error(209`Ollama server version ${versionInfo.version} is not supported. ` +210`Please upgrade to version ${MINIMUM_OLLAMA_VERSION} or higher. ` +211`Visit https://ollama.ai for upgrade instructions.`212);213}214} catch (e) {215if (e instanceof Error && e.message.includes('Ollama server version')) {216// Re-throw our custom version error217throw e;218}219// If version endpoint fails220throw new Error(221`Unable to verify Ollama server version. Please ensure you have Ollama version ${MINIMUM_OLLAMA_VERSION} or higher installed. ` +222`If you're running an older version, please upgrade from https://ollama.ai`223);224}225}226}227228