Skip to content

Commit d5259e1

Browse files
authored
feat(OpenAIClient): AZURE_USE_MODEL_AS_DEPLOYMENT_NAME, AZURE_OPENAI_DEFAULT_MODEL (danny-avila#1165)
* feat(OpenAIClient): AZURE_USE_MODEL_AS_DEPLOYMENT_NAME, AZURE_OPENAI_DEFAULT_MODEL * ci: fix initializeClient test
1 parent 9d100ec commit d5259e1

File tree

10 files changed

+242
-60
lines changed

10 files changed

+242
-60
lines changed

.env.example

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,23 @@ DEBUG_OPENAI=false # Set to true to enable debug mode for the OpenAI endpoint
169169
# AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME=
170170
# AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME=
171171

172-
# Identify the available models, separated by commas *without spaces*.
173-
# The first will be default.
174-
# Leave it blank to use internal settings.
172+
# NOTE: As of 2023-11-10, the Azure API only allows one model per deployment,
173+
# It's recommended to name your deployments after the model name, e.g. "gpt-35-turbo,"
174+
# which allows for fast deployment switching and AZURE_USE_MODEL_AS_DEPLOYMENT_NAME enabled.
175+
# However, you can use non-model deployment names and setting the AZURE_OPENAI_DEFAULT_MODEL to ensure it works as expected.
176+
177+
# Identify the available models, separated by commas *without spaces*.
178+
# The first will be default. Leave it blank or as is to use internal settings.
179+
# NOTE: as deployment names can't have periods, they will be removed when the endpoint is generated.
175180
AZURE_OPENAI_MODELS=gpt-3.5-turbo,gpt-4
176181

182+
# (Advanced) this enables the use of the model name as the deployment name, e.g. "gpt-3.5-turbo" as the deployment name
183+
AZURE_USE_MODEL_AS_DEPLOYMENT_NAME=TRUE
184+
185+
# (Advanced) this overrides the model setting for Azure, in case you want to use your custom deployment names
186+
# as the values for AZURE_OPENAI_MODELS
187+
# AZURE_OPENAI_DEFAULT_MODEL=gpt-3.5-turbo
188+
177189
# To use Azure with the Plugins endpoint, you need the variables above, and uncomment the following variable:
178190
# NOTE: This may not work as expected and Azure OpenAI may not support OpenAI Functions yet
179191
# Omit/leave it commented to use the default OpenAI API

api/app/clients/OpenAIClient.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@ class OpenAIClient extends BaseClient {
3030
: 'discard';
3131
this.shouldSummarize = this.contextStrategy === 'summarize';
3232
this.azure = options.azure || false;
33-
if (this.azure) {
34-
this.azureEndpoint = genAzureChatCompletion(this.azure);
35-
}
3633
this.setOptions(options);
3734
}
3835

@@ -86,6 +83,13 @@ class OpenAIClient extends BaseClient {
8683
isEnabled(OPENAI_FORCE_PROMPT) ||
8784
(reverseProxy && reverseProxy.includes('completions') && !reverseProxy.includes('chat'));
8885

86+
if (this.azure && process.env.AZURE_OPENAI_DEFAULT_MODEL) {
87+
this.azureEndpoint = genAzureChatCompletion(this.azure, this.modelOptions.model);
88+
this.modelOptions.model = process.env.AZURE_OPENAI_DEFAULT_MODEL;
89+
} else if (this.azure) {
90+
this.azureEndpoint = genAzureChatCompletion(this.azure, this.modelOptions.model);
91+
}
92+
8993
const { model } = this.modelOptions;
9094

9195
this.isChatCompletion = this.useOpenRouter || !!reverseProxy || model.includes('gpt-');
@@ -533,6 +537,7 @@ If your reverse proxy is compatible to OpenAI specs in every other way, it may s
533537
this.options.debug && console.error(e.message, e);
534538
modelOptions.model = OPENAI_TITLE_MODEL ?? 'gpt-3.5-turbo';
535539
if (this.azure) {
540+
modelOptions.model = process.env.AZURE_OPENAI_DEFAULT_MODEL ?? modelOptions.model;
536541
this.azureEndpoint = genAzureChatCompletion(this.azure, modelOptions.model);
537542
}
538543
const instructionsPayload = [

api/app/clients/llm/createLLM.js

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,60 @@
11
const { ChatOpenAI } = require('langchain/chat_models/openai');
22
const { sanitizeModelName } = require('../../../utils');
3+
const { isEnabled } = require('../../../server/utils');
34

5+
/**
6+
* @typedef {Object} ModelOptions
7+
* @property {string} modelName - The name of the model.
8+
* @property {number} [temperature] - The temperature setting for the model.
9+
* @property {number} [presence_penalty] - The presence penalty setting.
10+
* @property {number} [frequency_penalty] - The frequency penalty setting.
11+
* @property {number} [max_tokens] - The maximum number of tokens to generate.
12+
*/
13+
14+
/**
15+
* @typedef {Object} ConfigOptions
16+
* @property {string} [basePath] - The base path for the API requests.
17+
* @property {Object} [baseOptions] - Base options for the API requests, including headers.
18+
* @property {Object} [httpAgent] - The HTTP agent for the request.
19+
* @property {Object} [httpsAgent] - The HTTPS agent for the request.
20+
*/
21+
22+
/**
23+
* @typedef {Object} Callbacks
24+
* @property {Function} [handleChatModelStart] - A callback function for handleChatModelStart
25+
* @property {Function} [handleLLMEnd] - A callback function for handleLLMEnd
26+
* @property {Function} [handleLLMError] - A callback function for handleLLMError
27+
*/
28+
29+
/**
30+
* @typedef {Object} AzureOptions
31+
* @property {string} [azureOpenAIApiKey] - The Azure OpenAI API key.
32+
* @property {string} [azureOpenAIApiInstanceName] - The Azure OpenAI API instance name.
33+
* @property {string} [azureOpenAIApiDeploymentName] - The Azure OpenAI API deployment name.
34+
* @property {string} [azureOpenAIApiVersion] - The Azure OpenAI API version.
35+
*/
36+
37+
/**
38+
* Creates a new instance of a language model (LLM) for chat interactions.
39+
*
40+
* @param {Object} options - The options for creating the LLM.
41+
* @param {ModelOptions} options.modelOptions - The options specific to the model, including modelName, temperature, presence_penalty, frequency_penalty, and other model-related settings.
42+
* @param {ConfigOptions} options.configOptions - Configuration options for the API requests, including proxy settings and custom headers.
43+
* @param {Callbacks} options.callbacks - Callback functions for managing the lifecycle of the LLM, including token buffers, context, and initial message count.
44+
* @param {boolean} [options.streaming=false] - Determines if the LLM should operate in streaming mode.
45+
* @param {string} options.openAIApiKey - The API key for OpenAI, used for authentication.
46+
* @param {AzureOptions} [options.azure={}] - Optional Azure-specific configurations. If provided, Azure configurations take precedence over OpenAI configurations.
47+
*
48+
* @returns {ChatOpenAI} An instance of the ChatOpenAI class, configured with the provided options.
49+
*
50+
* @example
51+
* const llm = createLLM({
52+
* modelOptions: { modelName: 'gpt-3.5-turbo', temperature: 0.2 },
53+
* configOptions: { basePath: 'https://example.api/path' },
54+
* callbacks: { onMessage: handleMessage },
55+
* openAIApiKey: 'your-api-key'
56+
* });
57+
*/
458
function createLLM({
559
modelOptions,
660
configOptions,
@@ -16,10 +70,19 @@ function createLLM({
1670

1771
let azureOptions = {};
1872
if (azure) {
73+
const useModelName = isEnabled(process.env.AZURE_USE_MODEL_AS_DEPLOYMENT_NAME);
74+
1975
credentials = {};
2076
configuration = {};
2177
azureOptions = azure;
22-
azureOptions.azureOpenAIApiDeploymentName = sanitizeModelName(modelOptions.modelName);
78+
79+
azureOptions.azureOpenAIApiDeploymentName = useModelName
80+
? sanitizeModelName(modelOptions.modelName)
81+
: azureOptions.azureOpenAIApiDeploymentName;
82+
}
83+
84+
if (azure && process.env.AZURE_OPENAI_DEFAULT_MODEL) {
85+
modelOptions.modelName = process.env.AZURE_OPENAI_DEFAULT_MODEL;
2386
}
2487

2588
// console.debug('createLLM: configOptions');

api/app/clients/specs/OpenAIClient.test.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ describe('OpenAIClient', () => {
1212
{ role: 'assistant', sender: 'Assistant', text: 'Hi', messageId: '2' },
1313
];
1414

15+
beforeAll(() => {
16+
jest.spyOn(console, 'warn').mockImplementation(() => {});
17+
});
18+
19+
afterAll(() => {
20+
console.warn.mockRestore();
21+
});
22+
1523
beforeEach(() => {
1624
const options = {
1725
// debug: true,
@@ -90,6 +98,96 @@ describe('OpenAIClient', () => {
9098
});
9199
});
92100

101+
describe('setOptions with Simplified Azure Integration', () => {
102+
afterEach(() => {
103+
delete process.env.AZURE_OPENAI_DEFAULT_MODEL;
104+
delete process.env.AZURE_USE_MODEL_AS_DEPLOYMENT_NAME;
105+
});
106+
107+
const azureOpenAIApiInstanceName = 'test-instance';
108+
const azureOpenAIApiDeploymentName = 'test-deployment';
109+
const azureOpenAIApiVersion = '2020-07-01-preview';
110+
111+
const createOptions = (model) => ({
112+
modelOptions: { model },
113+
azure: {
114+
azureOpenAIApiInstanceName,
115+
azureOpenAIApiDeploymentName,
116+
azureOpenAIApiVersion,
117+
},
118+
});
119+
120+
it('should set model from AZURE_OPENAI_DEFAULT_MODEL when Azure is enabled', () => {
121+
process.env.AZURE_OPENAI_DEFAULT_MODEL = 'gpt-4-azure';
122+
const options = createOptions('test');
123+
client.azure = options.azure;
124+
client.setOptions(options);
125+
expect(client.modelOptions.model).toBe('gpt-4-azure');
126+
});
127+
128+
it('should not change model if Azure is not enabled', () => {
129+
process.env.AZURE_OPENAI_DEFAULT_MODEL = 'gpt-4-azure';
130+
const originalModel = 'test';
131+
client.azure = false;
132+
client.setOptions(createOptions('test'));
133+
expect(client.modelOptions.model).toBe(originalModel);
134+
});
135+
136+
it('should not change model if AZURE_OPENAI_DEFAULT_MODEL is not set and model is passed', () => {
137+
const originalModel = 'GROK-LLM';
138+
const options = createOptions(originalModel);
139+
client.azure = options.azure;
140+
client.setOptions(options);
141+
expect(client.modelOptions.model).toBe(originalModel);
142+
});
143+
144+
it('should change model if AZURE_OPENAI_DEFAULT_MODEL is set and model is passed', () => {
145+
process.env.AZURE_OPENAI_DEFAULT_MODEL = 'gpt-4-azure';
146+
const originalModel = 'GROK-LLM';
147+
const options = createOptions(originalModel);
148+
client.azure = options.azure;
149+
client.setOptions(options);
150+
expect(client.modelOptions.model).toBe(process.env.AZURE_OPENAI_DEFAULT_MODEL);
151+
});
152+
153+
it('should include model in deployment name if AZURE_USE_MODEL_AS_DEPLOYMENT_NAME is set', () => {
154+
process.env.AZURE_USE_MODEL_AS_DEPLOYMENT_NAME = 'true';
155+
const model = 'gpt-4-azure';
156+
157+
const AzureClient = new OpenAIClient('test-api-key', createOptions(model));
158+
159+
const expectedValue = `https://${azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${model}/chat/completions?api-version=${azureOpenAIApiVersion}`;
160+
161+
expect(AzureClient.modelOptions.model).toBe(model);
162+
expect(AzureClient.azureEndpoint).toBe(expectedValue);
163+
});
164+
165+
it('should include model in deployment name if AZURE_USE_MODEL_AS_DEPLOYMENT_NAME and default model is set', () => {
166+
const defaultModel = 'gpt-4-azure';
167+
process.env.AZURE_USE_MODEL_AS_DEPLOYMENT_NAME = 'true';
168+
process.env.AZURE_OPENAI_DEFAULT_MODEL = defaultModel;
169+
const model = 'gpt-4-this-is-a-test-model-name';
170+
171+
const AzureClient = new OpenAIClient('test-api-key', createOptions(model));
172+
173+
const expectedValue = `https://${azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${model}/chat/completions?api-version=${azureOpenAIApiVersion}`;
174+
175+
expect(AzureClient.modelOptions.model).toBe(defaultModel);
176+
expect(AzureClient.azureEndpoint).toBe(expectedValue);
177+
});
178+
179+
it('should not include model in deployment name if AZURE_USE_MODEL_AS_DEPLOYMENT_NAME is not set', () => {
180+
const model = 'gpt-4-azure';
181+
182+
const AzureClient = new OpenAIClient('test-api-key', createOptions(model));
183+
184+
const expectedValue = `https://${azureOpenAIApiInstanceName}.openai.azure.com/openai/deployments/${azureOpenAIApiDeploymentName}/chat/completions?api-version=${azureOpenAIApiVersion}`;
185+
186+
expect(AzureClient.modelOptions.model).toBe(model);
187+
expect(AzureClient.azureEndpoint).toBe(expectedValue);
188+
});
189+
});
190+
93191
describe('selectTokenizer', () => {
94192
it('should get the correct tokenizer based on the instance state', () => {
95193
const tokenizer = client.selectTokenizer();

api/server/routes/endpoints/gptPlugins/initializeClient.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const { PluginsClient } = require('../../../../app');
22
const { isEnabled } = require('../../../utils');
3-
const { getAzureCredentials, sanitizeModelName } = require('../../../../utils');
3+
const { getAzureCredentials } = require('../../../../utils');
44
const { getUserKey, checkUserKeyExpiry } = require('../../../services/UserService');
55

66
const initializeClient = async ({ req, res, endpointOption }) => {
@@ -47,9 +47,6 @@ const initializeClient = async ({ req, res, endpointOption }) => {
4747

4848
if (useAzure || (apiKey && apiKey.includes('azure') && !clientOptions.azure)) {
4949
clientOptions.azure = isUserProvided ? JSON.parse(userKey) : getAzureCredentials();
50-
clientOptions.azure.azureOpenAIApiDeploymentName = sanitizeModelName(
51-
clientOptions.modelOptions.model,
52-
);
5350
apiKey = clientOptions.azure.azureOpenAIApiKey;
5451
}
5552

api/server/routes/endpoints/gptPlugins/initializeClient.spec.js

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ describe('gptPlugins/initializeClient', () => {
4343

4444
test('should initialize PluginsClient with Azure credentials when PLUGINS_USE_AZURE is true', async () => {
4545
process.env.AZURE_API_KEY = 'test-azure-api-key';
46-
process.env.PLUGINS_USE_AZURE = 'true';
46+
(process.env.AZURE_OPENAI_API_INSTANCE_NAME = 'some-value'),
47+
(process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME = 'some-value'),
48+
(process.env.AZURE_OPENAI_API_VERSION = 'some-value'),
49+
(process.env.AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME = 'some-value'),
50+
(process.env.AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME = 'some-value'),
51+
(process.env.PLUGINS_USE_AZURE = 'true');
4752
process.env.DEBUG_PLUGINS = 'false';
4853
process.env.OPENAI_SUMMARIZE = 'false';
4954

@@ -174,31 +179,6 @@ describe('gptPlugins/initializeClient', () => {
174179
);
175180
});
176181

177-
test('should sanitize model name for Azure when modelOptions is provided', async () => {
178-
process.env.AZURE_API_KEY = 'azure-provided-api-key';
179-
process.env.PLUGINS_USE_AZURE = 'true';
180-
181-
const modelName = 'test-3.5-model';
182-
const sanitizedModelName = 'test-35-model';
183-
const req = {
184-
body: { key: new Date(Date.now() + 10000).toISOString() },
185-
user: { id: '123' },
186-
};
187-
const res = {};
188-
const endpointOption = { modelOptions: { model: modelName } };
189-
190-
getUserKey.mockResolvedValue(
191-
JSON.stringify({
192-
azureOpenAIApiKey: 'test-user-provided-azure-api-key',
193-
azureOpenAIApiDeploymentName: modelName,
194-
}),
195-
);
196-
197-
const { azure } = await initializeClient({ req, res, endpointOption });
198-
199-
expect(azure.azureOpenAIApiDeploymentName).toBe(sanitizedModelName);
200-
});
201-
202182
test('should throw an error if the user-provided Azure key is invalid JSON', async () => {
203183
process.env.AZURE_API_KEY = 'user_provided';
204184
process.env.PLUGINS_USE_AZURE = 'true';

api/server/routes/endpoints/openAI/initializeClient.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const { OpenAIClient } = require('../../../../app');
22
const { isEnabled } = require('../../../utils');
3-
const { getAzureCredentials, sanitizeModelName } = require('../../../../utils');
3+
const { getAzureCredentials } = require('../../../../utils');
44
const { getUserKey, checkUserKeyExpiry } = require('../../../services/UserService');
55

66
const initializeClient = async ({ req, res, endpointOption }) => {
@@ -44,9 +44,6 @@ const initializeClient = async ({ req, res, endpointOption }) => {
4444

4545
if (endpoint === 'azureOpenAI') {
4646
clientOptions.azure = isUserProvided ? JSON.parse(userKey) : getAzureCredentials();
47-
clientOptions.azure.azureOpenAIApiDeploymentName = sanitizeModelName(
48-
clientOptions.modelOptions.model,
49-
);
5047
apiKey = clientOptions.azure.azureOpenAIApiKey;
5148
}
5249

api/server/routes/endpoints/openAI/initializeClient.spec.js

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ describe('initializeClient', () => {
4141

4242
test('should initialize client with Azure credentials when endpoint is azureOpenAI', async () => {
4343
process.env.AZURE_API_KEY = 'test-azure-api-key';
44-
process.env.OPENAI_API_KEY = 'test-openai-api-key';
44+
(process.env.AZURE_OPENAI_API_INSTANCE_NAME = 'some-value'),
45+
(process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME = 'some-value'),
46+
(process.env.AZURE_OPENAI_API_VERSION = 'some-value'),
47+
(process.env.AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME = 'some-value'),
48+
(process.env.AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME = 'some-value'),
49+
(process.env.OPENAI_API_KEY = 'test-openai-api-key');
4550
process.env.DEBUG_OPENAI = 'false';
4651
process.env.OPENAI_SUMMARIZE = 'false';
4752

@@ -191,21 +196,4 @@ describe('initializeClient', () => {
191196
/Your OpenAI API key has expired/,
192197
);
193198
});
194-
195-
test('should sanitize model name for Azure when modelOptions is provided', async () => {
196-
const modelName = 'test-3.5-model';
197-
const sanitizedModelName = 'test-35-model';
198-
const req = {
199-
body: { key: new Date(Date.now() + 10000).toISOString(), endpoint: 'azureOpenAI' },
200-
user: { id: '123' },
201-
};
202-
const res = {};
203-
const endpointOption = { modelOptions: { model: modelName } };
204-
process.env.AZURE_API_KEY = 'azure-provided-api-key';
205-
getUserKey.mockResolvedValue('test-user-provided-openai-api-key');
206-
207-
const result = await initializeClient({ req, res, endpointOption });
208-
209-
expect(result.client.options.azure.azureOpenAIApiDeploymentName).toBe(sanitizedModelName);
210-
});
211199
});

api/utils/azureUtils.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* @property {string} azureOpenAIApiVersion - The Azure OpenAI API version.
77
*/
88

9+
const { isEnabled } = require('../server/utils');
10+
911
/**
1012
* Sanitizes the model name to be used in the URL by removing or replacing disallowed characters.
1113
* @param {string} modelName - The model name to be sanitized.
@@ -44,7 +46,7 @@ const genAzureChatCompletion = (
4446
) => {
4547
// Determine the deployment segment of the URL based on provided modelName or azureOpenAIApiDeploymentName
4648
let deploymentSegment;
47-
if (modelName) {
49+
if (isEnabled(process.env.AZURE_USE_MODEL_AS_DEPLOYMENT_NAME) && modelName) {
4850
const sanitizedModelName = sanitizeModelName(modelName);
4951
deploymentSegment = `${sanitizedModelName}`;
5052
} else if (azureOpenAIApiDeploymentName) {

0 commit comments

Comments
 (0)