From 5372e0978e706a02113de2bd808ade80057332b9 Mon Sep 17 00:00:00 2001 From: Lily Du Date: Tue, 9 Jan 2024 11:20:00 -0800 Subject: [PATCH] [JS] feat: Removed Deprecated Completions Logic, Added OpenAIClient and AzureOpenAIClient Unit Tests, Updated Response/Request Parameters (#1138) ## Linked issues closes: #1136, #54 ## Details - Updated `createEmbeddings` with missing `model` parameter - Added unit tests for `OpenAIClient` and `AzureOpenAIClient` - Fixed model bug (Previously was appending an empty string to the URL if it wasn't specified) - Added missing header `Ocp-Apim-Subscription-Key` to enable calls to Azure Content Safety - Removed deprecated `Completions` logic - [More info here](https://platform.openai.com/docs/api-reference/completions) - Updated request/response parameters (optional, new fields, and deprecated fields) - Created a [ticket ](https://github.com/microsoft/teams-ai/issues/1137) for updating moderation parameters and do a general cleanup of the `types` file. I didn't make these changes in this PR since it'll require logic changes as well (incl in C#), and there's quite a number of fixes here already. ## Attestation Checklist - [x] My code follows the style guidelines of this project - I have checked for/fixed spelling, linting, and other errors - I have commented my code for clarity - I have made corresponding changes to the documentation (we use [TypeDoc](https://typedoc.org/) to document our code) - My changes generate no new warnings - I have added tests that validates my changes, and provides sufficient test coverage. I have tested with: - Local testing - E2E testing in Teams - New and existing unit tests pass locally with my changes --- .../src/embeddings/EmbeddingsModel.ts | 3 +- .../src/embeddings/OpenAIEmbeddings.ts | 3 +- .../teams-ai/src/embeddings/TestEmbeddings.ts | 3 +- .../src/internals/AzureOpenAIClient.spec.ts | 207 ++++++++++++++ .../src/internals/AzureOpenAIClient.ts | 28 +- .../src/internals/OpenAIClient.spec.ts | 213 ++++++++++++++ .../teams-ai/src/internals/OpenAIClient.ts | 9 +- js/packages/teams-ai/src/internals/types.ts | 104 +++---- .../teams-ai/src/models/OpenAIModel.ts | 268 +++++------------- 9 files changed, 547 insertions(+), 291 deletions(-) create mode 100644 js/packages/teams-ai/src/internals/AzureOpenAIClient.spec.ts create mode 100644 js/packages/teams-ai/src/internals/OpenAIClient.spec.ts diff --git a/js/packages/teams-ai/src/embeddings/EmbeddingsModel.ts b/js/packages/teams-ai/src/embeddings/EmbeddingsModel.ts index f8c5dba63..59d16cb97 100644 --- a/js/packages/teams-ai/src/embeddings/EmbeddingsModel.ts +++ b/js/packages/teams-ai/src/embeddings/EmbeddingsModel.ts @@ -12,10 +12,11 @@ export interface EmbeddingsModel { /** * Creates embeddings for the given inputs. + * @param model Name of the model to use (or deployment for Azure). * @param inputs Text inputs to create embeddings for. * @returns A `EmbeddingsResponse` with a status and the generated embeddings or a message when an error occurs. */ - createEmbeddings(inputs: string | string[]): Promise; + createEmbeddings(model: string, inputs: string | string[]): Promise; } /** diff --git a/js/packages/teams-ai/src/embeddings/OpenAIEmbeddings.ts b/js/packages/teams-ai/src/embeddings/OpenAIEmbeddings.ts index a13b458bf..0f49611ff 100644 --- a/js/packages/teams-ai/src/embeddings/OpenAIEmbeddings.ts +++ b/js/packages/teams-ai/src/embeddings/OpenAIEmbeddings.ts @@ -156,7 +156,7 @@ export class OpenAIEmbeddings implements EmbeddingsModel { * @param inputs Text inputs to create embeddings for. * @returns A `EmbeddingsResponse` with a status and the generated embeddings or a message when an error occurs. */ - public async createEmbeddings(inputs: string | string[]): Promise { + public async createEmbeddings(model: string, inputs: string | string[]): Promise { if (this.options.logRequests) { console.log(Colorize.title('EMBEDDINGS REQUEST:')); console.log(Colorize.output(inputs)); @@ -164,6 +164,7 @@ export class OpenAIEmbeddings implements EmbeddingsModel { const startTime = Date.now(); const response = await this.createEmbeddingRequest({ + model: model, input: inputs }); diff --git a/js/packages/teams-ai/src/embeddings/TestEmbeddings.ts b/js/packages/teams-ai/src/embeddings/TestEmbeddings.ts index 2237ad3a4..743528f95 100644 --- a/js/packages/teams-ai/src/embeddings/TestEmbeddings.ts +++ b/js/packages/teams-ai/src/embeddings/TestEmbeddings.ts @@ -49,10 +49,11 @@ export class TestEmbeddings implements EmbeddingsModel { /** * Returns a generated set of test embeddings + * @param model Name of the model to use (or deployment for Azure). * @param inputs Input to generate embeddings for. * @returns The generated embeddings. */ - public createEmbeddings(inputs: string | string[]): Promise { + public createEmbeddings(model: string, inputs: string | string[]): Promise { // Validate inputs if (typeof inputs == 'string') { if (inputs.trim().length == 0) { diff --git a/js/packages/teams-ai/src/internals/AzureOpenAIClient.spec.ts b/js/packages/teams-ai/src/internals/AzureOpenAIClient.spec.ts new file mode 100644 index 000000000..1cc563a3b --- /dev/null +++ b/js/packages/teams-ai/src/internals/AzureOpenAIClient.spec.ts @@ -0,0 +1,207 @@ +import assert from 'assert'; +import { AzureOpenAIClient, AzureOpenAIClientOptions } from './AzureOpenAIClient'; +import { CreateChatCompletionRequest, CreateEmbeddingRequest, ModerationInput } from './types'; +import sinon, { SinonStub } from 'sinon'; +import axios from 'axios'; + +describe('AzureOpenAIClient', () => { + const mockAxios = axios; + let client: AzureOpenAIClient; + let clientWithApiVersion: AzureOpenAIClient; + let cognitiveServiceClient: AzureOpenAIClient; + let createStub: SinonStub; + + const options: AzureOpenAIClientOptions = { + apiKey: 'mock-key', + endpoint: 'https://mock.openai.azure.com/' + }; + const optionsWithApiVersion: AzureOpenAIClientOptions = { + apiKey: 'mock-key', + endpoint: 'https://mock.openai.azure.com/', + apiVersion: '2023-03-15-preview' + }; + const optionsMissingEndpoint: AzureOpenAIClientOptions = { + apiKey: 'mock-key', + endpoint: '' + }; + const cognitiveServiceOptions: AzureOpenAIClientOptions = { + apiKey: 'mock-key', + endpoint: 'https://mock-content-safety.cognitiveservices.azure.com/', + apiVersion: '2023-10-01', + ocpApimSubscriptionKey: 'mock-key-2' + }; + const header = { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'Microsoft Teams Conversational AI SDK', + 'api-key': `${options.apiKey}` + } + }; + const cognitiveServiceHeader = { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'Microsoft Teams Conversational AI SDK', + 'api-key': `${cognitiveServiceOptions.apiKey}`, + 'Ocp-Apim-Subscription-Key': `${cognitiveServiceOptions.ocpApimSubscriptionKey}` + } + }; + const chatCompletionRequest: CreateChatCompletionRequest = { + model: 'gpt-35-turbo', + messages: [ + { + role: 'system', + content: 'You are a helpful assistant.' + }, + { + role: 'user', + content: 'Does Azure OpenAI support customer managed keys?' + }, + { + role: 'assistant', + content: 'Yes, customer managed keys are supported by Azure OpenAI.' + }, + { + role: 'user', + content: 'Do other Azure AI services support this too?' + } + ] + }; + const chatCompletionResponse = { + status: '200', + statusText: 'OK', + data: { object: 'chat.completion' } + }; + const embeddingRequest: CreateEmbeddingRequest = { + model: 'text-embedding-ada-002', + input: 'The food was delicious and the waiter...' + }; + const embeddingResponse = { + status: '200', + statusText: 'OK', + data: { object: 'list' } + }; + const moderationRequest: ModerationInput = { + text: 'I want to eat' + }; + const moderationResponse = { + status: '200', + statusText: 'OK', + data: { + blocklistsMatch: [], + categoriesAnalysis: [ + { + category: 'Hate', + severity: 0 + } + ] + } + }; + + beforeEach(() => { + createStub = sinon.stub(axios, 'create').returns(mockAxios); + client = new AzureOpenAIClient(options); + clientWithApiVersion = new AzureOpenAIClient(optionsWithApiVersion); + cognitiveServiceClient = new AzureOpenAIClient(cognitiveServiceOptions); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('constructor', () => { + it('should create a valid OpenAIClient with all required fields', () => { + const azureOpenAIClient = new AzureOpenAIClient(options); + + assert.equal(createStub.called, true); + assert.notEqual(azureOpenAIClient, undefined); + assert.equal(azureOpenAIClient.options.apiKey, options.apiKey); + }); + + it('should throw error due to invalid endpoint', () => { + assert.throws( + () => new AzureOpenAIClient(optionsMissingEndpoint), + new Error(`AzureOpenAIClient initialized without an 'endpoint'.`) + ); + }); + + it('should create a valid OpenAIClient with added apiVersion field', () => { + const azureOpenAIClient = new AzureOpenAIClient(optionsWithApiVersion); + + assert.equal(createStub.called, true); + assert.notEqual(azureOpenAIClient, undefined); + assert.equal(azureOpenAIClient.options.apiKey, optionsWithApiVersion.apiKey); + assert.equal(azureOpenAIClient.options.endpoint, optionsWithApiVersion.endpoint); + assert.equal(azureOpenAIClient.options.apiVersion, optionsWithApiVersion.apiVersion); + }); + }); + + describe('createChatCompletion', () => { + it('creates valid chat completion response', async () => { + const postStub = sinon.stub(mockAxios, 'post').returns(Promise.resolve(chatCompletionResponse)); + const url = `${options.endpoint}/openai/deployments/${chatCompletionRequest.model}/chat/completions?api-version=2023-03-15-preview`; + const response = await client.createChatCompletion(chatCompletionRequest); + + assert.equal(postStub.calledOnce, true); + assert.equal(postStub.calledOnceWith(url, chatCompletionRequest, header), true); + assert.equal(response.status, 200); + assert.equal(response.statusText, 'OK'); + assert.notEqual(response.data, undefined); + assert.equal(response.data?.object, 'chat.completion'); + }); + + it('creates valid chat completion response, with api version specified', async () => { + const postStub = sinon.stub(mockAxios, 'post').returns(Promise.resolve(chatCompletionResponse)); + const url = `${optionsWithApiVersion.endpoint}/openai/deployments/${chatCompletionRequest.model}/chat/completions?api-version=${optionsWithApiVersion.apiVersion}`; + const response = await clientWithApiVersion.createChatCompletion(chatCompletionRequest); + + assert.equal(postStub.calledOnce, true); + assert.equal(postStub.calledOnceWith(url, chatCompletionRequest, header), true); + assert.equal(response.status, 200); + assert.equal(response.statusText, 'OK'); + assert.notEqual(response.data, undefined); + assert.equal(response.data?.object, 'chat.completion'); + }); + }); + + describe('createEmbedding', () => { + it('creates valid embedding response', async () => { + const postStub = sinon.stub(mockAxios, 'post').returns(Promise.resolve(embeddingResponse)); + const url = `${options.endpoint}/openai/deployments/${embeddingRequest.model}/embeddings?api-version=2022-12-01`; + const response = await client.createEmbedding(embeddingRequest); + + assert.equal(postStub.calledOnce, true); + assert.equal(postStub.calledOnceWith(url, embeddingRequest, header), true); + assert.equal(response.status, 200); + assert.equal(response.statusText, 'OK'); + assert.notEqual(response.data, undefined); + assert.equal(response.data?.object, 'list'); + }); + + it('creates valid embedding response with api version specified', async () => { + const postStub = sinon.stub(mockAxios, 'post').returns(Promise.resolve(embeddingResponse)); + const url = `${optionsWithApiVersion.endpoint}/openai/deployments/${embeddingRequest.model}/embeddings?api-version=${optionsWithApiVersion.apiVersion}`; + const response = await clientWithApiVersion.createEmbedding(embeddingRequest); + + assert.equal(postStub.calledOnce, true); + assert.equal(postStub.calledOnceWith(url, embeddingRequest, header), true); + assert.equal(response.status, 200); + assert.equal(response.statusText, 'OK'); + assert.notEqual(response.data, undefined); + assert.equal(response.data?.object, 'list'); + }); + }); + + describe('createModeration', () => { + it('creates valid moderation response', async () => { + const postStub = sinon.stub(mockAxios, 'post').returns(Promise.resolve(moderationResponse)); + const url = `${cognitiveServiceOptions.endpoint}/contentsafety/text:analyze?api-version=${cognitiveServiceOptions.apiVersion}`; + const response = await cognitiveServiceClient.createModeration(moderationRequest); + + assert.equal(postStub.calledOnce, true); + assert.equal(postStub.calledOnceWith(url, moderationRequest, cognitiveServiceHeader), true); + assert.equal(response.status, 200); + assert.equal(response.statusText, 'OK'); + assert.notEqual(response.data, undefined); + }); + }); +}); diff --git a/js/packages/teams-ai/src/internals/AzureOpenAIClient.ts b/js/packages/teams-ai/src/internals/AzureOpenAIClient.ts index 9f9ca7f2c..899a57485 100644 --- a/js/packages/teams-ai/src/internals/AzureOpenAIClient.ts +++ b/js/packages/teams-ai/src/internals/AzureOpenAIClient.ts @@ -9,14 +9,11 @@ import { CreateChatCompletionRequest, CreateChatCompletionResponse, - CreateCompletionRequest, - CreateCompletionResponse, CreateEmbeddingRequest, CreateEmbeddingResponse, ModerationInput, ModerationResponse, OpenAICreateChatCompletionRequest, - OpenAICreateCompletionRequest, OpenAICreateEmbeddingRequest } from './types'; import { OpenAIClient, OpenAIClientOptions, OpenAIClientResponse } from './OpenAIClient'; @@ -52,20 +49,11 @@ export class AzureOpenAIClient extends OpenAIClient { } } - public createCompletion(request: CreateCompletionRequest): Promise> { - const clone = Object.assign({}, request) as OpenAICreateCompletionRequest; - const deployment = this.removeModel(clone); - const endpoint = (this.options as AzureOpenAIClientOptions).endpoint; - const apiVersion = (this.options as AzureOpenAIClientOptions).apiVersion ?? '2022-12-01'; - const url = `${endpoint}/openai/deployments/${deployment}/completions?api-version=${apiVersion}`; - return this.post(url, clone); - } - public createChatCompletion( request: CreateChatCompletionRequest ): Promise> { const clone = Object.assign({}, request) as OpenAICreateChatCompletionRequest; - const deployment = this.removeModel(clone); + const deployment = request.model; const endpoint = (this.options as AzureOpenAIClientOptions).endpoint; const apiVersion = (this.options as AzureOpenAIClientOptions).apiVersion ?? '2023-03-15-preview'; const url = `${endpoint}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}`; @@ -74,7 +62,7 @@ export class AzureOpenAIClient extends OpenAIClient { public createEmbedding(request: CreateEmbeddingRequest): Promise> { const clone = Object.assign({}, request) as OpenAICreateEmbeddingRequest; - const deployment = this.removeModel(clone); + const deployment = request.model; const endpoint = (this.options as AzureOpenAIClientOptions).endpoint; const apiVersion = (this.options as AzureOpenAIClientOptions).apiVersion ?? '2022-12-01'; const url = `${endpoint}/openai/deployments/${deployment}/embeddings?api-version=${apiVersion}`; @@ -91,16 +79,8 @@ export class AzureOpenAIClient extends OpenAIClient { protected addRequestHeaders(headers: Record, options: OpenAIClientOptions): void { headers[options.headerKey ?? 'api-key'] = options.apiKey; - } - - private removeModel(request: { model?: string }): string { - const model = request.model; - delete request.model; - - if (model) { - return model; - } else { - return ''; + if (options.ocpApimSubscriptionKey) { + headers['Ocp-Apim-Subscription-Key'] = options.ocpApimSubscriptionKey; } } } diff --git a/js/packages/teams-ai/src/internals/OpenAIClient.spec.ts b/js/packages/teams-ai/src/internals/OpenAIClient.spec.ts new file mode 100644 index 000000000..6e941e339 --- /dev/null +++ b/js/packages/teams-ai/src/internals/OpenAIClient.spec.ts @@ -0,0 +1,213 @@ +import assert from 'assert'; +import { OpenAIClient, OpenAIClientOptions } from './OpenAIClient'; +import { CreateChatCompletionRequest, CreateEmbeddingRequest, CreateModerationRequest } from './types'; +import sinon, { SinonStub } from 'sinon'; +import axios from 'axios'; + +describe('OpenAIClient', () => { + const mockAxios = axios; + let client: OpenAIClient; + let clientWithAllFields: OpenAIClient; + let createStub: SinonStub; + + const options: OpenAIClientOptions = { + apiKey: 'mock-key' + }; + const optionsWithInvalidEndpoint: OpenAIClientOptions = { + apiKey: 'mock-key', + endpoint: 'www.' + }; + const optionsWithEmptyAPIKey: OpenAIClientOptions = { + apiKey: '' + }; + const optionsWithAllFields: OpenAIClientOptions = { + apiKey: 'mock-key', + organization: 'org', + endpoint: 'https://api.openai.com', + headerKey: '456' + }; + const header = { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'Microsoft Teams Conversational AI SDK', + Authorization: `Bearer ${options.apiKey}` + } + }; + const headerWithAllFields = { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'Microsoft Teams Conversational AI SDK', + Authorization: `Bearer ${optionsWithAllFields.apiKey}`, + 'OpenAI-Organization': `${optionsWithAllFields.organization}` + } + }; + const chatCompletionRequest: CreateChatCompletionRequest = { + model: 'gpt-3.5-turbo', + messages: [ + { + role: 'system', + content: 'You are a helpful assistant.' + }, + { + role: 'user', + content: 'Hello!' + } + ] + }; + const chatCompletionResponse = { + status: '200', + statusText: 'OK', + data: { object: 'chat.completion' } + }; + const embeddingRequest: CreateEmbeddingRequest = { + model: 'text-embedding-ada-002', + input: 'The food was delicious and the waiter...' + }; + const embeddingResponse = { + status: '200', + statusText: 'OK', + data: { object: 'list' } + }; + const moderationRequest: CreateModerationRequest = { + input: 'I want to eat' + }; + const moderationResponse = { + status: '200', + statusText: 'OK', + data: { + blocklistsMatch: [], + categoriesAnalysis: [ + { + category: 'Hate', + severity: 0 + } + ] + } + }; + + beforeEach(() => { + createStub = sinon.stub(axios, 'create').returns(mockAxios); + client = new OpenAIClient(options); + clientWithAllFields = new OpenAIClient(optionsWithAllFields); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('constructor', () => { + it('should create a valid OpenAIClient with required fields', () => { + const openAIClient = new OpenAIClient(options); + + assert.equal(createStub.called, true); + assert.notEqual(openAIClient, undefined); + assert.equal(openAIClient.options.apiKey, options.apiKey); + }); + + it('should throw error due to invalid endpoint', () => { + assert.throws( + () => new OpenAIClient(optionsWithInvalidEndpoint), + new Error( + `OpenAIClient initialized with an invalid endpoint of '${optionsWithInvalidEndpoint.endpoint}'. The endpoint must be a valid HTTPS url.` + ) + ); + }); + + it('should throw error due to invalid api key', () => { + assert.throws( + () => new OpenAIClient(optionsWithEmptyAPIKey), + new Error(`OpenAIClient initialized without an 'apiKey'.`) + ); + }); + + it('should create a valid OpenAIClient with all fields', () => { + const openAIClient = new OpenAIClient(optionsWithAllFields); + + assert.equal(createStub.called, true); + assert.notEqual(openAIClient, undefined); + assert.equal(openAIClient.options.apiKey, optionsWithAllFields.apiKey); + assert.equal(openAIClient.options.organization, optionsWithAllFields.organization); + assert.equal(openAIClient.options.endpoint, optionsWithAllFields.endpoint); + assert.equal(openAIClient.options.headerKey, optionsWithAllFields.headerKey); + }); + }); + + describe('createChatCompletion', () => { + it('creates valid chat completion response with no endpoint', async () => { + const postStub = sinon.stub(mockAxios, 'post').returns(Promise.resolve(chatCompletionResponse)); + const url = `https://api.openai.com/v1/chat/completions`; + const response = await client.createChatCompletion(chatCompletionRequest); + + assert.equal(postStub.calledOnce, true); + assert.equal(postStub.calledOnceWith(url, chatCompletionRequest, header), true); + assert.equal(response.status, 200); + assert.equal(response.statusText, 'OK'); + assert.notEqual(response.data, undefined); + assert.equal(response.data?.object, 'chat.completion'); + }); + it('creates valid chat completion response with valid endpoint', async () => { + const postStub = sinon.stub(mockAxios, 'post').returns(Promise.resolve(chatCompletionResponse)); + const url = `${optionsWithAllFields.endpoint}/v1/chat/completions`; + const response = await clientWithAllFields.createChatCompletion(chatCompletionRequest); + + assert.equal(postStub.calledOnce, true); + assert.equal(postStub.calledOnceWith(url, chatCompletionRequest, headerWithAllFields), true); + assert.equal(response.status, 200); + assert.equal(response.statusText, 'OK'); + assert.notEqual(response.data, undefined); + assert.equal(response.data?.object, 'chat.completion'); + }); + }); + + describe('createEmbedding', () => { + it('creates valid embedding response with no endpoint', async () => { + const postStub = sinon.stub(mockAxios, 'post').returns(Promise.resolve(embeddingResponse)); + const url = `https://api.openai.com/v1/embeddings`; + const response = await client.createEmbedding(embeddingRequest); + + assert.equal(postStub.calledOnce, true); + assert.equal(postStub.calledOnceWith(url, embeddingRequest, header), true); + assert.equal(response.status, 200); + assert.equal(response.statusText, 'OK'); + assert.notEqual(response.data, undefined); + assert.equal(response.data?.object, 'list'); + }); + + it('creates valid embedding response with valid endpoint', async () => { + const postStub = sinon.stub(mockAxios, 'post').returns(Promise.resolve(embeddingResponse)); + const url = `${optionsWithAllFields.endpoint}/v1/embeddings`; + const response = await clientWithAllFields.createEmbedding(embeddingRequest); + + assert.equal(postStub.calledOnce, true); + assert.equal(postStub.calledOnceWith(url, embeddingRequest, headerWithAllFields), true); + assert.equal(response.status, 200); + assert.equal(response.statusText, 'OK'); + assert.notEqual(response.data, undefined); + assert.equal(response.data?.object, 'list'); + }); + }); + + describe('createModeration', () => { + it('creates valid moderation response with no endpoint', async () => { + const postStub = sinon.stub(mockAxios, 'post').returns(Promise.resolve(moderationResponse)); + const url = `https://api.openai.com/v1/moderations`; + const response = await client.createModeration(moderationRequest); + + assert.equal(postStub.calledOnce, true); + assert.equal(postStub.calledOnceWith(url, moderationRequest, header), true); + assert.equal(response.status, 200); + assert.equal(response.statusText, 'OK'); + }); + + it('creates valid moderation response with valid endpoint', async () => { + const postStub = sinon.stub(mockAxios, 'post').returns(Promise.resolve(moderationResponse)); + const url = `${optionsWithAllFields.endpoint}/v1/moderations`; + const response = await clientWithAllFields.createModeration(moderationRequest); + + assert.equal(postStub.calledOnce, true); + assert.equal(postStub.calledOnceWith(url, moderationRequest, headerWithAllFields), true); + assert.equal(response.status, 200); + assert.equal(response.statusText, 'OK'); + }); + }); +}); diff --git a/js/packages/teams-ai/src/internals/OpenAIClient.ts b/js/packages/teams-ai/src/internals/OpenAIClient.ts index 1bac4c218..e4c782c16 100644 --- a/js/packages/teams-ai/src/internals/OpenAIClient.ts +++ b/js/packages/teams-ai/src/internals/OpenAIClient.ts @@ -11,8 +11,6 @@ import axios, { AxiosInstance } from 'axios'; import { CreateChatCompletionRequest, CreateChatCompletionResponse, - CreateCompletionRequest, - CreateCompletionResponse, CreateEmbeddingRequest, CreateEmbeddingResponse, CreateModerationRequest, @@ -37,6 +35,8 @@ export interface OpenAIClientOptions { organization?: string; endpoint?: string; headerKey?: string; + apiVersion?: string; + ocpApimSubscriptionKey?: string; } /** @@ -78,11 +78,6 @@ export class OpenAIClient { public readonly options: OpenAIClientOptions; - public createCompletion(request: CreateCompletionRequest): Promise> { - const url = `${this.options.endpoint ?? this.DefaultEndpoint}/v1/completions`; - return this.post(url, request); - } - public createChatCompletion( request: CreateChatCompletionRequest ): Promise> { diff --git a/js/packages/teams-ai/src/internals/types.ts b/js/packages/teams-ai/src/internals/types.ts index 0319fc4ac..fc0f2e032 100644 --- a/js/packages/teams-ai/src/internals/types.ts +++ b/js/packages/teams-ai/src/internals/types.ts @@ -8,66 +8,6 @@ import { ChatCompletionAction } from '../models/ChatCompletionAction'; -/** - * @private - */ -export interface CreateCompletionRequest { - prompt?: CreateCompletionRequestPrompt | null; - suffix?: string | null; - max_tokens?: number | null; - temperature?: number | null; - top_p?: number | null; - n?: number | null; - stream?: boolean | null; - logprobs?: number | null; - echo?: boolean | null; - stop?: CreateCompletionRequestStop | null; - presence_penalty?: number | null; - frequency_penalty?: number | null; - best_of?: number | null; - logit_bias?: object | null; - user?: string; -} - -/** - * @private - */ -export interface OpenAICreateCompletionRequest extends CreateCompletionRequest { - model: string; -} - -/** - * @private - */ -export interface CreateCompletionResponse { - id: string; - object: string; - created: number; - model: string; - choices: Array; - usage?: CreateCompletionResponseUsage; -} - -/** - * @private - */ -export interface CreateCompletionResponseChoicesInner { - text?: string; - index?: number; - logprobs?: CreateCompletionResponseChoicesInnerLogprobs | null; - finish_reason?: string; -} - -/** - * @private - */ -export interface CreateCompletionResponseChoicesInnerLogprobs { - tokens?: Array; - token_logprobs?: Array; - top_logprobs?: Array; - text_offset?: Array; -} - /** * @private */ @@ -82,8 +22,7 @@ export interface CreateCompletionResponseUsage { */ export interface CreateChatCompletionRequest { messages: Array; - functions?: Array; - function_call?: CreateChatCompletionRequestFunctionCall; + model: string; temperature?: number | null; top_p?: number | null; n?: number | null; @@ -94,6 +33,12 @@ export interface CreateChatCompletionRequest { frequency_penalty?: number | null; logit_bias?: object | null; user?: string; + logprobs?: boolean | false; + top_logprobs?: number | null; + response_format?: object | null; + seed?: number | null; + tools?: Array; + tool_choice?: CreateChatCompletionRequestFunctionCall; } /** @@ -101,6 +46,14 @@ export interface CreateChatCompletionRequest { */ export declare type CreateChatCompletionRequestFunctionCall = CreateChatCompletionRequestFunctionCallOneOf | string; +/** + * @private + */ +export interface CreateChatCompletionTool { + type: 'function'; + function: ChatCompletionAction; +} + /** * @private */ @@ -115,14 +68,24 @@ export interface OpenAICreateChatCompletionRequest extends CreateChatCompletionR model: string; } +/** + * @private + */ +export interface ChatCompletionRequestMessageToolCall { + id: string; + type: 'function'; + function: ChatCompletionRequestMessageFunctionCall; +} + /** * @private */ export interface ChatCompletionRequestMessage { - role: 'system' | 'user' | 'assistant'; + role: 'system' | 'user' | 'assistant' | 'tool'; content: string; name?: string; - function_call?: ChatCompletionRequestMessageFunctionCall; + tool_calls?: Array; + tool_call_id?: string; } /** @@ -134,7 +97,8 @@ export interface CreateChatCompletionResponse { created: number; model: string; choices: Array; - usage?: CreateCompletionResponseUsage; + usage: CreateCompletionResponseUsage; + system_fingerprint: string; } /** @@ -144,6 +108,7 @@ export interface CreateChatCompletionResponseChoicesInner { index?: number; message?: ChatCompletionResponseMessage; finish_reason?: string; + logprobs?: object | null; } /** @@ -152,7 +117,7 @@ export interface CreateChatCompletionResponseChoicesInner { export interface ChatCompletionResponseMessage { role: 'system' | 'user' | 'assistant'; content: string | undefined; - function_call?: ChatCompletionRequestMessageFunctionCall; + tool_calls?: Array; } /** @@ -220,6 +185,8 @@ export interface CreateModerationResponseResultsInnerCategoryScores { */ export interface CreateEmbeddingRequest { input: CreateEmbeddingRequestInput; + model: string; + encoding_format?: string; user?: string; } @@ -272,6 +239,11 @@ export type CreateCompletionRequestStop = Array | string; */ export type CreateChatCompletionRequestStop = Array | string; +/** + * @private + */ +export type ChatCompletionRequestToolChoice = object | string; + /** * @private */ diff --git a/js/packages/teams-ai/src/models/OpenAIModel.ts b/js/packages/teams-ai/src/models/OpenAIModel.ts index 5f21d879a..061d6d38d 100644 --- a/js/packages/teams-ai/src/models/OpenAIModel.ts +++ b/js/packages/teams-ai/src/models/OpenAIModel.ts @@ -14,10 +14,7 @@ import { Colorize, CreateChatCompletionRequest, CreateChatCompletionResponse, - CreateCompletionRequest, - CreateCompletionResponse, - OpenAICreateChatCompletionRequest, - OpenAICreateCompletionRequest + OpenAICreateChatCompletionRequest } from '../internals'; import { Tokenizer } from '../tokenizers'; import { TurnContext } from 'botbuilder'; @@ -27,11 +24,6 @@ import { Memory } from '../MemoryFork'; * Base model options common to both OpenAI and Azure OpenAI services. */ export interface BaseOpenAIModelOptions { - /** - * Optional. Type of completion to use for the default model. Defaults to 'chat'. - */ - completion_type?: 'chat' | 'text'; - /** * Optional. Whether to log requests to the console. * @remarks @@ -197,175 +189,92 @@ export class OpenAIModel implements PromptCompletionModel { ): Promise> { const startTime = Date.now(); const max_input_tokens = template.config.completion.max_input_tokens; - const completion_type = template.config.completion.completion_type ?? this.options.completion_type; const model = template.config.completion.model ?? (this._useAzure ? (this.options as AzureOpenAIModelOptions).azureDefaultDeployment : (this.options as OpenAIModelOptions).defaultModel); - if (completion_type == 'text') { - // Render prompt - const result = await template.prompt.renderAsText(context, memory, functions, tokenizer, max_input_tokens); - if (result.tooLong) { - return { - status: 'too_long', - input: undefined, - error: new Error( - `The generated text completion prompt had a length of ${result.length} tokens which exceeded the max_input_tokens of ${max_input_tokens}.` - ) - }; - } - if (this.options.logRequests) { - console.log(Colorize.title('PROMPT:')); - console.log(Colorize.output(result.output)); - } - - // Call text completion API - const request: CreateCompletionRequest = this.copyOptionsToRequest( - { - prompt: result.output - }, - template.config.completion, - [ - 'max_tokens', - 'temperature', - 'top_p', - 'n', - 'stream', - 'logprobs', - 'echo', - 'stop', - 'presence_penalty', - 'frequency_penalty', - 'best_of', - 'logit_bias', - 'user' - ] - ); - const response = await this.createCompletion(request, model); - if (this.options.logRequests) { - console.log(Colorize.title('RESPONSE:')); - console.log(Colorize.value('status', response.status)); - console.log(Colorize.value('duration', Date.now() - startTime, 'ms')); - console.log(Colorize.output(response.data)); - } + // Render prompt + const result = await template.prompt.renderAsMessages(context, memory, functions, tokenizer, max_input_tokens); + if (result.tooLong) { + return { + status: 'too_long', + input: undefined, + error: new Error( + `The generated chat completion prompt had a length of ${result.length} tokens which exceeded the max_input_tokens of ${max_input_tokens}.` + ) + }; + } + if (!this.options.useSystemMessages && result.output.length > 0 && result.output[0].role == 'system') { + result.output[0].role = 'user'; + } + if (this.options.logRequests) { + console.log(Colorize.title('CHAT PROMPT:')); + console.log(Colorize.output(result.output)); + } - // Process response - if (response.status < 300) { - const completion = response.data.choices[0]; - return { - status: 'success', - input: undefined, - message: { role: 'assistant', content: completion.text ?? '' } - }; - } else if (response.status == 429) { - if (this.options.logRequests) { - console.log(Colorize.title('HEADERS:')); - console.log(Colorize.output(response.headers)); - } - return { - status: 'rate_limited', - input: undefined, - error: new Error(`The text completion API returned a rate limit error.`) - }; - } else { - return { - status: 'error', - input: undefined, - error: new Error( - `The text completion API returned an error status of ${response.status}: ${response.statusText}` - ) - }; - } - } else { - // Render prompt - const result = await template.prompt.renderAsMessages( - context, - memory, - functions, - tokenizer, - max_input_tokens - ); - if (result.tooLong) { - return { - status: 'too_long', - input: undefined, - error: new Error( - `The generated chat completion prompt had a length of ${result.length} tokens which exceeded the max_input_tokens of ${max_input_tokens}.` - ) - }; - } - if (!this.options.useSystemMessages && result.output.length > 0 && result.output[0].role == 'system') { - result.output[0].role = 'user'; - } - if (this.options.logRequests) { - console.log(Colorize.title('CHAT PROMPT:')); - console.log(Colorize.output(result.output)); - } + // Get input message + // - we're doing this here because the input message can be complex and include images. + let input: Message | undefined; + const last = result.output.length - 1; + if (last > 0 && result.output[last].role == 'user') { + input = result.output[last]; + } - // Get input message - // - we're doing this here because the input message can be complex and include images. - let input: Message | undefined; - const last = result.output.length - 1; - if (last > 0 && result.output[last].role == 'user') { - input = result.output[last]; - } + // Call chat completion API + const request: CreateChatCompletionRequest = this.copyOptionsToRequest( + { + messages: result.output as ChatCompletionRequestMessage[] + }, + template.config.completion, + [ + 'max_tokens', + 'temperature', + 'top_p', + 'n', + 'stream', + 'logprobs', + 'echo', + 'stop', + 'presence_penalty', + 'frequency_penalty', + 'best_of', + 'logit_bias', + 'user', + 'functions', + 'function_call' + ] + ); + const response = await this.createChatCompletion(request, model); + if (this.options.logRequests) { + console.log(Colorize.title('CHAT RESPONSE:')); + console.log(Colorize.value('status', response.status)); + console.log(Colorize.value('duration', Date.now() - startTime, 'ms')); + console.log(Colorize.output(response.data)); + } - // Call chat completion API - const request: CreateChatCompletionRequest = this.copyOptionsToRequest( - { - messages: result.output as ChatCompletionRequestMessage[] - }, - template.config.completion, - [ - 'max_tokens', - 'temperature', - 'top_p', - 'n', - 'stream', - 'logprobs', - 'echo', - 'stop', - 'presence_penalty', - 'frequency_penalty', - 'best_of', - 'logit_bias', - 'user', - 'functions', - 'function_call' - ] - ); - const response = await this.createChatCompletion(request, model); + // Process response + if (response.status < 300) { + const completion = response.data.choices[0]; + return { status: 'success', input, message: completion.message ?? { role: 'assistant', content: '' } }; + } else if (response.status == 429) { if (this.options.logRequests) { - console.log(Colorize.title('CHAT RESPONSE:')); - console.log(Colorize.value('status', response.status)); - console.log(Colorize.value('duration', Date.now() - startTime, 'ms')); - console.log(Colorize.output(response.data)); - } - - // Process response - if (response.status < 300) { - const completion = response.data.choices[0]; - return { status: 'success', input, message: completion.message ?? { role: 'assistant', content: '' } }; - } else if (response.status == 429) { - if (this.options.logRequests) { - console.log(Colorize.title('HEADERS:')); - console.log(Colorize.output(response.headers)); - } - return { - status: 'rate_limited', - input: undefined, - error: new Error(`The chat completion API returned a rate limit error.`) - }; - } else { - return { - status: 'error', - input: undefined, - error: new Error( - `The chat completion API returned an error status of ${response.status}: ${response.statusText}` - ) - }; + console.log(Colorize.title('HEADERS:')); + console.log(Colorize.output(response.headers)); } + return { + status: 'rate_limited', + input: undefined, + error: new Error(`The chat completion API returned a rate limit error.`) + }; + } else { + return { + status: 'error', + input: undefined, + error: new Error( + `The chat completion API returned an error status of ${response.status}: ${response.statusText}` + ) + }; } } @@ -385,29 +294,6 @@ export class OpenAIModel implements PromptCompletionModel { return target as TRequest; } - /** - * @param request - * @param model - * @private - */ - protected createCompletion( - request: CreateCompletionRequest, - model: string - ): Promise> { - if (this._useAzure) { - const options = this.options as AzureOpenAIModelOptions; - const url = `${ - options.azureEndpoint - }/openai/deployments/${model}/completions?api-version=${options.azureApiVersion!}`; - return this.post(url, request); - } else { - const options = this.options as OpenAIModelOptions; - const url = `${options.endpoint ?? 'https://api.openai.com'}/v1/completions`; - (request as OpenAICreateCompletionRequest).model = model; - return this.post(url, request); - } - } - /** * @param request * @param model