Skip to content

Commit 3514c39

Browse files
authored
fix(OpenAIClient/PluginsClient): allow non-v1 reverse proxy, handle "v1/completions" reverse proxy (danny-avila#1029)
* fix(OpenAIClient): handle completions request in reverse proxy, also force prompt by env var * fix(reverseProxyUrl): allow url without /v1/ but add server warning as it will not be compatible with plugins * fix(ModelService): handle reverse proxy without v1 * refactor: make changes cleaner * ci(OpenAIClient): add tests for OPENROUTER_API_KEY, FORCE_PROMPT, and reverseProxyUrl handling in setOptions
1 parent 407b37f commit 3514c39

File tree

5 files changed

+80
-14
lines changed

5 files changed

+80
-14
lines changed

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ DEBUG_OPENAI=false # Set to true to enable debug mode for the OpenAI endpoint
117117
# https://github.com/waylaidwanderer/node-chatgpt-api#using-a-reverse-proxy
118118
# OPENAI_REVERSE_PROXY=
119119

120+
# (Advanced) Sometimes when using Local LLM APIs, you may need to force the API
121+
# to be called with a `prompt` payload instead of a `messages` payload; to mimic the
122+
# a `/v1/completions` request instead of `/v1/chat/completions`
123+
# This may be the case for LocalAI with some models. To do so, uncomment the following:
124+
# OPENAI_FORCE_PROMPT=true
125+
120126
##########################
121127
# OpenRouter (overrides OpenAI and Plugins Endpoints):
122128
##########################

api/app/clients/OpenAIClient.js

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const BaseClient = require('./BaseClient');
44
const { getModelMaxTokens, genAzureChatCompletion } = require('../../utils');
55
const { truncateText, formatMessage, CUT_OFF_PROMPT } = require('./prompts');
66
const spendTokens = require('../../models/spendTokens');
7+
const { isEnabled } = require('../../server/utils');
78
const { createLLM, RunManager } = require('./llm');
89
const { summaryBuffer } = require('./memory');
910
const { runTitleChain } = require('./chains');
@@ -71,20 +72,22 @@ class OpenAIClient extends BaseClient {
7172
};
7273
}
7374

74-
if (process.env.OPENROUTER_API_KEY) {
75-
this.apiKey = process.env.OPENROUTER_API_KEY;
75+
const { OPENROUTER_API_KEY, OPENAI_FORCE_PROMPT } = process.env ?? {};
76+
if (OPENROUTER_API_KEY) {
77+
this.apiKey = OPENROUTER_API_KEY;
7678
this.useOpenRouter = true;
7779
}
7880

81+
const { reverseProxyUrl: reverseProxy } = this.options;
82+
this.FORCE_PROMPT =
83+
isEnabled(OPENAI_FORCE_PROMPT) ||
84+
(reverseProxy && reverseProxy.includes('completions') && !reverseProxy.includes('chat'));
85+
7986
const { model } = this.modelOptions;
8087

81-
this.isChatCompletion =
82-
this.useOpenRouter ||
83-
this.options.reverseProxyUrl ||
84-
this.options.localAI ||
85-
model.includes('gpt-');
88+
this.isChatCompletion = this.useOpenRouter || !!reverseProxy || model.includes('gpt-');
8689
this.isChatGptModel = this.isChatCompletion;
87-
if (model.includes('text-davinci-003') || model.includes('instruct')) {
90+
if (model.includes('text-davinci-003') || model.includes('instruct') || this.FORCE_PROMPT) {
8891
this.isChatCompletion = false;
8992
this.isChatGptModel = false;
9093
}
@@ -128,9 +131,13 @@ class OpenAIClient extends BaseClient {
128131
this.modelOptions.stop = stopTokens;
129132
}
130133

131-
if (this.options.reverseProxyUrl) {
132-
this.completionsUrl = this.options.reverseProxyUrl;
133-
this.langchainProxy = this.options.reverseProxyUrl.match(/.*v1/)[0];
134+
if (reverseProxy) {
135+
this.completionsUrl = reverseProxy;
136+
this.langchainProxy = reverseProxy.match(/.*v1/)?.[0];
137+
!this.langchainProxy &&
138+
console.warn(`The reverse proxy URL ${reverseProxy} is not valid for Plugins.
139+
The url must follow OpenAI specs, for example: https://localhost:8080/v1/chat/completions
140+
If your reverse proxy is compatible to OpenAI specs in every other way, it may still work without plugins enabled.`);
134141
} else if (isChatGptModel) {
135142
this.completionsUrl = 'https://api.openai.com/v1/chat/completions';
136143
} else {
@@ -185,7 +192,7 @@ class OpenAIClient extends BaseClient {
185192
this.encoding = model.includes('instruct') ? 'text-davinci-003' : model;
186193
tokenizer = this.constructor.getTokenizer(this.encoding, true);
187194
} catch {
188-
tokenizer = this.constructor.getTokenizer(this.encoding, true);
195+
tokenizer = this.constructor.getTokenizer('text-davinci-003', true);
189196
}
190197
}
191198

api/app/clients/PluginsClient.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ class PluginsClient extends OpenAIClient {
3434
this.isGpt3 = this.modelOptions?.model?.includes('gpt-3');
3535

3636
if (this.options.reverseProxyUrl) {
37-
this.langchainProxy = this.options.reverseProxyUrl.match(/.*v1/)[0];
37+
this.langchainProxy = this.options.reverseProxyUrl.match(/.*v1/)?.[0];
38+
!this.langchainProxy &&
39+
console.warn(`The reverse proxy URL ${this.options.reverseProxyUrl} is not valid for Plugins.
40+
The url must follow OpenAI specs, for example: https://localhost:8080/v1/chat/completions
41+
If your reverse proxy is compatible to OpenAI specs in every other way, it may still work without plugins enabled.`);
3842
}
3943
}
4044

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
require('dotenv').config();
12
const OpenAIClient = require('../OpenAIClient');
23

34
jest.mock('meilisearch');
@@ -39,6 +40,54 @@ describe('OpenAIClient', () => {
3940
expect(client.modelOptions.model).toBe(model);
4041
expect(client.modelOptions.temperature).toBe(0.7);
4142
});
43+
44+
it('should set apiKey and useOpenRouter if OPENROUTER_API_KEY is present', () => {
45+
process.env.OPENROUTER_API_KEY = 'openrouter-key';
46+
client.setOptions({});
47+
expect(client.apiKey).toBe('openrouter-key');
48+
expect(client.useOpenRouter).toBe(true);
49+
delete process.env.OPENROUTER_API_KEY; // Cleanup
50+
});
51+
52+
it('should set FORCE_PROMPT based on OPENAI_FORCE_PROMPT or reverseProxyUrl', () => {
53+
process.env.OPENAI_FORCE_PROMPT = 'true';
54+
client.setOptions({});
55+
expect(client.FORCE_PROMPT).toBe(true);
56+
delete process.env.OPENAI_FORCE_PROMPT; // Cleanup
57+
client.FORCE_PROMPT = undefined;
58+
59+
client.setOptions({ reverseProxyUrl: 'https://example.com/completions' });
60+
expect(client.FORCE_PROMPT).toBe(true);
61+
client.FORCE_PROMPT = undefined;
62+
63+
client.setOptions({ reverseProxyUrl: 'https://example.com/chat' });
64+
expect(client.FORCE_PROMPT).toBe(false);
65+
});
66+
67+
it('should set isChatCompletion based on useOpenRouter, reverseProxyUrl, or model', () => {
68+
client.setOptions({ reverseProxyUrl: null });
69+
// true by default since default model will be gpt-3.5-turbo
70+
expect(client.isChatCompletion).toBe(true);
71+
client.isChatCompletion = undefined;
72+
73+
// false because completions url will force prompt payload
74+
client.setOptions({ reverseProxyUrl: 'https://example.com/completions' });
75+
expect(client.isChatCompletion).toBe(false);
76+
client.isChatCompletion = undefined;
77+
78+
client.setOptions({ modelOptions: { model: 'gpt-3.5-turbo' }, reverseProxyUrl: null });
79+
expect(client.isChatCompletion).toBe(true);
80+
});
81+
82+
it('should set completionsUrl and langchainProxy based on reverseProxyUrl', () => {
83+
client.setOptions({ reverseProxyUrl: 'https://localhost:8080/v1/chat/completions' });
84+
expect(client.completionsUrl).toBe('https://localhost:8080/v1/chat/completions');
85+
expect(client.langchainProxy).toBe('https://localhost:8080/v1');
86+
87+
client.setOptions({ reverseProxyUrl: 'https://example.com/completions' });
88+
expect(client.completionsUrl).toBe('https://example.com/completions');
89+
expect(client.langchainProxy).toBeUndefined();
90+
});
4291
});
4392

4493
describe('selectTokenizer', () => {

api/server/services/ModelService.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const fetchOpenAIModels = async (opts = { azure: false, plugins: false }, _model
2828
}
2929

3030
if (reverseProxyUrl) {
31-
basePath = reverseProxyUrl.match(/.*v1/)[0];
31+
basePath = reverseProxyUrl.match(/.*v1/)?.[0];
3232
}
3333

3434
const cachedModels = await modelsCache.get(basePath);

0 commit comments

Comments
 (0)