[OpenAI connector] Fix OpenAI node package url (#180955)

## Summary

Fixes a bug with OpenAI (not azure) connectors when used with
`invokeAsyncIterator`:

```
ActionsClientChatOpenAI: an error occurred while running the action - Unexpected API Error: - 404 Invalid URL (POST /v1/chat/completions/chat/completions)
```

Our current default url for OpenAI connectors is
`https://api.openai.com/v1/chat/completions`

When using the `invokeAsyncIterator` subaction, we use the OpenAI node
package. This takes the url as an argument to instatiate. It wants the
URL of OpenAI, not the completions endpoint which is our default.

Looking back, I wish we had made the default url
`https://api.openai.com/v1`. However, because we want to support
existing connectors I think we should keep this the default and remove
the endpoint from the url when it is passed to the OpenAI node package.

### To test

Send a message in Security Assistant with an OpenAI (not azure)
connector with streaming enabled. Observe a successful response.

### Note to docs team

@elastic/security-docs We should call this out. If a user has an open
source OpenAI connector with a chat completions endpoint that does not
have the same structure as OpenAI's ending in `/chat/completions`, their
full endpoint url will be used with LangChain OpenAI Streaming
implementations. For example, if a user had a url like
`https://mycustomllm.com/execute/completions` and tries to use the the
Security Assistant with Knowledge Base on and Streaming on, they may get
this error:

```
ActionsClientChatOpenAI: an error occurred while running the action - Unexpected API Error: - 404 Invalid URL (POST /execute/completions/chat/completions)
```

We should instruct them to follow the same route structure as OpenAI.
Their endpoint needs to end in `/chat/completions`
This commit is contained in:
Steph Milovic 2024-04-16 12:28:30 -06:00 committed by GitHub
parent 448d42bc34
commit f002b8f1ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 28 additions and 2 deletions

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { sanitizeRequest, getRequestWithStreamOption } from './openai_utils';
import { sanitizeRequest, getRequestWithStreamOption, removeEndpointFromUrl } from './openai_utils';
import {
DEFAULT_OPENAI_MODEL,
OPENAI_CHAT_URL,
@ -182,4 +182,22 @@ describe('Open AI Utils', () => {
expect(sanitizedBodyString).toEqual(bodyString);
});
});
describe('removeEndpointFromUrl', () => {
test('removes "/chat/completions" from the end of the URL', () => {
const originalUrl = 'https://api.openai.com/v1/chat/completions';
const expectedUrl = 'https://api.openai.com/v1';
expect(removeEndpointFromUrl(originalUrl)).toBe(expectedUrl);
});
test('does not modify the URL if it does not end with "/chat/completions"', () => {
const originalUrl = 'https://api.openai.com/v1/some/other/endpoint';
expect(removeEndpointFromUrl(originalUrl)).toBe(originalUrl);
});
test('handles URLs with a trailing slash correctly', () => {
const originalUrl = 'https://api.openai.com/v1/chat/completions/';
const expectedUrl = 'https://api.openai.com/v1';
expect(removeEndpointFromUrl(originalUrl)).toBe(expectedUrl);
});
});
});

View file

@ -49,3 +49,10 @@ export const getRequestWithStreamOption = (
return body;
};
// removes the chat completions endpoint from the OpenAI url in order
// to provide the correct endpoint for the OpenAI node package
export const removeEndpointFromUrl = (url: string): string => {
const endpointToRemove = /\/chat\/completions\/?$/;
return url.replace(endpointToRemove, '');
};

View file

@ -16,6 +16,7 @@ import {
ChatCompletionMessageParam,
} from 'openai/resources/chat/completions';
import { Stream } from 'openai/streaming';
import { removeEndpointFromUrl } from './lib/openai_utils';
import {
RunActionParamsSchema,
RunActionResponseSchema,
@ -76,7 +77,7 @@ export class OpenAIConnector extends SubActionConnector<Config, Secrets> {
},
})
: new OpenAI({
baseURL: this.config.apiUrl,
baseURL: removeEndpointFromUrl(this.config.apiUrl),
apiKey: this.secrets.apiKey,
defaultHeaders: {
...this.config.headers,