From cf8df67a201955495f74d6ad511f9231d48337dd Mon Sep 17 00:00:00 2001 From: Stefan Dirix Date: Fri, 11 Oct 2024 15:28:51 +0200 Subject: [PATCH] feat: support custom keys for custom Open AI models The configuration for custom OpenAI models now allows specifying a unique 'apiKey' for each model, or reusing the global OpenAI API key. fixes #14288 --- packages/ai-openai/README.md | 20 +++++++++++++++++- ...penai-frontend-application-contribution.ts | 12 ++++++----- .../src/browser/openai-preferences.ts | 13 ++++++++++-- .../common/openai-language-models-manager.ts | 4 ++++ .../src/node/openai-language-model.ts | 8 +++---- .../openai-language-models-manager-impl.ts | 21 ++++++++++++------- 6 files changed, 58 insertions(+), 20 deletions(-) diff --git a/packages/ai-openai/README.md b/packages/ai-openai/README.md index 679035fe6b435..1ec7facb4a1b0 100644 --- a/packages/ai-openai/README.md +++ b/packages/ai-openai/README.md @@ -14,7 +14,25 @@ The `@theia/ai-openai` integrates OpenAI's models with Theia AI. The OpenAI API key and the models to use can be configured via preferences. -Alternatively the OpenAI API key can also be handed in via an environment variable. +Alternatively the OpenAI API key can also be handed in via the `OPENAI_API_KEY` variable. + +### Custom models + +The extension also supports OpenAI compatible models hosted on different end points. +You can configure the end points via the `ai-features.openAiCustom.customOpenAiModels` preference: + +```ts +{ + model: string + url: string + id?: string + apiKey?: string | true +} +``` + +- `model` and `url` are mandatory attributes, indicating the end point and model to use +- `id` is an optional attribute which is used in the UI to refer to this configuration +- `apiKey` is either the key to access the API served at the given URL or `true` to use the global OpenAI API key. If not given 'no-key' will be used. ## Additional Information diff --git a/packages/ai-openai/src/browser/openai-frontend-application-contribution.ts b/packages/ai-openai/src/browser/openai-frontend-application-contribution.ts index 9295eb9cede17..b16f80aa0ef90 100644 --- a/packages/ai-openai/src/browser/openai-frontend-application-contribution.ts +++ b/packages/ai-openai/src/browser/openai-frontend-application-contribution.ts @@ -64,7 +64,7 @@ export class OpenAiFrontendApplicationContribution implements FrontendApplicatio const modelsToRemove = oldModels.filter(model => !newModels.some(newModel => newModel.id === model.id)); const modelsToAddOrUpdate = newModels.filter(newModel => !oldModels.some(model => - model.id === newModel.id && model.model === newModel.model && model.url === newModel.url)); + model.id === newModel.id && model.model === newModel.model && model.url === newModel.url && model.apiKey === newModel.apiKey)); this.manager.removeLanguageModels(...modelsToRemove.map(model => model.id)); this.manager.createOrUpdateLanguageModels(...modelsToAddOrUpdate); @@ -77,21 +77,23 @@ export class OpenAiFrontendApplicationContribution implements FrontendApplicatio function createOpenAIModelDescription(modelId: string): OpenAiModelDescription { return { id: `openai/${modelId}`, - model: modelId + model: modelId, + apiKey: true }; } function createCustomModelDescriptionsFromPreferences(preferences: Partial[]): OpenAiModelDescription[] { return preferences.reduce((acc, pref) => { - if (!pref.model || !pref.url) { + if (!pref.model || !pref.url || typeof pref.model !== 'string' || typeof pref.url !== 'string') { return acc; } return [ ...acc, { - id: pref.id ?? pref.model, + id: pref.id && typeof pref.id === 'string' ? pref.id : pref.model, model: pref.model, - url: pref.url + url: pref.url, + apiKey: typeof pref.apiKey === 'string' || pref.apiKey === true ? pref.apiKey : undefined } ]; }, []); diff --git a/packages/ai-openai/src/browser/openai-preferences.ts b/packages/ai-openai/src/browser/openai-preferences.ts index 0b2eab0d4f3bf..ce937578ce623 100644 --- a/packages/ai-openai/src/browser/openai-preferences.ts +++ b/packages/ai-openai/src/browser/openai-preferences.ts @@ -43,7 +43,12 @@ export const OpenAiPreferencesSchema: PreferenceSchema = { type: 'array', title: AI_CORE_PREFERENCES_TITLE, markdownDescription: 'Integrate custom models compatible with the OpenAI API, for example via `vllm`. The required attributes are `model` and `url`.\ - Optionally, you can provide a unique `id` to identify the custom model in the UI. If none is given `model` will be used as `id`.', + \n\ + Optionally, you can\ + \n\ + - specify a unique `id` to identify the custom model in the UI. If none is given `model` will be used as `id`.\ + \n\ + - provide an `apiKey` to access the API served at the given url. Use `true` to indicate the use of the global OpenAI API key.', default: [], items: { type: 'object', @@ -59,7 +64,11 @@ export const OpenAiPreferencesSchema: PreferenceSchema = { id: { type: 'string', title: 'A unique identifier which is used in the UI to identify the custom model', - } + }, + apiKey: { + type: ['string', 'boolean'], + title: 'Either the key to access the API served at the given url or `true` to use the global OpenAI API key', + }, } } } diff --git a/packages/ai-openai/src/common/openai-language-models-manager.ts b/packages/ai-openai/src/common/openai-language-models-manager.ts index bf5ad33ca3950..363b2552dc38b 100644 --- a/packages/ai-openai/src/common/openai-language-models-manager.ts +++ b/packages/ai-openai/src/common/openai-language-models-manager.ts @@ -28,6 +28,10 @@ export interface OpenAiModelDescription { * The OpenAI API compatible endpoint where the model is hosted. If not provided the default OpenAI endpoint will be used. */ url?: string; + /** + * The key for the model. If 'true' is provided the global OpenAI API key will be used. + */ + apiKey: string | true | undefined; } export interface OpenAiLanguageModelsManager { apiKey: string | undefined; diff --git a/packages/ai-openai/src/node/openai-language-model.ts b/packages/ai-openai/src/node/openai-language-model.ts index 7692a21f8a9e4..7d47f72d99584 100644 --- a/packages/ai-openai/src/node/openai-language-model.ts +++ b/packages/ai-openai/src/node/openai-language-model.ts @@ -55,7 +55,7 @@ export class OpenAiModel implements LanguageModel { * @param model the model id as it is used by the OpenAI API * @param openAIInitializer initializer for the OpenAI client, used for each request. */ - constructor(public readonly id: string, public model: string, protected apiKey: (() => string | undefined) | undefined, public url: string | undefined) { } + constructor(public readonly id: string, public model: string, public apiKey: () => string | undefined, public url: string | undefined) { } async request(request: LanguageModelRequest, cancellationToken?: CancellationToken): Promise { const openai = this.initializeOpenAi(); @@ -180,11 +180,11 @@ export class OpenAiModel implements LanguageModel { } protected initializeOpenAi(): OpenAI { - const apiKey = this.apiKey && this.apiKey(); + const apiKey = this.apiKey(); if (!apiKey && !(this.url)) { throw new Error('Please provide OPENAI_API_KEY in preferences or via environment variable'); } - // do not hand over API key to custom urls - return new OpenAI({ apiKey: this.url ? 'no-key' : apiKey, baseURL: this.url }); + // We need to hand over "some" key, even if a custom url is not key protected as otherwise the OpenAI client will throw an error + return new OpenAI({ apiKey: apiKey ?? 'no-key', baseURL: this.url }); } } diff --git a/packages/ai-openai/src/node/openai-language-models-manager-impl.ts b/packages/ai-openai/src/node/openai-language-models-manager-impl.ts index d0f60856fb2ab..cfc81ba3b8adb 100644 --- a/packages/ai-openai/src/node/openai-language-models-manager-impl.ts +++ b/packages/ai-openai/src/node/openai-language-models-manager-impl.ts @@ -36,6 +36,15 @@ export class OpenAiLanguageModelsManagerImpl implements OpenAiLanguageModelsMana async createOrUpdateLanguageModels(...modelDescriptions: OpenAiModelDescription[]): Promise { for (const modelDescription of modelDescriptions) { const model = await this.languageModelRegistry.getLanguageModel(modelDescription.id); + const apiKeyProvider = () => { + if (modelDescription.apiKey === true) { + return this.apiKey; + } + if (modelDescription.apiKey) { + return modelDescription.apiKey; + } + return undefined; + }; if (model) { if (!(model instanceof OpenAiModel)) { console.warn(`Open AI: model ${modelDescription.id} is not an OpenAI model`); @@ -46,15 +55,11 @@ export class OpenAiLanguageModelsManagerImpl implements OpenAiLanguageModelsMana console.info(`Open AI: skip creating model ${modelDescription.id} because it already exists`); continue; } - if (model.url !== modelDescription.url || model.model !== modelDescription.model) { - model.url = modelDescription.url; - model.model = modelDescription.model; - } else { - // This can happen during the initializing of more than one frontends. - console.info(`Open AI: skip creating or updating model ${modelDescription.id} because it already exists and is up to date`); - } + model.url = modelDescription.url; + model.model = modelDescription.model; + model.apiKey = apiKeyProvider; } else { - this.languageModelRegistry.addLanguageModels([new OpenAiModel(modelDescription.id, modelDescription.model, () => this.apiKey, modelDescription.url)]); + this.languageModelRegistry.addLanguageModels([new OpenAiModel(modelDescription.id, modelDescription.model, apiKeyProvider, modelDescription.url)]); } } }