diff --git a/lib/shared/src/sourcegraph-api/graphql/client.ts b/lib/shared/src/sourcegraph-api/graphql/client.ts index 42d1cff40f6..d629a2f2e29 100644 --- a/lib/shared/src/sourcegraph-api/graphql/client.ts +++ b/lib/shared/src/sourcegraph-api/graphql/client.ts @@ -6,6 +6,7 @@ import { DOTCOM_URL, isDotCom } from '../environments' import { CURRENT_SITE_CODY_LLM_CONFIGURATION, + CURRENT_SITE_CODY_LLM_PROVIDER, CURRENT_SITE_GRAPHQL_FIELDS_QUERY, CURRENT_SITE_HAS_CODY_ENABLED_QUERY, CURRENT_SITE_IDENTIFICATION, @@ -57,6 +58,14 @@ interface CurrentUserIdHasVerifiedEmailResponse { currentUser: { id: string; hasVerifiedEmail: boolean } | null } +interface CodyLLMSiteConfigurationResponse { + site: { codyLLMConfiguration: Omit | null } | null +} + +interface CodyLLMSiteConfigurationProviderResponse { + site: { codyLLMConfiguration: Pick | null } | null +} + interface RepositoryIdResponse { repository: { id: string } | null } @@ -140,6 +149,7 @@ export interface CodyLLMSiteConfiguration { fastChatModelMaxTokens?: number completionModel?: string completionModelMaxTokens?: number + provider?: string } interface IsContextRequiredForChatQueryResponse { @@ -274,9 +284,28 @@ export class SourcegraphGraphQLAPIClient { } public async getCodyLLMConfiguration(): Promise { - const response = await this.fetchSourcegraphAPI>(CURRENT_SITE_CODY_LLM_CONFIGURATION) + // fetch Cody LLM provider separately for backward compatability + const [configResponse, providerResponse] = await Promise.all([ + this.fetchSourcegraphAPI>( + CURRENT_SITE_CODY_LLM_CONFIGURATION + ), + this.fetchSourcegraphAPI>( + CURRENT_SITE_CODY_LLM_PROVIDER + ), + ]) + + const config = extractDataOrError(configResponse, data => data.site?.codyLLMConfiguration || undefined) + if (!config || isError(config)) { + return config + } + + let provider: string | undefined + const llmProvider = extractDataOrError(providerResponse, data => data.site?.codyLLMConfiguration?.provider) + if (llmProvider && !isError(llmProvider)) { + provider = llmProvider + } - return extractDataOrError(response, data => data.site?.codyLLMConfiguration) + return { ...config, provider } } public async getRepoIds(names: string[]): Promise<{ id: string; name: string }[] | Error> { diff --git a/lib/shared/src/sourcegraph-api/graphql/queries.ts b/lib/shared/src/sourcegraph-api/graphql/queries.ts index 40608577916..fa8138919de 100644 --- a/lib/shared/src/sourcegraph-api/graphql/queries.ts +++ b/lib/shared/src/sourcegraph-api/graphql/queries.ts @@ -36,6 +36,15 @@ query CurrentUser { } }` +export const CURRENT_SITE_CODY_LLM_PROVIDER = ` +query CurrentSiteCodyLlmConfiguration { + site { + codyLLMConfiguration { + provider + } + } +}` + export const CURRENT_SITE_CODY_LLM_CONFIGURATION = ` query CurrentSiteCodyLlmConfiguration { site { diff --git a/vscode/src/completions/createVSCodeInlineCompletionItemProvider.ts b/vscode/src/completions/createVSCodeInlineCompletionItemProvider.ts index 8d66f105bd0..62516e88b19 100644 --- a/vscode/src/completions/createVSCodeInlineCompletionItemProvider.ts +++ b/vscode/src/completions/createVSCodeInlineCompletionItemProvider.ts @@ -43,7 +43,7 @@ export async function createInlineCompletionItemProvider({ const disposables: vscode.Disposable[] = [] const [providerConfig, graphContextFlag] = await Promise.all([ - createProviderConfig(config, client, featureFlagProvider), + createProviderConfig(config, client, featureFlagProvider, authProvider.getAuthStatus().configOverwrites), featureFlagProvider?.evaluateFeatureFlag(FeatureFlag.CodyAutocompleteGraphContext), ]) if (providerConfig) { diff --git a/vscode/src/completions/providers/createProvider.test.ts b/vscode/src/completions/providers/createProvider.test.ts new file mode 100644 index 00000000000..f6f41a000b7 --- /dev/null +++ b/vscode/src/completions/providers/createProvider.test.ts @@ -0,0 +1,264 @@ +import { describe, expect, it } from 'vitest' + +import { Configuration } from '@sourcegraph/cody-shared/src/configuration' +import { DOTCOM_URL } from '@sourcegraph/cody-shared/src/sourcegraph-api/environments' +import { CodyLLMSiteConfiguration } from '@sourcegraph/cody-shared/src/sourcegraph-api/graphql/client' + +import { CodeCompletionsClient } from '../client' + +import { createProviderConfig } from './createProvider' + +const DEFAULT_VSCODE_SETTINGS: Configuration = { + serverEndpoint: DOTCOM_URL.href, + proxy: null, + codebase: '', + customHeaders: {}, + chatPreInstruction: 'My name is John Doe.', + useContext: 'embeddings', + autocomplete: true, + experimentalCommandLenses: false, + experimentalEditorTitleCommandIcon: false, + experimentalChatPredictions: false, + experimentalGuardrails: false, + experimentalLocalSymbols: false, + inlineChat: true, + isRunningInsideAgent: false, + experimentalNonStop: false, + experimentalSymfAnthropicKey: '', + experimentalSymfPath: 'symf', + debugEnable: false, + debugVerbose: false, + debugFilter: null, + telemetryLevel: 'all', + autocompleteAdvancedProvider: null, + autocompleteAdvancedServerEndpoint: null, + autocompleteAdvancedModel: null, + autocompleteAdvancedAccessToken: null, + autocompleteAdvancedEmbeddings: true, + autocompleteExperimentalCompleteSuggestWidgetSelection: false, + autocompleteExperimentalSyntacticPostProcessing: false, + autocompleteExperimentalGraphContext: false, +} + +const getVSCodeSettings = (config: Partial = {}): Configuration => ({ + ...DEFAULT_VSCODE_SETTINGS, + ...config, +}) + +const dummyCodeCompletionsClient: CodeCompletionsClient = { + complete: () => Promise.resolve({ completion: '', stopReason: '' }), + onConfigurationChange: () => undefined, +} + +describe('createProviderConfig', () => { + describe('if completions provider fields are defined in VSCode settings', () => { + it('returns null if completions provider is not supported', async () => { + const provider = await createProviderConfig( + getVSCodeSettings({ + autocompleteAdvancedProvider: 'nasa-ai' as Configuration['autocompleteAdvancedProvider'], + }), + dummyCodeCompletionsClient, + undefined, + {} + ) + expect(provider).toBeNull() + }) + + it('returns "codegen" provider config if the corresponding provider name and endpoint are specified', async () => { + const provider = await createProviderConfig( + getVSCodeSettings({ + autocompleteAdvancedProvider: 'unstable-codegen', + autocompleteAdvancedServerEndpoint: 'https://unstable-codegen.com', + }), + dummyCodeCompletionsClient, + undefined, + {} + ) + expect(provider?.identifier).toBe('codegen') + expect(provider?.model).toBe('codegen') + }) + + it('returns null if provider is "unstable-codegen", but the server endpoint is not set', async () => { + const provider = await createProviderConfig( + getVSCodeSettings({ autocompleteAdvancedProvider: 'unstable-codegen' }), + dummyCodeCompletionsClient, + undefined, + {} + ) + expect(provider).toBeNull() + }) + + it('returns "fireworks" provider config and corresponding model if specified', async () => { + const provider = await createProviderConfig( + getVSCodeSettings({ + autocompleteAdvancedProvider: 'unstable-fireworks', + autocompleteAdvancedModel: 'starcoder-3b', + }), + dummyCodeCompletionsClient, + undefined, + {} + ) + expect(provider?.identifier).toBe('fireworks') + expect(provider?.model).toBe('starcoder-3b') + }) + + it('returns "fireworks" provider config if specified in settings and default model', async () => { + const provider = await createProviderConfig( + getVSCodeSettings({ autocompleteAdvancedProvider: 'unstable-fireworks' }), + dummyCodeCompletionsClient, + undefined, + {} + ) + expect(provider?.identifier).toBe('fireworks') + expect(provider?.model).toBe('starcoder-7b') + }) + + it('returns "openai" provider config if specified in VSCode settings; model is ignored', async () => { + const provider = await createProviderConfig( + getVSCodeSettings({ + autocompleteAdvancedProvider: 'unstable-openai', + autocompleteAdvancedModel: 'hello-world', + }), + dummyCodeCompletionsClient, + undefined, + {} + ) + expect(provider?.identifier).toBe('unstable-openai') + expect(provider?.model).toBe('gpt-35-turbo') + }) + + it('returns "anthropic" provider config if specified in VSCode settings; model is ignored', async () => { + const provider = await createProviderConfig( + getVSCodeSettings({ + autocompleteAdvancedProvider: 'anthropic', + autocompleteAdvancedModel: 'hello-world', + }), + dummyCodeCompletionsClient, + undefined, + {} + ) + expect(provider?.identifier).toBe('anthropic') + expect(provider?.model).toBe('claude-instant-1') + }) + + it('provider specified in VSCode settings takes precedence over the one defined in the site config', async () => { + const provider = await createProviderConfig( + getVSCodeSettings({ + autocompleteAdvancedProvider: 'unstable-codegen', + autocompleteAdvancedServerEndpoint: 'https://unstable-codegen.com', + }), + dummyCodeCompletionsClient, + undefined, + { provider: 'azure-open-ai', completionModel: 'gpt-35-turbo-test' } + ) + expect(provider?.identifier).toBe('codegen') + expect(provider?.model).toBe('codegen') + }) + }) + + describe('completions provider and model are defined in the site config and not set in VSCode settings', () => { + describe('if provider is "sourcegraph"', () => { + const testCases: { + codyLLMConfig: CodyLLMSiteConfiguration + expected: { provider: string; model?: string } | null + }[] = [ + // sourcegraph + { codyLLMConfig: { provider: 'sourcegraph', completionModel: 'hello-world' }, expected: null }, + { + codyLLMConfig: { provider: 'sourcegraph', completionModel: 'anthropic/claude-instant-1' }, + expected: { provider: 'anthropic', model: 'claude-instant-1' }, + }, + { + codyLLMConfig: { provider: 'sourcegraph', completionModel: 'anthropic/' }, + expected: null, + }, + { + codyLLMConfig: { provider: 'sourcegraph', completionModel: '/claude-instant-1' }, + expected: null, + }, + + // aws-bedrock + { codyLLMConfig: { provider: 'aws-bedrock', completionModel: 'hello-world' }, expected: null }, + { + codyLLMConfig: { provider: 'aws-bedrock', completionModel: 'anthropic.claude-instant-1' }, + expected: { provider: 'anthropic', model: 'claude-instant-1' }, + }, + { + codyLLMConfig: { provider: 'aws-bedrock', completionModel: 'anthropic.' }, + expected: null, + }, + { + codyLLMConfig: { provider: 'aws-bedrock', completionModel: 'anthropic/claude-instant-1' }, + expected: null, + }, + + // open-ai + { + codyLLMConfig: { provider: 'openai', completionModel: 'gpt-35-turbo-test' }, + expected: { provider: 'unstable-openai', model: 'gpt-35-turbo-test' }, + }, + { + codyLLMConfig: { provider: 'openai' }, + expected: { provider: 'unstable-openai', model: 'gpt-35-turbo' }, + }, + + // azure-openai + { + codyLLMConfig: { provider: 'azure-openai', completionModel: 'gpt-35-turbo-test' }, + expected: { provider: 'unstable-openai', model: '' }, + }, + { + codyLLMConfig: { provider: 'azure-openai' }, + expected: { provider: 'unstable-openai', model: 'gpt-35-turbo' }, + }, + + // fireworks + { + codyLLMConfig: { provider: 'fireworks', completionModel: 'llama-code-7b' }, + expected: { provider: 'fireworks', model: 'llama-code-7b' }, + }, + { + codyLLMConfig: { provider: 'fireworks' }, + expected: { provider: 'fireworks', model: 'starcoder-7b' }, + }, + + // unknown-provider + { + codyLLMConfig: { provider: 'unknown-provider', completionModel: 'llama-code-7b' }, + expected: null, + }, + + // provider not defined (backward compat) + { + codyLLMConfig: { provider: undefined, completionModel: 'llama-code-7b' }, + expected: { provider: 'anthropic', model: 'claude-instant-1' }, + }, + ] + + for (const { codyLLMConfig, expected } of testCases) { + it(`returns ${JSON.stringify(expected)} when cody LLM config is ${JSON.stringify( + codyLLMConfig + )}`, async () => { + const provider = await createProviderConfig( + getVSCodeSettings(), + dummyCodeCompletionsClient, + undefined, + codyLLMConfig + ) + if (expected === null) { + expect(provider).toBeNull() + } else { + expect(provider?.identifier).toBe(expected.provider) + expect(provider?.model).toBe(expected.model) + } + }) + } + }) + }) + + it('returns anthropic provider config if no completions provider specified in VSCode settings or site config', async () => { + const provider = await createProviderConfig(getVSCodeSettings(), dummyCodeCompletionsClient, undefined, {}) + expect(provider?.identifier).toBe('anthropic') + expect(provider?.model).toBe('claude-instant-1') + }) +}) diff --git a/vscode/src/completions/providers/createProvider.ts b/vscode/src/completions/providers/createProvider.ts index 924bec69472..9b782f2fe86 100644 --- a/vscode/src/completions/providers/createProvider.ts +++ b/vscode/src/completions/providers/createProvider.ts @@ -1,5 +1,6 @@ import { Configuration } from '@sourcegraph/cody-shared/src/configuration' import { FeatureFlag, FeatureFlagProvider } from '@sourcegraph/cody-shared/src/experimentation/FeatureFlagProvider' +import { CodyLLMSiteConfiguration } from '@sourcegraph/cody-shared/src/sourcegraph-api/graphql/client' import { logError } from '../../log' import { CodeCompletionsClient } from '../client' @@ -13,53 +14,114 @@ import { createProviderConfig as createUnstableOpenAIProviderConfig } from './un export async function createProviderConfig( config: Configuration, client: CodeCompletionsClient, - featureFlagProvider?: FeatureFlagProvider + featureFlagProvider?: FeatureFlagProvider, + codyLLMSiteConfig?: CodyLLMSiteConfiguration ): Promise { - const { provider, model } = await resolveDefaultProvider(config.autocompleteAdvancedProvider, featureFlagProvider) - switch (provider) { - case 'unstable-codegen': { - if (config.autocompleteAdvancedServerEndpoint !== null) { - return createUnstableCodeGenProviderConfig(config.autocompleteAdvancedServerEndpoint) + const defaultAnthropicProviderConfig = createAnthropicProviderConfig({ + client, + contextWindowTokens: 2048, + mode: config.autocompleteAdvancedModel === 'claude-instant-infill' ? 'infill' : 'default', + }) + + /** + * Look for the autocomplete provider in VSCode settings and return matching provider config. + */ + const providerAndModelFromVSCodeConfig = await resolveDefaultProviderFromVSCodeConfigOrFeatureFlags( + config.autocompleteAdvancedProvider, + featureFlagProvider + ) + if (providerAndModelFromVSCodeConfig) { + const { provider, model } = providerAndModelFromVSCodeConfig + + switch (provider) { + case 'unstable-codegen': { + if (config.autocompleteAdvancedServerEndpoint !== null) { + return createUnstableCodeGenProviderConfig(config.autocompleteAdvancedServerEndpoint) + } + + logError( + 'createProviderConfig', + 'Provider `unstable-codegen` can not be used without configuring `cody.autocomplete.advanced.serverEndpoint`.' + ) + return null + } + case 'unstable-openai': { + return createUnstableOpenAIProviderConfig({ + client, + contextWindowTokens: 2048, + }) + } + case 'unstable-fireworks': { + return createUnstableFireworksProviderConfig({ + client, + model: config.autocompleteAdvancedModel ?? model ?? null, + }) + } + case 'anthropic': { + return defaultAnthropicProviderConfig } + default: + logError( + 'createProviderConfig', + `Unrecognized provider '${config.autocompleteAdvancedProvider}' configured.` + ) + return null + } + } + /** + * If autocomplete provider is not defined in the VSCode settings, + * check the completions provider in the connected Sourcegraph instance site config + * and return the matching provider config. + */ + if (codyLLMSiteConfig?.provider) { + const parsed = parseProviderAndModel({ + provider: codyLLMSiteConfig.provider, + model: codyLLMSiteConfig.completionModel, + }) + if (!parsed) { logError( 'createProviderConfig', - 'Provider `unstable-codegen` can not be used without configuring `cody.autocomplete.advanced.serverEndpoint`.' + `Failed to parse the model name for '${codyLLMSiteConfig.provider}' completions provider.` ) return null } - case 'unstable-openai': { - return createUnstableOpenAIProviderConfig({ - client, - contextWindowTokens: 2048, - }) - } - case 'unstable-fireworks': { - return createUnstableFireworksProviderConfig({ - client, - model: config.autocompleteAdvancedModel ?? model ?? null, - }) - } - case 'anthropic': { - return createAnthropicProviderConfig({ - client, - contextWindowTokens: 2048, - mode: config.autocompleteAdvancedModel === 'claude-instant-infill' ? 'infill' : 'default', - }) + const { provider, model } = parsed + switch (provider) { + case 'openai': + case 'azure-openai': + return createUnstableOpenAIProviderConfig({ + client, + contextWindowTokens: 2048, + // Model name for azure openai provider is a deployment name. It shouldn't appear in logs. + model: provider === 'azure-openai' && model ? '' : model, + }) + + case 'fireworks': + return createUnstableFireworksProviderConfig({ + client, + model: model ?? null, + }) + case 'aws-bedrock': + case 'anthropic': + return defaultAnthropicProviderConfig + default: + logError('createProviderConfig', `Unrecognized provider '${provider}' configured.`) + return null } - default: - logError( - 'createProviderConfig', - `Unrecognized provider '${config.autocompleteAdvancedProvider}' configured.` - ) - return null } + + /** + * If autocomplete provider is not defined neither in VSCode nor in Sourcegraph instance site config, + * use the default provider config ("anthropic"). + */ + return defaultAnthropicProviderConfig } -async function resolveDefaultProvider( +async function resolveDefaultProviderFromVSCodeConfigOrFeatureFlags( configuredProvider: string | null, featureFlagProvider?: FeatureFlagProvider -): Promise<{ provider: string; model?: 'starcoder-7b' | 'starcoder-16b' | 'claude-instant-infill' }> { +): Promise<{ provider: string; model?: 'starcoder-7b' | 'starcoder-16b' | 'claude-instant-infill' } | null> { if (configuredProvider) { return { provider: configuredProvider } } @@ -78,5 +140,45 @@ async function resolveDefaultProvider( return { provider: 'anthropic', model: 'claude-instant-infill' } } - return { provider: 'anthropic' } + return null +} + +const delimeters: 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 delimeter (see {@link delimeters}). + * + * 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, + model, +}: { + provider: string + model?: string +}): { provider: string; model?: string } | null { + const delimeter = delimeters[provider] + if (!delimeter) { + return { provider, model } + } + + if (model) { + const index = model.indexOf(delimeter) + const parsedProvider = model.slice(0, index) + const parsedModel = model.slice(index + 1) + if (parsedProvider && parsedModel) { + return { provider: parsedProvider, model: parsedModel } + } + } + + return null } diff --git a/vscode/src/completions/providers/unstable-openai.ts b/vscode/src/completions/providers/unstable-openai.ts index aa306ea9706..b29fbe81441 100644 --- a/vscode/src/completions/providers/unstable-openai.ts +++ b/vscode/src/completions/providers/unstable-openai.ts @@ -153,7 +153,10 @@ export class UnstableOpenAIProvider extends Provider { } } -export function createProviderConfig(unstableAzureOpenAIOptions: UnstableOpenAIOptions): ProviderConfig { +export function createProviderConfig({ + model, + ...unstableAzureOpenAIOptions +}: UnstableOpenAIOptions & { model?: string }): ProviderConfig { return { create(options: ProviderOptions) { return new UnstableOpenAIProvider(options, { ...unstableAzureOpenAIOptions }) @@ -161,6 +164,6 @@ export function createProviderConfig(unstableAzureOpenAIOptions: UnstableOpenAIO maximumContextCharacters: tokensToChars(unstableAzureOpenAIOptions.contextWindowTokens), enableExtendedMultilineTriggers: false, identifier: PROVIDER_IDENTIFIER, - model: 'gpt-35-turbo', + model: model ?? 'gpt-35-turbo', } } diff --git a/vscode/src/completions/vscodeInlineCompletionItemProvider.ts b/vscode/src/completions/vscodeInlineCompletionItemProvider.ts index 3b052324720..f984d75a566 100644 --- a/vscode/src/completions/vscodeInlineCompletionItemProvider.ts +++ b/vscode/src/completions/vscodeInlineCompletionItemProvider.ts @@ -113,7 +113,7 @@ export class InlineCompletionItemProvider implements vscode.InlineCompletionItem logDebug( 'CodyCompletionProvider:initialized', - `${this.config.providerConfig.identifier}/${this.config.providerConfig.model}` + [this.config.providerConfig.identifier, this.config.providerConfig.model].join('/') ) }