Skip to content

Commit 52d9949

Browse files
authored
fix: Support Azure OpenAI (#14722)
* Add configuration of OpenAI API version * Add configuration of supportsDeveloperMessage * If OpenAI API version is configured, use AzureOpenAI Fixes #14711
1 parent d8022a1 commit 52d9949

6 files changed

+123
-11
lines changed

packages/ai-openai/README.md

+54
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,66 @@ You can configure the end points via the `ai-features.openAiCustom.customOpenAiM
2727
url: string
2828
id?: string
2929
apiKey?: string | true
30+
apiVersion?: string | true
31+
supportsDeveloperMessage?: boolean
32+
enableStreaming?: boolean
3033
}
3134
```
3235

3336
- `model` and `url` are mandatory attributes, indicating the end point and model to use
3437
- `id` is an optional attribute which is used in the UI to refer to this configuration
3538
- `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.
39+
- `apiVersion` is either the api version to access the API served at the given URL in Azure or `true` to use the global OpenAI API version.
40+
- `supportsDeveloperMessage` is a flag that indicates whether the model supports the `developer` role or not. `true` by default.
41+
- `enableStreaming` is a flag that indicates whether the streaming API shall be used or not. `true` by default.
42+
43+
### Azure OpenAI
44+
45+
To use a custom OpenAI model hosted on Azure, the `AzureOpenAI` class needs to be used, as described in the
46+
[openai-node docs](https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai).
47+
48+
Requests to an OpenAI model hosted on Azure need an `apiVersion`. To configure a custom OpenAI model in Theia you therefore need to configure the `apiVersion` with the end point.
49+
Note that if you don't configure an `apiVersion`, the default `OpenAI` object is used for initialization and a connection to an Azure hosted OpenAI model will fail.
50+
51+
An OpenAI model version deployed on Azure might not support the `developer` role. In that case it is possible to configure whether the `developer` role is supported or not via the
52+
`supportsDeveloperMessage` option, which defaults to `true`.
53+
54+
The following snippet shows a possible configuration to access an OpenAI model hosted on Azure. The `AZURE_OPENAI_API_BASE_URL` needs to be given without the `/chat/completions`
55+
path and without the `api-version` parameter, e.g. _`https://<my_prefix>.openai.azure.com/openai/deployments/<my_deployment>`_
56+
57+
```json
58+
{
59+
"ai-features.AiEnable.enableAI": true,
60+
"ai-features.openAiCustom.customOpenAiModels": [
61+
{
62+
"model": "gpt4o",
63+
"url": "<AZURE_OPENAI_API_BASE_URL>",
64+
"id": "azure-deployment",
65+
"apiKey": "<AZURE_OPENAI_API_KEY>",
66+
"apiVersion": "<AZURE_OPENAI_API_VERSION>",
67+
"supportsDeveloperMessage": false
68+
}
69+
],
70+
"ai-features.agentSettings": {
71+
"Universal": {
72+
"languageModelRequirements": [
73+
{
74+
"purpose": "chat",
75+
"identifier": "azure-deployment"
76+
}
77+
]
78+
},
79+
"Orchestrator": {
80+
"languageModelRequirements": [
81+
{
82+
"purpose": "agent-selection",
83+
"identifier": "azure-deployment"
84+
}
85+
]
86+
}
87+
}
88+
}
89+
```
3690

3791
## Additional Information
3892

packages/ai-openai/src/browser/openai-frontend-application-contribution.ts

+7
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ export class OpenAiFrontendApplicationContribution implements FrontendApplicatio
9191
model.model === newModel.model &&
9292
model.url === newModel.url &&
9393
model.apiKey === newModel.apiKey &&
94+
model.apiVersion === newModel.apiVersion &&
95+
model.supportsDeveloperMessage === newModel.supportsDeveloperMessage &&
9496
model.enableStreaming === newModel.enableStreaming));
9597

9698
this.manager.removeLanguageModels(...modelsToRemove.map(model => model.id));
@@ -113,6 +115,8 @@ export class OpenAiFrontendApplicationContribution implements FrontendApplicatio
113115
id: id,
114116
model: modelId,
115117
apiKey: true,
118+
apiVersion: true,
119+
supportsDeveloperMessage: !openAIModelsSupportingDeveloperMessages.includes(modelId),
116120
enableStreaming: !openAIModelsWithDisabledStreaming.includes(modelId),
117121
defaultRequestSettings: modelRequestSetting?.requestSettings
118122
};
@@ -136,6 +140,8 @@ export class OpenAiFrontendApplicationContribution implements FrontendApplicatio
136140
model: pref.model,
137141
url: pref.url,
138142
apiKey: typeof pref.apiKey === 'string' || pref.apiKey === true ? pref.apiKey : undefined,
143+
apiVersion: typeof pref.apiVersion === 'string' || pref.apiVersion === true ? pref.apiVersion : undefined,
144+
supportsDeveloperMessage: pref.supportsDeveloperMessage ?? true,
139145
enableStreaming: pref.enableStreaming ?? true,
140146
defaultRequestSettings: modelRequestSetting?.requestSettings
141147
}
@@ -160,3 +166,4 @@ export class OpenAiFrontendApplicationContribution implements FrontendApplicatio
160166
}
161167

162168
const openAIModelsWithDisabledStreaming = ['o1-preview', 'o1-mini'];
169+
const openAIModelsSupportingDeveloperMessages = ['o1-preview', 'o1-mini'];

packages/ai-openai/src/browser/openai-preferences.ts

+12
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ export const OpenAiPreferencesSchema: PreferenceSchema = {
5050
\n\
5151
- provide an `apiKey` to access the API served at the given url. Use `true` to indicate the use of the global OpenAI API key.\
5252
\n\
53+
- provide an `apiVersion` to access the API served at the given url in Azure. Use `true` to indicate the use of the global OpenAI API version.\
54+
\n\
55+
- specify `supportsDeveloperMessage: false` to indicate that the developer role shall not be used.\
56+
\n\
5357
- specify `enableStreaming: false` to indicate that streaming shall not be used.\
5458
\n\
5559
Refer to [our documentation](https://theia-ide.org/docs/user_ai/#openai-compatible-models-eg-via-vllm) for more information.',
@@ -73,6 +77,14 @@ export const OpenAiPreferencesSchema: PreferenceSchema = {
7377
type: ['string', 'boolean'],
7478
title: 'Either the key to access the API served at the given url or `true` to use the global OpenAI API key',
7579
},
80+
apiVersion: {
81+
type: ['string', 'boolean'],
82+
title: 'Either the version to access the API served at the given url in Azure or `true` to use the global OpenAI API version',
83+
},
84+
supportsDeveloperMessage: {
85+
type: 'boolean',
86+
title: 'Indicates whether the model supports the `developer` role. `true` by default.',
87+
},
7688
enableStreaming: {
7789
type: 'boolean',
7890
title: 'Indicates whether the streaming API shall be used. `true` by default.',

packages/ai-openai/src/common/openai-language-models-manager.ts

+9
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,18 @@ export interface OpenAiModelDescription {
3232
* The key for the model. If 'true' is provided the global OpenAI API key will be used.
3333
*/
3434
apiKey: string | true | undefined;
35+
/**
36+
* The version for the api. If 'true' is provided the global OpenAI version will be used.
37+
*/
38+
apiVersion: string | true | undefined;
3539
/**
3640
* Indicate whether the streaming API shall be used.
3741
*/
3842
enableStreaming: boolean;
43+
/**
44+
* Flag to configure whether the OpenAPI model supports the `developer` role. Default is `true`.
45+
*/
46+
supportsDeveloperMessage: boolean;
3947
/**
4048
* Default request settings for the OpenAI model.
4149
*/
@@ -44,6 +52,7 @@ export interface OpenAiModelDescription {
4452
export interface OpenAiLanguageModelsManager {
4553
apiKey: string | undefined;
4654
setApiKey(key: string | undefined): void;
55+
setApiVersion(version: string | undefined): void;
4756
createOrUpdateLanguageModels(...models: OpenAiModelDescription[]): Promise<void>;
4857
removeLanguageModels(...modelIds: string[]): void
4958
}

packages/ai-openai/src/node/openai-language-model.ts

+15-11
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
LanguageModelTextResponse
2525
} from '@theia/ai-core';
2626
import { CancellationToken } from '@theia/core';
27-
import OpenAI from 'openai';
27+
import { OpenAI, AzureOpenAI } from 'openai';
2828
import { ChatCompletionStream } from 'openai/lib/ChatCompletionStream';
2929
import { RunnableToolFunctionWithoutParse } from 'openai/lib/RunnableFunction';
3030
import { ChatCompletionMessageParam } from 'openai/resources';
@@ -38,6 +38,8 @@ export class OpenAiModel implements LanguageModel {
3838
* @param model the model id as it is used by the OpenAI API
3939
* @param enableStreaming whether the streaming API shall be used
4040
* @param apiKey a function that returns the API key to use for this model, called on each request
41+
* @param apiVersion a function that returns the OpenAPI version to use for this model, called on each request
42+
* @param supportsDeveloperMessage whether the model supports the `developer` role
4143
* @param url the OpenAI API compatible endpoint where the model is hosted. If not provided the default OpenAI endpoint will be used.
4244
* @param defaultRequestSettings optional default settings for requests made using this model.
4345
*/
@@ -46,6 +48,8 @@ export class OpenAiModel implements LanguageModel {
4648
public model: string,
4749
public enableStreaming: boolean,
4850
public apiKey: () => string | undefined,
51+
public apiVersion: () => string | undefined,
52+
public supportsDeveloperMessage: boolean,
4953
public url: string | undefined,
5054
public defaultRequestSettings?: { [key: string]: unknown }
5155
) { }
@@ -164,7 +168,7 @@ export class OpenAiModel implements LanguageModel {
164168
protected toOpenAiRole(message: LanguageModelRequestMessage): 'developer' | 'user' | 'assistant' {
165169
switch (message.actor) {
166170
case 'system':
167-
return this.supportsDeveloperMessage() ? 'developer' : 'user';
171+
return this.supportsDeveloperMessage ? 'developer' : 'user';
168172
case 'ai':
169173
return 'assistant';
170174
default:
@@ -185,13 +189,6 @@ export class OpenAiModel implements LanguageModel {
185189
].includes(this.model);
186190
}
187191

188-
protected supportsDeveloperMessage(): boolean {
189-
return ![
190-
'o1-preview',
191-
'o1-mini'
192-
].includes(this.model);
193-
}
194-
195192
protected async handleStructuredOutputRequest(openai: OpenAI, request: LanguageModelRequest): Promise<LanguageModelParsedResponse> {
196193
const settings = this.getSettings(request);
197194
// TODO implement tool support for structured output (parse() seems to require different tool format)
@@ -235,7 +232,14 @@ export class OpenAiModel implements LanguageModel {
235232
if (!apiKey && !(this.url)) {
236233
throw new Error('Please provide OPENAI_API_KEY in preferences or via environment variable');
237234
}
238-
// 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
239-
return new OpenAI({ apiKey: apiKey ?? 'no-key', baseURL: this.url });
235+
236+
const apiVersion = this.apiVersion();
237+
if (apiVersion) {
238+
// 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
239+
return new AzureOpenAI({ apiKey: apiKey ?? 'no-key', baseURL: this.url, apiVersion: apiVersion });
240+
} else {
241+
// 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
242+
return new OpenAI({ apiKey: apiKey ?? 'no-key', baseURL: this.url });
243+
}
240244
}
241245
}

packages/ai-openai/src/node/openai-language-models-manager-impl.ts

+26
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { OpenAiLanguageModelsManager, OpenAiModelDescription } from '../common';
2323
export class OpenAiLanguageModelsManagerImpl implements OpenAiLanguageModelsManager {
2424

2525
protected _apiKey: string | undefined;
26+
protected _apiVersion: string | undefined;
2627

2728
@inject(LanguageModelRegistry)
2829
protected readonly languageModelRegistry: LanguageModelRegistry;
@@ -31,6 +32,10 @@ export class OpenAiLanguageModelsManagerImpl implements OpenAiLanguageModelsMana
3132
return this._apiKey ?? process.env.OPENAI_API_KEY;
3233
}
3334

35+
get apiVersion(): string | undefined {
36+
return this._apiVersion ?? process.env.OPENAI_API_VERSION;
37+
}
38+
3439
// Triggered from frontend. In case you want to use the models on the backend
3540
// without a frontend then call this yourself
3641
async createOrUpdateLanguageModels(...modelDescriptions: OpenAiModelDescription[]): Promise<void> {
@@ -45,6 +50,15 @@ export class OpenAiLanguageModelsManagerImpl implements OpenAiLanguageModelsMana
4550
}
4651
return undefined;
4752
};
53+
const apiVersionProvider = () => {
54+
if (modelDescription.apiVersion === true) {
55+
return this.apiVersion;
56+
}
57+
if (modelDescription.apiVersion) {
58+
return modelDescription.apiVersion;
59+
}
60+
return undefined;
61+
};
4862

4963
if (model) {
5064
if (!(model instanceof OpenAiModel)) {
@@ -55,6 +69,8 @@ export class OpenAiLanguageModelsManagerImpl implements OpenAiLanguageModelsMana
5569
model.enableStreaming = modelDescription.enableStreaming;
5670
model.url = modelDescription.url;
5771
model.apiKey = apiKeyProvider;
72+
model.apiVersion = apiVersionProvider;
73+
model.supportsDeveloperMessage = modelDescription.supportsDeveloperMessage;
5874
model.defaultRequestSettings = modelDescription.defaultRequestSettings;
5975
} else {
6076
this.languageModelRegistry.addLanguageModels([
@@ -63,6 +79,8 @@ export class OpenAiLanguageModelsManagerImpl implements OpenAiLanguageModelsMana
6379
modelDescription.model,
6480
modelDescription.enableStreaming,
6581
apiKeyProvider,
82+
apiVersionProvider,
83+
modelDescription.supportsDeveloperMessage,
6684
modelDescription.url,
6785
modelDescription.defaultRequestSettings
6886
)
@@ -82,4 +100,12 @@ export class OpenAiLanguageModelsManagerImpl implements OpenAiLanguageModelsMana
82100
this._apiKey = undefined;
83101
}
84102
}
103+
104+
setApiVersion(apiVersion: string | undefined): void {
105+
if (apiVersion) {
106+
this._apiVersion = apiVersion;
107+
} else {
108+
this._apiVersion = undefined;
109+
}
110+
}
85111
}

0 commit comments

Comments
 (0)