diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ApiVersionId.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ApiVersionId.kt deleted file mode 100644 index cd4223b175a1..000000000000 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ApiVersionId.kt +++ /dev/null @@ -1,5 +0,0 @@ -@file:Suppress("FunctionName", "ClassName", "unused", "EnumEntryName", "UnusedImport") -package com.sourcegraph.cody.agent.protocol_generated; - -typealias ApiVersionId = String // One of: - diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Constants.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Constants.kt index 82ab79c03755..6e078238b6ba 100644 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Constants.kt +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/Constants.kt @@ -84,6 +84,7 @@ object Constants { const val tree = "tree" const val `tree-sitter` = "tree-sitter" const val unified = "unified" + const val unknown = "unknown" const val use = "use" const val user = "user" const val waitlist = "waitlist" diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ModelId.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ModelId.kt deleted file mode 100644 index 546c974b83c0..000000000000 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ModelId.kt +++ /dev/null @@ -1,5 +0,0 @@ -@file:Suppress("FunctionName", "ClassName", "unused", "EnumEntryName", "UnusedImport") -package com.sourcegraph.cody.agent.protocol_generated; - -typealias ModelId = String // One of: - diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ModelRef.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ModelRef.kt index 1f794351e605..1260cdb57f48 100644 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ModelRef.kt +++ b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ModelRef.kt @@ -1,9 +1,16 @@ @file:Suppress("FunctionName", "ClassName", "unused", "EnumEntryName", "UnusedImport") package com.sourcegraph.cody.agent.protocol_generated; +import com.google.gson.annotations.SerializedName; + data class ModelRef( - val providerId: ProviderId, - val apiVersionId: ApiVersionId, - val modelId: ModelId, -) + val providerId: String, + val apiVersionId: ApiVersionIdEnum, // Oneof: unknown + val modelId: String, +) { + + enum class ApiVersionIdEnum { + @SerializedName("unknown") Unknown, + } +} diff --git a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ProviderId.kt b/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ProviderId.kt deleted file mode 100644 index c1ab6d7578e4..000000000000 --- a/agent/bindings/kotlin/lib/src/main/kotlin/com/sourcegraph/cody/agent/protocol_generated/ProviderId.kt +++ /dev/null @@ -1,5 +0,0 @@ -@file:Suppress("FunctionName", "ClassName", "unused", "EnumEntryName", "UnusedImport") -package com.sourcegraph.cody.agent.protocol_generated; - -typealias ProviderId = String // One of: - diff --git a/lib/shared/src/index.ts b/lib/shared/src/index.ts index 589d4a6d347d..7bb7eee30b9d 100644 --- a/lib/shared/src/index.ts +++ b/lib/shared/src/index.ts @@ -1,6 +1,12 @@ // Add anything else here that needs to be used outside of this library. -export { Model, modelsService, type ServerModel, type ServerModelConfiguration } from './models' +export { + Model, + modelsService, + mockModelsService, + type ServerModel, + type ServerModelConfiguration, +} from './models' export { type EditModel, type EditProvider, @@ -16,6 +22,7 @@ export { getModelInfo, isCodyProModel, isCustomModel, + toModelRefStr, } from './models/utils' export { BotResponseMultiplexer } from './chat/bot-response-multiplexer' export { ChatClient } from './chat/chat' diff --git a/lib/shared/src/models/index.test.ts b/lib/shared/src/models/index.test.ts index 97209bc80bb7..ce02bc649dad 100644 --- a/lib/shared/src/models/index.test.ts +++ b/lib/shared/src/models/index.test.ts @@ -6,9 +6,10 @@ import { type ModelCategory, type ModelTier, ModelsService, - type PerSitePreferences, type ServerModel, type ServerModelConfiguration, + type TestStorage, + mockModelsService, } from '../models/index' import { DOTCOM_URL } from '../sourcegraph-api/environments' import { CHAT_INPUT_TOKEN_BUDGET, CHAT_OUTPUT_TOKEN_BUDGET } from '../token/constants' @@ -231,35 +232,13 @@ describe('Model Provider', () => { let storage: TestStorage - class TestStorage { - constructor(public data: Map = new Map()) {} - get(key: string): string | null { - return this.data.get(key) ?? null - } - - async set(key: string, value: string) { - await this.data.set(key, value) - } - - async delete(key: string) { - this.data.delete(key) - } - - parse(): PerSitePreferences | undefined { - const dumped = this.data.get('model-preferences') - console.log(dumped) - if (dumped) { - return JSON.parse(dumped) - } - return undefined - } - } - beforeEach(async () => { - storage = new TestStorage() - modelsService.setStorage(storage) - mockAuthStatus(enterpriseAuthStatus) - await modelsService.setServerSentModels(SERVER_MODELS) + const result = await mockModelsService({ + config: SERVER_MODELS, + authStatus: enterpriseAuthStatus, + }) + storage = result.storage + modelsService = result.modelsService }) it('constructs from server models', () => { diff --git a/lib/shared/src/models/index.ts b/lib/shared/src/models/index.ts index 2d8aa99945b3..acab910ff1f1 100644 --- a/lib/shared/src/models/index.ts +++ b/lib/shared/src/models/index.ts @@ -1,6 +1,8 @@ import { type Observable, Subject } from 'observable-fns' import { authStatus, currentAuthStatus } from '../auth/authStatus' +import { mockAuthStatus } from '../auth/authStatus' import { type AuthStatus, isCodyProUser, isEnterpriseUser } from '../auth/types' +import { AUTH_STATUS_FIXTURE_AUTHED_DOTCOM } from '../auth/types' import { CodyIDE } from '../configuration' import { resolvedConfig } from '../configuration/resolver' import { fetchLocalOllamaModels } from '../llm-providers/ollama/utils' @@ -16,8 +18,8 @@ type ModelId = string type ApiVersionId = string type ProviderId = string -type ModelRefStr = `${ProviderId}::${ApiVersionId}::${ModelId}` -interface ModelRef { +export type ModelRefStr = `${ProviderId}::${ApiVersionId}::${ModelId}` +export interface ModelRef { providerId: ProviderId apiVersionId: ApiVersionId modelId: ModelId @@ -501,7 +503,6 @@ export class ModelsService { * Gets the available models of the specified usage type, with the default model first. * * @param type - The usage type of the models to retrieve. - * @param authStatus - The authentication status of the user. * @returns An array of models, with the default model first. */ public getModels(type: ModelUsage): Model[] { @@ -647,3 +648,56 @@ function capabilityToUsage(capability: ModelCapability): ModelUsage[] { return [ModelUsage.Chat, ModelUsage.Edit] } } + +interface MockModelsServiceResult { + storage: TestStorage + modelsService: ModelsService +} + +export class TestStorage { + constructor(public data: Map = new Map()) {} + get(key: string): string | null { + return this.data.get(key) ?? null + } + + async set(key: string, value: string) { + await this.data.set(key, value) + } + + async delete(key: string) { + this.data.delete(key) + } + + parse(): PerSitePreferences | undefined { + const dumped = this.data.get('model-preferences') + if (dumped) { + return JSON.parse(dumped) + } + return undefined + } +} + +interface MockModelsServiceParams { + config: ServerModelConfiguration + authStatus?: AuthStatus + modelsService?: ModelsService + storage?: TestStorage +} + +export async function mockModelsService( + params: MockModelsServiceParams +): Promise { + const { + storage = new TestStorage(), + modelsService = new ModelsService(), + authStatus = AUTH_STATUS_FIXTURE_AUTHED_DOTCOM, + config, + } = params + + modelsService.setStorage(storage) + mockAuthStatus(authStatus) + + await modelsService.setServerSentModels(config) + + return { storage, modelsService } +} diff --git a/lib/shared/src/models/utils.ts b/lib/shared/src/models/utils.ts index 92be3d6ec73d..c1d30ea0579e 100644 --- a/lib/shared/src/models/utils.ts +++ b/lib/shared/src/models/utils.ts @@ -1,4 +1,4 @@ -import type { Model } from '.' +import type { Model, ModelRef, ModelRefStr } from '.' import { ModelTag } from '..' export function getProviderName(name: string): string { @@ -49,3 +49,8 @@ export function isCustomModel(model: Model): boolean { function modelHasTag(model: Model, modelTag: ModelTag): boolean { return model.tags.includes(modelTag) } + +export function toModelRefStr(modelRef: ModelRef): ModelRefStr { + const { providerId, apiVersionId, modelId } = modelRef + return `${providerId}::${apiVersionId}::${modelId}` +} diff --git a/lib/shared/src/sourcegraph-api/rest/client.ts b/lib/shared/src/sourcegraph-api/rest/client.ts index f050137d0378..49408b43497a 100644 --- a/lib/shared/src/sourcegraph-api/rest/client.ts +++ b/lib/shared/src/sourcegraph-api/rest/client.ts @@ -66,7 +66,7 @@ export class RestClient { // TODO(PRIME-322): Export the type information via NPM. For now, we just blindly // walk the returned object model. // - // NOTE: This API endpoint hasn't shippeted yet, and probably won't work for you. + // NOTE: This API endpoint hasn't shipped yet, and probably won't work for you. // Also, the URL definitely will change. const serverSideConfig = await this.getRequest( 'getAvailableModels', diff --git a/vscode/src/completions/artificial-delay.ts b/vscode/src/completions/artificial-delay.ts index 151e342b78c2..514a7a4a6d4f 100644 --- a/vscode/src/completions/artificial-delay.ts +++ b/vscode/src/completions/artificial-delay.ts @@ -82,7 +82,7 @@ export function getArtificialDelay( } if (total > 0) { - logDebug('CodyCompletionProvider:getLatency', `Delay added: ${total}`) + logDebug('AutocompleteProvider:getLatency', `Delay added: ${total}`) } return total diff --git a/vscode/src/completions/create-inline-completion-item-provider.ts b/vscode/src/completions/create-inline-completion-item-provider.ts index 20cd9b98da78..75dae27502df 100644 --- a/vscode/src/completions/create-inline-completion-item-provider.ts +++ b/vscode/src/completions/create-inline-completion-item-provider.ts @@ -50,7 +50,7 @@ export function createInlineCompletionItemProvider({ }: InlineCompletionItemProviderArgs): Observable { const authStatus = currentAuthStatus() if (!authStatus.authenticated) { - logDebug('CodyCompletionProvider:notSignedIn', 'You are not signed in.') + logDebug('AutocompleteProvider:notSignedIn', 'You are not signed in.') if (config.isRunningInsideAgent) { // Register an empty completion provider when running inside the diff --git a/vscode/src/completions/create-multi-model-inline-completion-provider.ts b/vscode/src/completions/create-multi-model-inline-completion-provider.ts index 37a37976bf48..e978cc1c2ef2 100644 --- a/vscode/src/completions/create-multi-model-inline-completion-provider.ts +++ b/vscode/src/completions/create-multi-model-inline-completion-provider.ts @@ -136,6 +136,7 @@ export async function createInlineCompletionItemFromMultipleProviders({ legacyModel: currentProviderConfig.model, provider: currentProviderConfig.provider, config: newConfig, + source: 'local-editor-settings', }) const triggerDelay = vscode.workspace diff --git a/vscode/src/completions/get-inline-completions-tests/helpers.ts b/vscode/src/completions/get-inline-completions-tests/helpers.ts index 353ba9d1d321..87536918adfc 100644 --- a/vscode/src/completions/get-inline-completions-tests/helpers.ts +++ b/vscode/src/completions/get-inline-completions-tests/helpers.ts @@ -45,6 +45,7 @@ import { SINGLE_LINE_STOP_SEQUENCES, createProvider as createAnthropicProvider, } from '../providers/anthropic' +import type { AutocompleteProviderID } from '../providers/create-provider' import { createProvider as createFireworksProvider } from '../providers/fireworks' import { pressEnterAndGetIndentString } from '../providers/hot-streak' import { RequestManager } from '../request-manager' @@ -175,7 +176,8 @@ export function params( legacyModel: configuration?.autocompleteAdvancedModel!, config: configWithAccessToken, anonymousUserID: 'anonymousUserID', - provider: configuration?.autocompleteAdvancedModel || 'anthropic', + provider: (configuration?.autocompleteAdvancedModel as AutocompleteProviderID) || 'anthropic', + source: 'local-editor-settings', }) provider.client = client diff --git a/vscode/src/completions/inline-completion-item-provider-e2e.test.ts b/vscode/src/completions/inline-completion-item-provider-e2e.test.ts index 7421d2400b77..dd951088ee0a 100644 --- a/vscode/src/completions/inline-completion-item-provider-e2e.test.ts +++ b/vscode/src/completions/inline-completion-item-provider-e2e.test.ts @@ -155,6 +155,7 @@ function createNetworkProvider(params: RequestParams): MockRequestProvider { id: 'mock-provider', anonymousUserID: 'anonymousUserID', legacyModel: 'test-model', + source: 'local-editor-settings', }, providerOptions ) diff --git a/vscode/src/completions/inline-completion-item-provider.ts b/vscode/src/completions/inline-completion-item-provider.ts index 449fb9997a23..1e668fb70e49 100644 --- a/vscode/src/completions/inline-completion-item-provider.ts +++ b/vscode/src/completions/inline-completion-item-provider.ts @@ -191,9 +191,12 @@ export class InlineCompletionItemProvider this.smartThrottleService = new SmartThrottleService() this.disposables.push(this.smartThrottleService) + // TODO(valery): replace `model_configured_by_site_config` with the actual model ID received from backend. logDebug( - 'CodyCompletionProvider:initialized', - [this.config.provider.id, this.config.provider.legacyModel].join('/') + 'AutocompleteProvider:initialized', + `using "${this.config.provider.configSource}": "${this.config.provider.id}::${ + this.config.provider.legacyModel || 'model_configured_by_site_config' + }"` ) if (!this.config.noInlineAccept) { @@ -1132,7 +1135,7 @@ function logIgnored(uri: vscode.Uri, reason: CodyIgnoreType, isManualCompletion: } lastIgnoredUriLogged = string logDebug( - 'CodyCompletionProvider:ignored', + 'AutocompleteProvider:ignored', 'Cody is disabled in file ' + uri.toString() + ' (' + reason + ')' ) } diff --git a/vscode/src/completions/providers/__mocks__/create-provider-mocks.ts b/vscode/src/completions/providers/__mocks__/create-provider-mocks.ts new file mode 100644 index 000000000000..988aaf8574e7 --- /dev/null +++ b/vscode/src/completions/providers/__mocks__/create-provider-mocks.ts @@ -0,0 +1,302 @@ + +import { ModelTag, ServerModelConfiguration } from '@sourcegraph/cody-shared' + +/** + * Created by copy-pasting the sg02 model configuration API response. + * + * Ideally, we want this to be generated by the server-side and distributed as an npm package + * similar to context-filters-config. But this is a good start compared to not having any client + * tests for this type of configuration. + */ +export function getServerSentModelsMock(): ServerModelConfiguration { + return { + schemaVersion: '1.0', + revision: '0.0.0+dev', + providers: [ + { + id: 'anthropic', + displayName: 'Anthropic', + }, + { + id: 'fireworks', + displayName: 'Fireworks', + }, + { + id: 'google', + displayName: 'Google', + }, + { + id: 'openai', + displayName: 'OpenAI', + }, + { + id: 'mistral', + displayName: 'Mistral', + }, + { + id: 'groq', + displayName: 'Groq', + }, + { + id: 'cerebras', + displayName: 'cerebras', + }, + { + id: 'azure-openai', + displayName: 'Azure OpenAI', + }, + { + id: 'aws-bedrock', + displayName: 'AWS Bedrock', + }, + { + id: 'google-anthropic', + displayName: 'Google Anthropic', + }, + ], + models: [ + { + modelRef: 'anthropic::2023-06-01::claude-3-sonnet', + displayName: 'Claude 3 Sonnet', + modelName: 'claude-3-sonnet-20240229', + capabilities: ['autocomplete', 'chat'], + category: 'balanced' as ModelTag.Balanced, + status: 'stable', + tier: 'free' as ModelTag.Free, + contextWindow: { + maxInputTokens: 30000, + maxOutputTokens: 4000, + }, + }, + { + modelRef: 'anthropic::2023-06-01::claude-3.5-sonnet', + displayName: 'Claude 3.5 Sonnet', + modelName: 'claude-3-5-sonnet-20240620', + capabilities: ['autocomplete', 'chat'], + category: 'power' as ModelTag.Balanced, + status: 'stable', + tier: 'pro' as ModelTag.Pro, + contextWindow: { + maxInputTokens: 30000, + maxOutputTokens: 4000, + }, + }, + { + modelRef: 'anthropic::2023-06-01::claude-3-opus', + displayName: 'Claude 3 Opus', + modelName: 'claude-3-opus-20240229', + capabilities: ['autocomplete', 'chat'], + category: 'power' as ModelTag.Balanced, + status: 'stable', + tier: 'pro' as ModelTag.Pro, + contextWindow: { + maxInputTokens: 30000, + maxOutputTokens: 4000, + }, + }, + { + modelRef: 'anthropic::2023-06-01::claude-3-haiku', + displayName: 'Claude 3 Haiku', + modelName: 'claude-3-haiku-20240307', + capabilities: ['autocomplete', 'chat'], + category: 'speed' as ModelTag.Balanced, + status: 'stable', + tier: 'pro' as ModelTag.Pro, + contextWindow: { + maxInputTokens: 7000, + maxOutputTokens: 4000, + }, + }, + { + modelRef: 'fireworks::v1::starcoder', + displayName: 'StarCoder', + modelName: 'starcoder', + capabilities: ['autocomplete'], + category: 'speed' as ModelTag.Balanced, + status: 'stable', + tier: 'pro' as ModelTag.Pro, + contextWindow: { + maxInputTokens: 2048, + maxOutputTokens: 256, + }, + }, + { + modelRef: 'fireworks::v1::deepseek-coder-v2-lite-base', + displayName: 'DeepSeek V2 Lite Base', + modelName: 'accounts/sourcegraph/models/deepseek-coder-v2-lite-base', + capabilities: ['autocomplete'], + category: 'speed' as ModelTag.Balanced, + status: 'stable', + tier: 'pro' as ModelTag.Pro, + contextWindow: { + maxInputTokens: 2048, + maxOutputTokens: 256, + }, + }, + { + modelRef: 'google::v1::gemini-1.5-pro-latest', + displayName: 'Gemini 1.5 Pro', + modelName: 'gemini-1.5-pro-latest', + capabilities: ['autocomplete', 'chat'], + category: 'power' as ModelTag.Balanced, + status: 'stable', + tier: 'pro' as ModelTag.Pro, + contextWindow: { + maxInputTokens: 30000, + maxOutputTokens: 4000, + }, + }, + { + modelRef: 'google::v1::gemini-1.5-flash-latest', + displayName: 'Gemini 1.5 Flash', + modelName: 'gemini-1.5-flash-latest', + capabilities: ['autocomplete', 'chat'], + category: 'speed' as ModelTag.Balanced, + status: 'stable', + tier: 'pro' as ModelTag.Pro, + contextWindow: { + maxInputTokens: 30000, + maxOutputTokens: 4000, + }, + }, + { + modelRef: 'mistral::v1::mixtral-8x7b-instruct', + displayName: 'Mixtral 8x7B', + modelName: 'accounts/fireworks/models/mixtral-8x7b-instruct', + capabilities: ['autocomplete', 'chat'], + category: 'speed' as ModelTag.Balanced, + status: 'stable', + tier: 'pro' as ModelTag.Pro, + contextWindow: { + maxInputTokens: 7000, + maxOutputTokens: 4000, + }, + }, + { + modelRef: 'mistral::v1::mixtral-8x22b-instruct', + displayName: 'Mixtral 8x22B', + modelName: 'accounts/fireworks/models/mixtral-8x22b-instruct', + capabilities: ['autocomplete', 'chat'], + category: 'power' as ModelTag.Balanced, + status: 'stable', + tier: 'pro' as ModelTag.Pro, + contextWindow: { + maxInputTokens: 7000, + maxOutputTokens: 4000, + }, + }, + { + modelRef: 'openai::2024-02-01::gpt-4o', + displayName: 'GPT-4o', + modelName: 'gpt-4o', + capabilities: ['autocomplete', 'chat'], + category: 'power' as ModelTag.Balanced, + status: 'stable', + tier: 'pro' as ModelTag.Pro, + contextWindow: { + maxInputTokens: 30000, + maxOutputTokens: 4000, + }, + }, + { + modelRef: 'cerebras::v1::llama3.1-70b', + displayName: 'Llama 3.1 70b (via Cerebras)', + modelName: 'llama3.1-70b', + capabilities: ['chat', 'autocomplete'], + category: 'balanced' as ModelTag.Balanced, + status: 'stable', + tier: 'enterprise' as ModelTag.Enterprise, + contextWindow: { + maxInputTokens: 8192, + maxOutputTokens: 4000, + }, + }, + { + modelRef: 'groq::v1::llama-3.1-70b-versatile', + displayName: 'Llama-3.1 70b (via Groq)', + modelName: 'llama-3.1-70b-versatile', + capabilities: ['chat', 'autocomplete'], + category: 'balanced' as ModelTag.Balanced, + status: 'stable', + tier: 'enterprise' as ModelTag.Enterprise, + contextWindow: { + maxInputTokens: 8192, + maxOutputTokens: 4096, + }, + clientSideConfig: { + openAICompatible: {}, + }, + }, + { + modelRef: 'openai::v1::gpt-4o-rrr-n', + displayName: 'New model', + modelName: 'gpt-4o-rrr-n', + capabilities: ['chat'], + category: 'power' as ModelTag.Balanced, + status: 'stable', + tier: 'enterprise' as ModelTag.Enterprise, + contextWindow: { + maxInputTokens: 8192, + maxOutputTokens: 4000, + }, + }, + { + modelRef: 'azure-openai::v1::gpt-4o-test', + displayName: 'GPT-4o (via Azure OpenAI)', + modelName: 'gpt-4o-test', + capabilities: ['chat'], + category: 'power' as ModelTag.Balanced, + status: 'stable', + tier: 'enterprise' as ModelTag.Enterprise, + contextWindow: { + maxInputTokens: 8192, + maxOutputTokens: 4000, + }, + }, + { + modelRef: 'azure-openai::v1::gpt-4o-mini-test', + displayName: 'Mini (via Azure OpenAI)', + modelName: 'gpt-4o-mini-test', + capabilities: ['chat', 'autocomplete'], + category: 'power' as ModelTag.Balanced, + status: 'stable', + tier: 'enterprise' as ModelTag.Enterprise, + contextWindow: { + maxInputTokens: 8192, + maxOutputTokens: 4000, + }, + }, + { + modelRef: 'aws-bedrock::v1::claude-3-opus-20240229-v1', + displayName: 'Claude Opus (via AWS Bedrock)', + modelName: 'anthropic.claude-3-opus-20240229-v1:0', + capabilities: ['chat'], + category: 'power' as ModelTag.Balanced, + status: 'stable', + tier: 'enterprise' as ModelTag.Enterprise, + contextWindow: { + maxInputTokens: 8192, + maxOutputTokens: 4000, + }, + }, + { + modelRef: 'google-anthropic::unknown::claude-sonnet-google-anthropic', + displayName: 'Claude 3.5 Sonnet (via Google/Vertex)', + modelName: 'claude-3-5-sonnet@20240620', + capabilities: ['chat'], + category: 'power' as ModelTag.Balanced, + status: 'stable', + tier: 'enterprise' as ModelTag.Enterprise, + contextWindow: { + maxInputTokens: 8192, + maxOutputTokens: 4000, + }, + }, + ], + defaultModels: { + chat: 'anthropic::2023-06-01::claude-3.5-sonnet', + fastChat: 'anthropic::2023-06-01::claude-3-haiku', + codeCompletion: 'fireworks::v1::deepseek-coder-v2-lite-base', + }, + } +} diff --git a/vscode/src/completions/providers/anthropic.ts b/vscode/src/completions/providers/anthropic.ts index 8216af88a637..fbedf7c2b41a 100644 --- a/vscode/src/completions/providers/anthropic.ts +++ b/vscode/src/completions/providers/anthropic.ts @@ -284,12 +284,13 @@ function getClientModel(provider: string): string { } export function createProvider(params: ProviderFactoryParams): Provider { - const { provider, anonymousUserID } = params + const { provider, anonymousUserID, source } = params return new AnthropicProvider({ id: 'anthropic', legacyModel: getClientModel(provider), anonymousUserID, + source, }) } diff --git a/vscode/src/completions/providers/create-provider.test.ts b/vscode/src/completions/providers/create-provider.test.ts index 17c0c9fd5bec..7772f29372b1 100644 --- a/vscode/src/completions/providers/create-provider.test.ts +++ b/vscode/src/completions/providers/create-provider.test.ts @@ -1,16 +1,24 @@ +import { Observable } from 'observable-fns' +import { beforeAll, describe, expect, it, vi } from 'vitest' + import { + AUTH_STATUS_FIXTURE_AUTHED, AUTH_STATUS_FIXTURE_AUTHED_DOTCOM, type ClientConfiguration, type CodyLLMSiteConfiguration, + ModelUsage, featureFlagProvider, mockAuthStatus, + mockModelsService, + modelsService, toFirstValueGetter, + toModelRefStr, } from '@sourcegraph/cody-shared' -import { beforeAll, describe, expect, it, vi } from 'vitest' + import { mockLocalStorage } from '../../services/LocalStorageProvider' import { getVSCodeConfigurationWithAccessToken } from '../../testutils/mocks' -import { Observable } from 'observable-fns' +import { getServerSentModelsMock } from './__mocks__/create-provider-mocks' import { createProvider } from './create-provider' const createProviderFirstValue = toFirstValueGetter(createProvider) @@ -126,134 +134,185 @@ describe('createProvider', () => { }) }) - describe('completions provider and model are defined in the site config and not set in VSCode settings', () => { - describe('if provider is "sourcegraph"', () => { - const testCases: { - configOverwrites: CodyLLMSiteConfiguration - expected: { provider: string; legacyModel?: string } | null - }[] = [ - // sourcegraph - { - configOverwrites: { provider: 'sourcegraph', completionModel: 'hello-world' }, - expected: null, - }, - { - configOverwrites: { - provider: 'sourcegraph', - completionModel: 'anthropic/claude-instant-1.2', - }, - expected: { provider: 'anthropic', legacyModel: 'anthropic/claude-instant-1.2' }, - }, - { - configOverwrites: { provider: 'sourcegraph', completionModel: 'anthropic/' }, - expected: null, + describe('legacy site-config Cody LLM configuration', () => { + const testCases: { + configOverwrites: CodyLLMSiteConfiguration + expected: { provider: string; legacyModel?: string } | null + }[] = [ + // sourcegraph + { + configOverwrites: { provider: 'sourcegraph', completionModel: 'hello-world' }, + expected: null, + }, + { + configOverwrites: { + provider: 'sourcegraph', + completionModel: 'anthropic/claude-instant-1.2', }, - { - configOverwrites: { - provider: 'sourcegraph', - completionModel: '/claude-instant-1.2', - }, - expected: null, + expected: { provider: 'anthropic', legacyModel: 'anthropic/claude-instant-1.2' }, + }, + { + configOverwrites: { provider: 'sourcegraph', completionModel: 'anthropic/' }, + expected: null, + }, + { + configOverwrites: { + provider: 'sourcegraph', + completionModel: '/claude-instant-1.2', }, - { - configOverwrites: { - provider: 'sourcegraph', - completionModel: 'fireworks/starcoder', - }, - expected: { provider: 'fireworks', legacyModel: 'starcoder' }, + expected: null, + }, + { + configOverwrites: { + provider: 'sourcegraph', + completionModel: 'fireworks/starcoder', }, + expected: { provider: 'fireworks', legacyModel: 'starcoder' }, + }, - // aws-bedrock - { - configOverwrites: { provider: 'aws-bedrock', completionModel: 'hello-world' }, - expected: null, - }, - { - configOverwrites: { - provider: 'aws-bedrock', - completionModel: 'anthropic.claude-instant-1.2', - }, - expected: { provider: 'anthropic', legacyModel: 'anthropic/claude-instant-1.2' }, - }, - { - configOverwrites: { provider: 'aws-bedrock', completionModel: 'anthropic.' }, - expected: null, + // aws-bedrock + { + configOverwrites: { provider: 'aws-bedrock', completionModel: 'hello-world' }, + expected: null, + }, + { + configOverwrites: { + provider: 'aws-bedrock', + completionModel: 'anthropic.claude-instant-1.2', }, - { - configOverwrites: { - provider: 'aws-bedrock', - completionModel: 'anthropic/claude-instant-1.2', - }, - expected: null, + expected: { provider: 'anthropic', legacyModel: 'anthropic/claude-instant-1.2' }, + }, + { + configOverwrites: { provider: 'aws-bedrock', completionModel: 'anthropic.' }, + expected: null, + }, + { + configOverwrites: { + provider: 'aws-bedrock', + completionModel: 'anthropic/claude-instant-1.2', }, + expected: null, + }, - // open-ai - { - configOverwrites: { provider: 'openai', completionModel: 'gpt-35-turbo-test' }, - expected: { provider: 'unstable-openai', legacyModel: 'gpt-35-turbo-test' }, - }, - { - configOverwrites: { provider: 'openai' }, - expected: { provider: 'unstable-openai', legacyModel: 'gpt-35-turbo' }, - }, + // open-ai + { + configOverwrites: { provider: 'openai', completionModel: 'gpt-35-turbo-test' }, + expected: { provider: 'unstable-openai', legacyModel: 'gpt-35-turbo-test' }, + }, + { + configOverwrites: { provider: 'openai' }, + expected: { provider: 'unstable-openai', legacyModel: 'gpt-35-turbo' }, + }, - // azure-openai - { - configOverwrites: { provider: 'azure-openai', completionModel: 'gpt-35-turbo-test' }, - expected: { provider: 'unstable-openai', legacyModel: '' }, - }, - { - configOverwrites: { provider: 'azure-openai' }, - expected: { provider: 'unstable-openai', legacyModel: 'gpt-35-turbo' }, - }, + // azure-openai + { + configOverwrites: { provider: 'azure-openai', completionModel: 'gpt-35-turbo-test' }, + expected: { provider: 'unstable-openai', legacyModel: '' }, + }, + { + configOverwrites: { provider: 'azure-openai' }, + expected: { provider: 'unstable-openai', legacyModel: 'gpt-35-turbo' }, + }, - // fireworks - { - configOverwrites: { provider: 'fireworks', completionModel: 'starcoder-7b' }, - expected: { provider: 'fireworks', legacyModel: 'starcoder-7b' }, - }, - { - configOverwrites: { provider: 'fireworks' }, - expected: { provider: 'fireworks', legacyModel: 'deepseek-coder-v2-lite-base' }, - }, + // fireworks + { + configOverwrites: { provider: 'fireworks', completionModel: 'starcoder-7b' }, + expected: { provider: 'fireworks', legacyModel: 'starcoder-7b' }, + }, + { + configOverwrites: { provider: 'fireworks' }, + expected: { provider: 'fireworks', legacyModel: 'deepseek-coder-v2-lite-base' }, + }, - // unknown-provider - { - configOverwrites: { - provider: 'unknown-provider', - completionModel: 'superdupercoder-7b', - }, - expected: null, + // unknown-provider + { + configOverwrites: { + provider: 'unknown-provider', + completionModel: 'superdupercoder-7b', }, + expected: null, + }, - // provider not defined (backward compat) - { - configOverwrites: { provider: undefined, completionModel: 'superdupercoder-7b' }, - expected: null, - }, - ] - - for (const { configOverwrites, expected } of testCases) { - it(`returns ${JSON.stringify(expected)} when cody LLM config is ${JSON.stringify( - configOverwrites - )}`, async () => { - mockAuthStatus({ - ...AUTH_STATUS_FIXTURE_AUTHED_DOTCOM, - configOverwrites, - }) - - const provider = await createProviderFirstValue( - getVSCodeConfigurationWithAccessToken() - ) - - if (expected === null) { - expect(provider).toBeNull() - } else { - expect(provider?.id).toBe(expected.provider) - expect(provider?.legacyModel).toBe(expected.legacyModel) - } + // provider not defined (backward compat) + { + configOverwrites: { provider: undefined, completionModel: 'superdupercoder-7b' }, + expected: null, + }, + ] + + for (const { configOverwrites, expected } of testCases) { + it(`returns ${JSON.stringify(expected)} when cody LLM config is ${JSON.stringify( + configOverwrites + )}`, async () => { + mockAuthStatus({ + ...AUTH_STATUS_FIXTURE_AUTHED_DOTCOM, + configOverwrites, }) - } + + const provider = await createProviderFirstValue(getVSCodeConfigurationWithAccessToken()) + + if (expected === null) { + expect(provider).toBeNull() + } else { + expect(provider?.id).toBe(expected.provider) + expect(provider?.legacyModel).toBe(expected.legacyModel) + } + }) + } + }) + + describe('server-side model configuration', () => { + beforeAll(async () => { + await mockModelsService({ + modelsService: modelsService.instance!, + config: getServerSentModelsMock(), + authStatus: AUTH_STATUS_FIXTURE_AUTHED, + }) + }) + + it('uses all available autocomplete models', async () => { + const mockedConfig = getServerSentModelsMock() + const autocompleteModelsInServerConfig = mockedConfig.models.filter(model => + model.capabilities.includes('autocomplete') + ) + + const autocompleteModels = modelsService.instance!.getModels(ModelUsage.Autocomplete) + expect(autocompleteModels.length).toBe(autocompleteModelsInServerConfig.length) + }) + + it('uses the `fireworks` model from the config', async () => { + const provider = await createProviderFirstValue(getVSCodeConfigurationWithAccessToken()) + const currentModel = modelsService.instance!.getDefaultModel(ModelUsage.Autocomplete) + + expect(currentModel?.provider).toBe('fireworks') + expect(currentModel?.id).toBe('deepseek-coder-v2-lite-base') + + expect(provider?.id).toBe(currentModel?.provider) + expect(provider?.legacyModel).toBe(currentModel?.id) + }) + + it('uses the `anthropic` model from the config', async () => { + const mockedConfig = getServerSentModelsMock() + + const autocompleteModels = modelsService.instance!.getModels(ModelUsage.Autocomplete) + const anthropicModel = autocompleteModels.find(model => model.id === 'claude-3-sonnet')! + + // Change the default autocomplete model to anthropic + mockedConfig.defaultModels.codeCompletion = toModelRefStr(anthropicModel.modelRef!) + + await mockModelsService({ + modelsService: modelsService.instance!, + config: mockedConfig, + authStatus: AUTH_STATUS_FIXTURE_AUTHED, + }) + + const provider = await createProviderFirstValue(getVSCodeConfigurationWithAccessToken()) + + expect(anthropicModel.provider).toBe('anthropic') + expect(anthropicModel.id).toBe('claude-3-sonnet') + expect(provider?.id).toBe(anthropicModel.provider) + // TODO(valery): use a readable identifier for BYOK providers to communicate that the model ID from the server is used. + expect(provider?.legacyModel).toBe('') }) }) }) diff --git a/vscode/src/completions/providers/create-provider.ts b/vscode/src/completions/providers/create-provider.ts index 5cc0678114dd..eb81b5d6c832 100644 --- a/vscode/src/completions/providers/create-provider.ts +++ b/vscode/src/completions/providers/create-provider.ts @@ -1,20 +1,22 @@ import { type ClientConfigurationWithAccessToken, type Model, + ModelUsage, currentAuthStatusAuthed, + modelsService, } from '@sourcegraph/cody-shared' -import { Observable, map } from 'observable-fns' +import { Observable } from 'observable-fns' import { logError } from '../../log' import { localStorage } from '../../services/LocalStorageProvider' import { createProvider as createAnthropicProvider } from './anthropic' import { createProvider as createExperimentalOllamaProvider } from './experimental-ollama' import { createProvider as createExperimentalOpenAICompatibleProvider } from './expopenaicompatible' import { createProvider as createFireworksProvider } from './fireworks' -import { getExperimentModel } from './get-experiment-model' -import { getModelInfo } from './get-model-info' +import { getDotComExperimentModel } from './get-experiment-model' import { createProvider as createGeminiProviderConfig } from './google' import { createProvider as createOpenAICompatibleProviderConfig } from './openaicompatible' +import { parseProviderAndModel } from './parse-provider-and-model' import type { Provider, ProviderFactory } from './provider' import { createProvider as createUnstableOpenAIProviderConfig } from './unstable-openai' @@ -26,39 +28,66 @@ export function createProvider(config: ClientConfigurationWithAccessToken): Obse legacyModel: config.autocompleteAdvancedModel || undefined, provider: config.autocompleteAdvancedProvider, config, + source: 'local-editor-settings', }) ) } - return getExperimentModel().pipe( - map(configFromFeatureFlags => { - // Check if a user participates in autocomplete model experiments, and use the - // experiment model if available. - if (configFromFeatureFlags) { - return createProviderHelper({ - legacyModel: configFromFeatureFlags.model, - provider: configFromFeatureFlags.provider, - config, - }) - } + return getDotComExperimentModel().map(dotComExperiment => { + // Check if a user participates in autocomplete experiments. + if (dotComExperiment) { + return createProviderHelper({ + legacyModel: dotComExperiment.model, + provider: dotComExperiment.provider, + config, + source: 'dotcom-feature-flags', + }) + } - const modelInfoOrError = getModelInfo() + // Check if server-side model configuration is available. + const model = modelsService.instance!.getDefaultModel(ModelUsage.Autocomplete) - if (modelInfoOrError instanceof Error) { - logError('createProvider', modelInfoOrError.message) - return null - } - - const { provider, legacyModel, model } = modelInfoOrError + if (model) { + const provider = model.clientSideConfig?.openAICompatible + ? 'openaicompatible' + : model.provider return createProviderHelper({ - legacyModel, + legacyModel: model.id, model, provider, config, + source: 'server-side-model-config', }) - }) - ) + } + + // Fallback to site-config Cody LLM configuration. + const { configOverwrites } = currentAuthStatusAuthed() + + if (configOverwrites?.provider) { + const parsedProviderAndModel = parseProviderAndModel({ + provider: configOverwrites.provider, + legacyModel: configOverwrites.completionModel, + }) + + if (parsedProviderAndModel instanceof Error) { + logError('createProvider', parsedProviderAndModel.message) + return null + } + + return createProviderHelper({ + ...parsedProviderAndModel, + config, + source: 'site-config-cody-llm-configuration', + }) + } + + logError( + 'createProvider', + 'Failed to get autocomplete provider. Please configure the `completionModel` using site configuration.' + ) + return null + }) } interface CreateConfigHelperParams { @@ -66,10 +95,11 @@ interface CreateConfigHelperParams { provider: string config: ClientConfigurationWithAccessToken model?: Model + source: AutocompleteProviderConfigSource } export function createProviderHelper(params: CreateConfigHelperParams): Provider | null { - const { legacyModel, model, provider, config } = params + const { legacyModel, model, provider, config, source } = params const anonymousUserID = localStorage.anonymousUserID() const providerCreator = getProviderCreator({ @@ -82,7 +112,8 @@ export function createProviderHelper(params: CreateConfigHelperParams): Provider legacyModel: legacyModel, config, anonymousUserID, - provider, + provider: provider as AutocompleteProviderID, + source, }) } @@ -246,3 +277,32 @@ export const AUTOCOMPLETE_PROVIDER_ID = { */ 'unstable-ollama': 'unstable-ollama', } as const + +/** + * Config sources are listed in the order of precedence. + */ +export const AUTOCOMPLETE_PROVIDER_CONFIG_SOURCE = { + /** + * Local user configuration. Used to switch from the remote default to ollama and potentially other local providers. + */ + 'local-editor-settings': 'local-editor-settings', + + /** + * Used only on DotCom for A/B testing new models. + */ + 'dotcom-feature-flags': 'dotcom-feature-flags', + + /** + * The server-side models configuration API we intend to migrate to. Currently used only by a handful of enterprise customers. + * See {@link RestClient.getAvailableModels} for more details. + */ + 'server-side-model-config': 'server-side-model-config', + + /** + * The old way of configuring models. + * See {@link SourcegraphGraphQLAPIClient.getCodyLLMConfiguration} for more details. + */ + 'site-config-cody-llm-configuration': 'site-config-cody-llm-configuration', +} as const + +export type AutocompleteProviderConfigSource = keyof typeof AUTOCOMPLETE_PROVIDER_CONFIG_SOURCE diff --git a/vscode/src/completions/providers/experimental-ollama.ts b/vscode/src/completions/providers/experimental-ollama.ts index 3c723a550986..2ebd2036c8a5 100644 --- a/vscode/src/completions/providers/experimental-ollama.ts +++ b/vscode/src/completions/providers/experimental-ollama.ts @@ -199,7 +199,7 @@ class ExperimentalOllamaProvider extends Provider { } export function createProvider(params: ProviderFactoryParams): Provider { - const { config, anonymousUserID } = params + const { config, anonymousUserID, source } = params return new ExperimentalOllamaProvider( { @@ -207,6 +207,7 @@ export function createProvider(params: ProviderFactoryParams): Provider { legacyModel: config.autocompleteExperimentalOllamaOptions.model, anonymousUserID, mayUseOnDeviceInference: true, + source, }, config.autocompleteExperimentalOllamaOptions ) diff --git a/vscode/src/completions/providers/expopenaicompatible.ts b/vscode/src/completions/providers/expopenaicompatible.ts index 704d63537c00..3ee10a3612b0 100644 --- a/vscode/src/completions/providers/expopenaicompatible.ts +++ b/vscode/src/completions/providers/expopenaicompatible.ts @@ -355,7 +355,7 @@ function getClientModel(model?: string): OpenAICompatibleModel { } export function createProvider(params: ProviderFactoryParams): Provider { - const { legacyModel, anonymousUserID } = params + const { legacyModel, anonymousUserID, source } = params const clientModel = getClientModel(legacyModel) @@ -364,5 +364,6 @@ export function createProvider(params: ProviderFactoryParams): Provider { legacyModel: clientModel, maxContextTokens: getMaxContextTokens(clientModel), anonymousUserID, + source, }) } diff --git a/vscode/src/completions/providers/fireworks.ts b/vscode/src/completions/providers/fireworks.ts index e08de531d308..95633c7c8fd3 100644 --- a/vscode/src/completions/providers/fireworks.ts +++ b/vscode/src/completions/providers/fireworks.ts @@ -274,7 +274,7 @@ function getClientModel(model?: string): FireworksModel { } export function createProvider(params: ProviderFactoryParams): Provider { - const { legacyModel, anonymousUserID } = params + const { legacyModel, anonymousUserID, source } = params const clientModel = getClientModel(legacyModel) @@ -283,5 +283,6 @@ export function createProvider(params: ProviderFactoryParams): Provider { legacyModel: clientModel, maxContextTokens: getMaxContextTokens(clientModel), anonymousUserID, + source, }) } diff --git a/vscode/src/completions/providers/get-experiment-model.ts b/vscode/src/completions/providers/get-experiment-model.ts index 776f65375af2..ab65cca763a8 100644 --- a/vscode/src/completions/providers/get-experiment-model.ts +++ b/vscode/src/completions/providers/get-experiment-model.ts @@ -23,7 +23,7 @@ interface ProviderConfigFromFeatureFlags { model?: string } -export function getExperimentModel(): Observable { +export function getDotComExperimentModel(): Observable { // We run model experiments only on DotCom. if (!isDotComAuthed()) { return Observable.of(null) @@ -74,7 +74,7 @@ export function getExperimentModel(): Observable { +function resolveFIMModelExperimentFromFeatureFlags(): ReturnType { return combineLatest([ featureFlagProvider.evaluatedFeatureFlag(FeatureFlag.CodyAutocompleteFIMModelExperimentControl), featureFlagProvider.evaluatedFeatureFlag(FeatureFlag.CodyAutocompleteFIMModelExperimentVariant1), diff --git a/vscode/src/completions/providers/get-model-info.ts b/vscode/src/completions/providers/get-model-info.ts deleted file mode 100644 index 445886f0e477..000000000000 --- a/vscode/src/completions/providers/get-model-info.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { type Model, ModelUsage, currentAuthStatusAuthed, modelsService } from '@sourcegraph/cody-shared' - -interface ModelInfo { - provider: string - legacyModel?: string - model?: Model -} - -export function getModelInfo(): ModelInfo | Error { - const model = modelsService.instance!.getDefaultModel(ModelUsage.Autocomplete) - - if (model) { - let provider = model.provider - if (model.clientSideConfig?.openAICompatible) { - provider = 'openaicompatible' - } - return { provider, legacyModel: model.id, model } - } - - const { configOverwrites } = currentAuthStatusAuthed() - - if (configOverwrites?.provider) { - return parseProviderAndModel({ - provider: configOverwrites.provider, - legacyModel: configOverwrites.completionModel, - }) - } - - // Fail with error if no `completionModel` is configured. - return new Error( - 'Failed to get autocomplete model info. Please configure the `completionModel` using site configuration.' - ) -} - -const delimiters: Record = { - sourcegraph: '/', - 'aws-bedrock': '.', -} - -/** - * For certain completions providers configured in the Sourcegraph instance site config - * the model name consists MODEL_PROVIDER and MODEL_NAME separated by a specific delimiter (see {@link delimiters}). - * - * This function checks if the given provider has a specific model naming format and: - * - if it does, parses the model name and returns the parsed provider and model names; - * - if it doesn't, returns the original provider and model names. - * - * E.g. for "sourcegraph" provider the completions model name consists of model provider and model name separated by "/". - * So when received `{ provider: "sourcegraph", model: "anthropic/claude-instant-1" }` the expected output would be `{ provider: "anthropic", model: "claude-instant-1" }`. - */ -function parseProviderAndModel({ provider, legacyModel }: ModelInfo): ModelInfo | Error { - const delimiter = delimiters[provider] - if (!delimiter) { - return { provider, legacyModel: legacyModel } - } - - if (legacyModel) { - const index = legacyModel.indexOf(delimiter) - const parsedProvider = legacyModel.slice(0, index) - const parsedModel = legacyModel.slice(index + 1) - if (parsedProvider && parsedModel) { - return { provider: parsedProvider, legacyModel: parsedModel } - } - } - - return new Error( - (legacyModel - ? `Failed to parse the model name ${legacyModel}` - : `Model missing but delimiter ${delimiter} expected`) + - `for '${provider}' completions provider.` - ) -} diff --git a/vscode/src/completions/providers/google.ts b/vscode/src/completions/providers/google.ts index 8af92851ce6f..8c70f3e48bdc 100644 --- a/vscode/src/completions/providers/google.ts +++ b/vscode/src/completions/providers/google.ts @@ -168,7 +168,7 @@ Your response should contains only the code required to connect the gap, and the const SUPPORTED_GEMINI_MODELS = ['gemini-1.5-flash', 'gemini-pro', 'gemini-1.0-pro'] as const export function createProvider(params: ProviderFactoryParams): Provider { - const { legacyModel, anonymousUserID } = params + const { legacyModel, anonymousUserID, source } = params const clientModel = legacyModel ?? 'google/gemini-1.5-flash' if (!SUPPORTED_GEMINI_MODELS.some(m => clientModel.includes(m))) { @@ -179,5 +179,6 @@ export function createProvider(params: ProviderFactoryParams): Provider { id: 'google', legacyModel: clientModel, anonymousUserID, + source, }) } diff --git a/vscode/src/completions/providers/openaicompatible.ts b/vscode/src/completions/providers/openaicompatible.ts index 2ae538146172..da5782b9c494 100644 --- a/vscode/src/completions/providers/openaicompatible.ts +++ b/vscode/src/completions/providers/openaicompatible.ts @@ -102,7 +102,7 @@ class OpenAICompatibleProvider extends Provider { } export function createProvider(params: ProviderFactoryParams): Provider { - const { model, anonymousUserID } = params + const { model, anonymousUserID, source } = params if (model) { logDebug('OpenAICompatible', 'autocomplete provider using model', JSON.stringify(model)) @@ -120,6 +120,7 @@ export function createProvider(params: ProviderFactoryParams): Provider { model, maxContextTokens, anonymousUserID, + source, }) } diff --git a/vscode/src/completions/providers/parse-provider-and-model.ts b/vscode/src/completions/providers/parse-provider-and-model.ts new file mode 100644 index 000000000000..aedf63840b34 --- /dev/null +++ b/vscode/src/completions/providers/parse-provider-and-model.ts @@ -0,0 +1,44 @@ +interface ModelInfo { + provider: string + legacyModel: string | undefined +} + +const delimiters: Record = { + sourcegraph: '/', + 'aws-bedrock': '.', +} + +/** + * For certain completions providers configured in the Sourcegraph instance site config + * the model name consists MODEL_PROVIDER and MODEL_NAME separated by a specific delimiter (see {@link delimiters}). + * + * This function checks if the given provider has a specific model naming format and: + * - if it does, parses the model name and returns the parsed provider and model names; + * - if it doesn't, returns the original provider and model names. + * + * E.g. for "sourcegraph" provider the completions model name consists of model provider and model name separated by "/". + * So when received `{ provider: "sourcegraph", model: "anthropic/claude-instant-1" }` the expected output would be `{ provider: "anthropic", model: "claude-instant-1" }`. + */ +export function parseProviderAndModel({ + provider, + legacyModel, +}: ModelInfo): Required | Error { + const delimiter = delimiters[provider] + + if (!delimiter || !legacyModel) { + return { provider, legacyModel } + } + + if (legacyModel) { + const index = legacyModel.indexOf(delimiter) + const parsedProvider = legacyModel.slice(0, index) + const parsedModel = legacyModel.slice(index + 1) + if (parsedProvider && parsedModel) { + return { provider: parsedProvider, legacyModel: parsedModel } + } + } + + return new Error( + `Failed to parse the model name ${legacyModel} for '${provider}' completions provider.` + ) +} diff --git a/vscode/src/completions/providers/provider.ts b/vscode/src/completions/providers/provider.ts index c924122537c2..be15f9564422 100644 --- a/vscode/src/completions/providers/provider.ts +++ b/vscode/src/completions/providers/provider.ts @@ -18,6 +18,7 @@ import type { InlineCompletionItemWithAnalytics } from '../text-processing/proce import { defaultCodeCompletionsClient } from '../default-client' import { type DefaultModel, getModelHelpers } from '../model-helpers' +import type { AutocompleteProviderConfigSource, AutocompleteProviderID } from './create-provider' import type { FetchCompletionResult } from './fetch-and-process-completions' import { MAX_RESPONSE_TOKENS } from './get-completion-params' @@ -83,15 +84,31 @@ export type ProviderOptions = (ProviderModelOptions | ProviderLegacyModelOptions */ maxContextTokens?: number mayUseOnDeviceInference?: boolean + source: AutocompleteProviderConfigSource } export type ProviderFactoryParams = { + /** + * Model instance created from the server-side model config API response. + */ model?: Model + /** + * Model string ID kept here for backward compatibility. Should be replaced fully by `model`. + */ legacyModel?: string - config: ClientConfigurationWithAccessToken + /** + * Client provider ID. + */ + provider: AutocompleteProviderID + /** + * How the provider/model combination was resolved. + * Used for debugging purposes. + */ + source: AutocompleteProviderConfigSource + anonymousUserID: string + config: ClientConfigurationWithAccessToken mayUseOnDeviceInference?: boolean - provider: string } export type ProviderFactory = (params: ProviderFactoryParams) => Provider @@ -112,6 +129,7 @@ export abstract class Provider { public legacyModel: string public contextSizeHints: ProviderContextSizeHints public client: CodeCompletionsClient = defaultCodeCompletionsClient.instance! + public configSource: AutocompleteProviderConfigSource protected maxContextTokens: number protected anonymousUserID: string @@ -127,6 +145,7 @@ export abstract class Provider { maxContextTokens = DEFAULT_MAX_CONTEXT_TOKENS, anonymousUserID, mayUseOnDeviceInference = false, + source, } = options if ('model' in options) { @@ -140,6 +159,7 @@ export abstract class Provider { this.maxContextTokens = maxContextTokens this.anonymousUserID = anonymousUserID this.mayUseOnDeviceInference = mayUseOnDeviceInference + this.configSource = source this.modelHelper = getModelHelpers(this.legacyModel) this.promptChars = tokensToChars(maxContextTokens - MAX_RESPONSE_TOKENS) diff --git a/vscode/src/completions/providers/unstable-openai.ts b/vscode/src/completions/providers/unstable-openai.ts index 9a90fe00080a..027c6e81bfa1 100644 --- a/vscode/src/completions/providers/unstable-openai.ts +++ b/vscode/src/completions/providers/unstable-openai.ts @@ -174,7 +174,7 @@ ${OPENING_CODE_TAG}${infillBlock}` } export function createProvider(params: ProviderFactoryParams): Provider { - const { legacyModel, provider, anonymousUserID } = params + const { legacyModel, provider, anonymousUserID, source } = params let clientModel = legacyModel @@ -192,5 +192,6 @@ export function createProvider(params: ProviderFactoryParams): Provider { id: 'unstable-openai', legacyModel: clientModel ?? 'gpt-35-turbo', anonymousUserID, + source, }) } diff --git a/vscode/src/completions/request-manager.test.ts b/vscode/src/completions/request-manager.test.ts index ad67e12bd63d..f3d9f2856078 100644 --- a/vscode/src/completions/request-manager.test.ts +++ b/vscode/src/completions/request-manager.test.ts @@ -76,6 +76,7 @@ function createProvider() { id: 'mock-provider', anonymousUserID: 'anonymousUserID', legacyModel: 'test-model', + source: 'local-editor-settings', }) } diff --git a/vscode/src/completions/request-manager.ts b/vscode/src/completions/request-manager.ts index c6bf19dadd36..63885700cd04 100644 --- a/vscode/src/completions/request-manager.ts +++ b/vscode/src/completions/request-manager.ts @@ -301,7 +301,7 @@ export class RequestManager { } if (shouldAbort) { - logDebug('CodyCompletionProvider', 'Irrelevant request aborted') + logDebug('AutocompleteProvider', 'Irrelevant request aborted') request.abortController.abort() this.inflightRequests.delete(request) } diff --git a/vscode/src/models/sync.ts b/vscode/src/models/sync.ts index 3bbe2188aa9b..b171e77f6e3f 100644 --- a/vscode/src/models/sync.ts +++ b/vscode/src/models/sync.ts @@ -34,7 +34,7 @@ import { getEnterpriseContextWindow } from './utils' * or fallback to the limit from the authentication status if not configured. */ export async function syncModels(authStatus: AuthStatus): Promise { - // Offline mode only support Ollama models, which would be synced seperately. + // Offline mode only support Ollama models, which would be synced separately. if (authStatus.authenticated && authStatus.isOfflineMode) { modelsService.instance!.setModels([]) return