@celljs/ai-openai 3.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/lib/common/api/api-protocol.d.ts +47 -0
- package/lib/common/api/api-protocol.d.ts.map +1 -0
- package/lib/common/api/api-protocol.js +6 -0
- package/lib/common/api/api-protocol.js.map +1 -0
- package/lib/common/api/chat-request.d.ts +198 -0
- package/lib/common/api/chat-request.d.ts.map +1 -0
- package/lib/common/api/chat-request.js +145 -0
- package/lib/common/api/chat-request.js.map +1 -0
- package/lib/common/api/chat-response.d.ts +193 -0
- package/lib/common/api/chat-response.d.ts.map +1 -0
- package/lib/common/api/chat-response.js +124 -0
- package/lib/common/api/chat-response.js.map +1 -0
- package/lib/common/api/embeddings-request.d.ts +59 -0
- package/lib/common/api/embeddings-request.d.ts.map +1 -0
- package/lib/common/api/embeddings-request.js +49 -0
- package/lib/common/api/embeddings-request.js.map +1 -0
- package/lib/common/api/embeddings-response.d.ts +27 -0
- package/lib/common/api/embeddings-response.d.ts.map +1 -0
- package/lib/common/api/embeddings-response.js +30 -0
- package/lib/common/api/embeddings-response.js.map +1 -0
- package/lib/common/api/index.d.ts +10 -0
- package/lib/common/api/index.d.ts.map +1 -0
- package/lib/common/api/index.js +13 -0
- package/lib/common/api/index.js.map +1 -0
- package/lib/common/api/message.d.ts +216 -0
- package/lib/common/api/message.d.ts.map +1 -0
- package/lib/common/api/message.js +179 -0
- package/lib/common/api/message.js.map +1 -0
- package/lib/common/api/openai-api.d.ts +18 -0
- package/lib/common/api/openai-api.d.ts.map +1 -0
- package/lib/common/api/openai-api.js +99 -0
- package/lib/common/api/openai-api.js.map +1 -0
- package/lib/common/api/openai-api.spec.d.ts +2 -0
- package/lib/common/api/openai-api.spec.d.ts.map +1 -0
- package/lib/common/api/openai-api.spec.js +162 -0
- package/lib/common/api/openai-api.spec.js.map +1 -0
- package/lib/common/api/openai-model.d.ts +117 -0
- package/lib/common/api/openai-model.d.ts.map +1 -0
- package/lib/common/api/openai-model.js +121 -0
- package/lib/common/api/openai-model.js.map +1 -0
- package/lib/common/api/response-format.d.ts +60 -0
- package/lib/common/api/response-format.d.ts.map +1 -0
- package/lib/common/api/response-format.js +79 -0
- package/lib/common/api/response-format.js.map +1 -0
- package/lib/common/api/usage.d.ts +79 -0
- package/lib/common/api/usage.d.ts.map +1 -0
- package/lib/common/api/usage.js +95 -0
- package/lib/common/api/usage.js.map +1 -0
- package/lib/common/index.d.ts +4 -0
- package/lib/common/index.d.ts.map +1 -0
- package/lib/common/index.js +7 -0
- package/lib/common/index.js.map +1 -0
- package/lib/common/model/index.d.ts +5 -0
- package/lib/common/model/index.d.ts.map +1 -0
- package/lib/common/model/index.js +8 -0
- package/lib/common/model/index.js.map +1 -0
- package/lib/common/model/openai-chat-model.d.ts +37 -0
- package/lib/common/model/openai-chat-model.d.ts.map +1 -0
- package/lib/common/model/openai-chat-model.js +357 -0
- package/lib/common/model/openai-chat-model.js.map +1 -0
- package/lib/common/model/openai-chat-model.spec.d.ts +2 -0
- package/lib/common/model/openai-chat-model.spec.d.ts.map +1 -0
- package/lib/common/model/openai-chat-model.spec.js +82 -0
- package/lib/common/model/openai-chat-model.spec.js.map +1 -0
- package/lib/common/model/openai-chat-options.d.ts +159 -0
- package/lib/common/model/openai-chat-options.d.ts.map +1 -0
- package/lib/common/model/openai-chat-options.js +231 -0
- package/lib/common/model/openai-chat-options.js.map +1 -0
- package/lib/common/model/openai-embedding-model.d.ts +15 -0
- package/lib/common/model/openai-embedding-model.d.ts.map +1 -0
- package/lib/common/model/openai-embedding-model.js +64 -0
- package/lib/common/model/openai-embedding-model.js.map +1 -0
- package/lib/common/model/openai-embedding-model.spec.d.ts +2 -0
- package/lib/common/model/openai-embedding-model.spec.d.ts.map +1 -0
- package/lib/common/model/openai-embedding-model.spec.js +100 -0
- package/lib/common/model/openai-embedding-model.spec.js.map +1 -0
- package/lib/common/model/openai-embedding-options.d.ts +35 -0
- package/lib/common/model/openai-embedding-options.d.ts.map +1 -0
- package/lib/common/model/openai-embedding-options.js +56 -0
- package/lib/common/model/openai-embedding-options.js.map +1 -0
- package/lib/common/module.d.ts +4 -0
- package/lib/common/module.d.ts.map +1 -0
- package/lib/common/module.js +6 -0
- package/lib/common/module.js.map +1 -0
- package/lib/common/support/index.d.ts +3 -0
- package/lib/common/support/index.d.ts.map +1 -0
- package/lib/common/support/index.js +6 -0
- package/lib/common/support/index.js.map +1 -0
- package/lib/common/support/openai-api-response-headers.d.ts +34 -0
- package/lib/common/support/openai-api-response-headers.d.ts.map +1 -0
- package/lib/common/support/openai-api-response-headers.js +45 -0
- package/lib/common/support/openai-api-response-headers.js.map +1 -0
- package/lib/common/support/openai-response-header-extractor.d.ts +12 -0
- package/lib/common/support/openai-response-header-extractor.d.ts.map +1 -0
- package/lib/common/support/openai-response-header-extractor.js +131 -0
- package/lib/common/support/openai-response-header-extractor.js.map +1 -0
- package/lib/common/test/mock-openai-api.d.ts +13 -0
- package/lib/common/test/mock-openai-api.d.ts.map +1 -0
- package/lib/common/test/mock-openai-api.js +57 -0
- package/lib/common/test/mock-openai-api.js.map +1 -0
- package/lib/common/test/test-container.d.ts +3 -0
- package/lib/common/test/test-container.d.ts.map +1 -0
- package/lib/common/test/test-container.js +16 -0
- package/lib/common/test/test-container.js.map +1 -0
- package/lib/package.spec.d.ts +1 -0
- package/lib/package.spec.d.ts.map +1 -0
- package/lib/package.spec.js +4 -0
- package/lib/package.spec.js.map +1 -0
- package/package.json +46 -0
- package/src/common/api/api-protocol.ts +51 -0
- package/src/common/api/chat-request.ts +258 -0
- package/src/common/api/chat-response.ts +218 -0
- package/src/common/api/embeddings-request.ts +92 -0
- package/src/common/api/embeddings-response.ts +44 -0
- package/src/common/api/index.ts +9 -0
- package/src/common/api/message.ts +273 -0
- package/src/common/api/openai-api.spec.ts +175 -0
- package/src/common/api/openai-api.ts +111 -0
- package/src/common/api/openai-model.ts +137 -0
- package/src/common/api/response-format.ts +109 -0
- package/src/common/api/usage.ts +135 -0
- package/src/common/index.ts +3 -0
- package/src/common/model/index.ts +4 -0
- package/src/common/model/openai-chat-model.spec.ts +85 -0
- package/src/common/model/openai-chat-model.ts +399 -0
- package/src/common/model/openai-chat-options.ts +326 -0
- package/src/common/model/openai-embedding-model.spec.ts +112 -0
- package/src/common/model/openai-embedding-model.ts +83 -0
- package/src/common/model/openai-embedding-options.ts +72 -0
- package/src/common/module.ts +4 -0
- package/src/common/support/index.ts +2 -0
- package/src/common/support/openai-api-response-headers.ts +46 -0
- package/src/common/support/openai-response-header-extractor.ts +141 -0
- package/src/common/test/mock-openai-api.ts +70 -0
- package/src/common/test/test-container.ts +13 -0
- package/src/package.spec.ts +4 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './api-protocol';
|
|
2
|
+
export * from './chat-request';
|
|
3
|
+
export * from './chat-response';
|
|
4
|
+
export * from './embeddings-request';
|
|
5
|
+
export * from './embeddings-response';
|
|
6
|
+
export * from './openai-api';
|
|
7
|
+
export * from './message';
|
|
8
|
+
export * from './openai-model';
|
|
9
|
+
export * from './response-format';
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { Expose, Type } from 'class-transformer';
|
|
2
|
+
|
|
3
|
+
export enum ChatCompletionFinishReason {
|
|
4
|
+
/**
|
|
5
|
+
* The model hit a natural stop point or a provided stop sequence.
|
|
6
|
+
*/
|
|
7
|
+
STOP = 'stop',
|
|
8
|
+
/**
|
|
9
|
+
* The maximum number of tokens specified in the request was reached.
|
|
10
|
+
*/
|
|
11
|
+
LENGTH = 'length',
|
|
12
|
+
/**
|
|
13
|
+
* The content was omitted due to a flag from our content filters.
|
|
14
|
+
*/
|
|
15
|
+
CONTENT_FILTER = 'content_filter',
|
|
16
|
+
/**
|
|
17
|
+
* The model called a tool.
|
|
18
|
+
*/
|
|
19
|
+
TOOL_CALLS = 'tool_calls',
|
|
20
|
+
/**
|
|
21
|
+
* Only for compatibility with Mistral AI API
|
|
22
|
+
*/
|
|
23
|
+
TOOL_CALL = 'tool_call'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The role of the author of this message.
|
|
28
|
+
*/
|
|
29
|
+
export enum Role {
|
|
30
|
+
/**
|
|
31
|
+
* System message.
|
|
32
|
+
*/
|
|
33
|
+
SYSTEM = 'system',
|
|
34
|
+
/**
|
|
35
|
+
* User message.
|
|
36
|
+
*/
|
|
37
|
+
USER = 'user',
|
|
38
|
+
/**
|
|
39
|
+
* Assistant message.
|
|
40
|
+
*/
|
|
41
|
+
ASSISTANT = 'assistant',
|
|
42
|
+
/**
|
|
43
|
+
* Tool message.
|
|
44
|
+
*/
|
|
45
|
+
TOOL = 'tool'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The function definition.
|
|
50
|
+
*/
|
|
51
|
+
export class ChatCompletionFunction {
|
|
52
|
+
/**
|
|
53
|
+
* The name of the function.
|
|
54
|
+
*/
|
|
55
|
+
name: string;
|
|
56
|
+
/**
|
|
57
|
+
* The arguments that the model expects you to pass to the function.
|
|
58
|
+
*/
|
|
59
|
+
arguments: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The relevant tool call.
|
|
64
|
+
*/
|
|
65
|
+
export class ToolCall {
|
|
66
|
+
/**
|
|
67
|
+
* The index of the tool call in the list of tool calls. Required in
|
|
68
|
+
* case of streaming.
|
|
69
|
+
*/
|
|
70
|
+
index?: number;
|
|
71
|
+
/**
|
|
72
|
+
* The ID of the tool call. This ID must be referenced when you submit
|
|
73
|
+
* the tool outputs in using the Submit tool outputs to run endpoint.
|
|
74
|
+
*/
|
|
75
|
+
id: string;
|
|
76
|
+
/**
|
|
77
|
+
* The type of tool call the output is required for. For now, this is
|
|
78
|
+
* always function.
|
|
79
|
+
*/
|
|
80
|
+
type: string;
|
|
81
|
+
/**
|
|
82
|
+
* The function definition.
|
|
83
|
+
*/
|
|
84
|
+
function: ChatCompletionFunction;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Audio response from the model.
|
|
89
|
+
*/
|
|
90
|
+
export class AudioOutput {
|
|
91
|
+
/**
|
|
92
|
+
* Unique identifier for the audio response from the model.
|
|
93
|
+
*/
|
|
94
|
+
id?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Audio output from the model.
|
|
97
|
+
*/
|
|
98
|
+
data?: string;
|
|
99
|
+
/**
|
|
100
|
+
* When the audio content will no longer be available on the server.
|
|
101
|
+
*/
|
|
102
|
+
@Expose({ name: 'expires_at' })
|
|
103
|
+
expiresAt?: number;
|
|
104
|
+
/**
|
|
105
|
+
* Transcript of the audio output from the model.
|
|
106
|
+
*/
|
|
107
|
+
transcript?: string;
|
|
108
|
+
|
|
109
|
+
static create(id?: string, data?: string, expiresAt?: number, transcript?: string): AudioOutput {
|
|
110
|
+
const audio = new AudioOutput();
|
|
111
|
+
audio.id = id;
|
|
112
|
+
audio.data = data;
|
|
113
|
+
audio.expiresAt = expiresAt;
|
|
114
|
+
audio.transcript = transcript;
|
|
115
|
+
return audio;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The format of the encoded audio data. Currently supports "wav" and "mp3".
|
|
121
|
+
*/
|
|
122
|
+
export enum Format {
|
|
123
|
+
/**
|
|
124
|
+
* MP3 audio format.
|
|
125
|
+
*/
|
|
126
|
+
MP3 = 'mp3',
|
|
127
|
+
/**
|
|
128
|
+
* WAV audio format.
|
|
129
|
+
*/
|
|
130
|
+
WAV = 'wav'
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Audio input data.
|
|
135
|
+
*/
|
|
136
|
+
export class InputAudio {
|
|
137
|
+
/**
|
|
138
|
+
* Base64 encoded audio data.
|
|
139
|
+
*/
|
|
140
|
+
data: string;
|
|
141
|
+
/**
|
|
142
|
+
* The format of the encoded audio data. Currently supports "wav" and "mp3".
|
|
143
|
+
*/
|
|
144
|
+
format: Format;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Image content.
|
|
149
|
+
*/
|
|
150
|
+
export class ImageUrl {
|
|
151
|
+
/**
|
|
152
|
+
* Either a URL of the image or the base64 encoded image data. The
|
|
153
|
+
* base64 encoded image data must have a special prefix in the following
|
|
154
|
+
* format: "data:{mimetype};base64,{base64-encoded-image-data}".
|
|
155
|
+
*/
|
|
156
|
+
url: string;
|
|
157
|
+
/**
|
|
158
|
+
* Specifies the detail level of the image.
|
|
159
|
+
*/
|
|
160
|
+
detail?: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* An array of content parts with a defined type. Each MediaContent can be of
|
|
165
|
+
* either "text", "image_url", or "input_audio" type. Only one option allowed.
|
|
166
|
+
*/
|
|
167
|
+
export class MediaContent {
|
|
168
|
+
/**
|
|
169
|
+
* Content type, each can be of type text or image_url.
|
|
170
|
+
*/
|
|
171
|
+
type: string;
|
|
172
|
+
/**
|
|
173
|
+
* The text content of the message.
|
|
174
|
+
*/
|
|
175
|
+
text: string;
|
|
176
|
+
/**
|
|
177
|
+
* The image content of the message. You can pass multiple images
|
|
178
|
+
* by adding multiple image_url content parts. Image input is only supported when
|
|
179
|
+
* using the gpt-4-visual-preview model.
|
|
180
|
+
*/
|
|
181
|
+
@Expose({ name: 'image_url' })
|
|
182
|
+
@Type(() => ImageUrl)
|
|
183
|
+
imageUrl: ImageUrl;
|
|
184
|
+
/**
|
|
185
|
+
* Audio content part.
|
|
186
|
+
*/
|
|
187
|
+
@Expose({ name: 'input_audio' })
|
|
188
|
+
@Type(() => InputAudio)
|
|
189
|
+
inputAudio: InputAudio;
|
|
190
|
+
|
|
191
|
+
static createText(text: string): MediaContent {
|
|
192
|
+
const content = new MediaContent();
|
|
193
|
+
content.type = 'text';
|
|
194
|
+
content.text = text;
|
|
195
|
+
return content;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
static createImageUrl(url: string, detail?: string): MediaContent {
|
|
199
|
+
const content = new MediaContent();
|
|
200
|
+
content.type = 'image_url';
|
|
201
|
+
content.imageUrl = new ImageUrl();
|
|
202
|
+
content.imageUrl.url = url;
|
|
203
|
+
content.imageUrl.detail = detail;
|
|
204
|
+
return content;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
static createInputAudio(data: string, format: Format): MediaContent {
|
|
208
|
+
const content = new MediaContent();
|
|
209
|
+
content.type = 'input_audio';
|
|
210
|
+
content.inputAudio = new InputAudio();
|
|
211
|
+
content.inputAudio.data = data;
|
|
212
|
+
content.inputAudio.format = format;
|
|
213
|
+
return content;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export class ChatCompletionMessage {
|
|
218
|
+
/**
|
|
219
|
+
* The contents of the message. Can be either a {@link MediaContent}
|
|
220
|
+
* or a {@link String}. The response message content is always a {@link String}.
|
|
221
|
+
*/
|
|
222
|
+
@Expose({ name: 'content' })
|
|
223
|
+
content: any;
|
|
224
|
+
/**
|
|
225
|
+
* The role of the messages author. Could be one of the {@link Role}
|
|
226
|
+
* types.
|
|
227
|
+
*/
|
|
228
|
+
role: Role;
|
|
229
|
+
/**
|
|
230
|
+
* An optional name for the participant. Provides the model information to
|
|
231
|
+
* differentiate between participants of the same role. In case of Function calling,
|
|
232
|
+
* the name is the function name that the message is responding to.
|
|
233
|
+
*/
|
|
234
|
+
name?: string;
|
|
235
|
+
/**
|
|
236
|
+
* Tool call that this message is responding to. Only applicable for
|
|
237
|
+
* the {@link Role#TOOL} role and null otherwise.
|
|
238
|
+
*/
|
|
239
|
+
@Expose({ name: 'tool_call_id' })
|
|
240
|
+
toolCallId?: string;
|
|
241
|
+
/**
|
|
242
|
+
* The tool calls generated by the model, such as function calls.
|
|
243
|
+
* Applicable only for {@link Role#ASSISTANT} role and null otherwise.
|
|
244
|
+
*/
|
|
245
|
+
@Expose({ name: 'tool_calls' })
|
|
246
|
+
toolCalls?: ToolCall[];
|
|
247
|
+
/**
|
|
248
|
+
* The refusal message by the assistant. Applicable only for
|
|
249
|
+
* {@link Role#ASSISTANT} role and null otherwise.
|
|
250
|
+
*/
|
|
251
|
+
refusal?: string;
|
|
252
|
+
/**
|
|
253
|
+
* Audio response from the model.
|
|
254
|
+
*/
|
|
255
|
+
@Expose({ name: 'audio' })
|
|
256
|
+
@Type(() => AudioOutput)
|
|
257
|
+
audioOutput?: AudioOutput;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 创建一个新的聊天消息实例。
|
|
261
|
+
*
|
|
262
|
+
* @param text - 消息内容。
|
|
263
|
+
* @param role - 消息角色,表示发送者的身份。
|
|
264
|
+
* @returns 返回一个包含指定内容和角色的 ChatCompletionMessage 实例。
|
|
265
|
+
*/
|
|
266
|
+
static create(text: any, role: Role): ChatCompletionMessage {
|
|
267
|
+
const message = new ChatCompletionMessage();
|
|
268
|
+
message.content = text;
|
|
269
|
+
message.role = role;
|
|
270
|
+
return message;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { RestOperations } from '@celljs/http';
|
|
3
|
+
import { createContainer } from '../test/test-container';
|
|
4
|
+
import { OpenAIAPI } from './api-protocol';
|
|
5
|
+
import { ChatCompletionRequest } from './chat-request';
|
|
6
|
+
import { EmbeddingRequest } from './embeddings-request';
|
|
7
|
+
import { Role } from './message';
|
|
8
|
+
import '../index';
|
|
9
|
+
|
|
10
|
+
describe('OpenAIAPI', () => {
|
|
11
|
+
const container = createContainer();
|
|
12
|
+
let openaiAPI: OpenAIAPI;
|
|
13
|
+
|
|
14
|
+
// Mock response data
|
|
15
|
+
const mockResponses = {
|
|
16
|
+
chat: {
|
|
17
|
+
id: 'chatcmpl-123',
|
|
18
|
+
object: 'chat.completion',
|
|
19
|
+
created: 1677652288,
|
|
20
|
+
model: 'gpt-3.5-turbo',
|
|
21
|
+
choices: [{
|
|
22
|
+
index: 0,
|
|
23
|
+
message: {
|
|
24
|
+
role: 'assistant',
|
|
25
|
+
content: 'Hello! How can I help you today?'
|
|
26
|
+
},
|
|
27
|
+
finish_reason: 'stop'
|
|
28
|
+
}],
|
|
29
|
+
usage: {
|
|
30
|
+
prompt_tokens: 9,
|
|
31
|
+
completion_tokens: 12,
|
|
32
|
+
total_tokens: 21
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
stream: {
|
|
36
|
+
id: 'chatcmpl-123',
|
|
37
|
+
object: 'chat.completion.chunk',
|
|
38
|
+
created: 1677652288,
|
|
39
|
+
model: 'gpt-3.5-turbo',
|
|
40
|
+
choices: [{
|
|
41
|
+
index: 0,
|
|
42
|
+
delta: {
|
|
43
|
+
role: 'assistant',
|
|
44
|
+
content: 'Hello'
|
|
45
|
+
}
|
|
46
|
+
}]
|
|
47
|
+
},
|
|
48
|
+
embeddings: {
|
|
49
|
+
object: 'list',
|
|
50
|
+
data: [{ object: 'embedding', embedding: [0.1, 0.2, 0.3], index: 0 }],
|
|
51
|
+
model: 'text-embedding-3-small',
|
|
52
|
+
usage: { prompt_tokens: 8, total_tokens: 8 }
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
// Configure mock REST operations
|
|
58
|
+
container.rebind(RestOperations).toConstantValue({
|
|
59
|
+
post: async (url: string, data: any) => {
|
|
60
|
+
if (url === '/v1/embeddings') {
|
|
61
|
+
return { data: mockResponses.embeddings };
|
|
62
|
+
}
|
|
63
|
+
if (data.stream) {
|
|
64
|
+
const stream = new ReadableStream({
|
|
65
|
+
start(controller) {
|
|
66
|
+
controller.enqueue(`data: ${JSON.stringify(mockResponses.stream)}\n\n`);
|
|
67
|
+
controller.enqueue('data: [DONE]\n\n');
|
|
68
|
+
controller.close();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return { data: stream };
|
|
72
|
+
}
|
|
73
|
+
return { data: mockResponses.chat };
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
openaiAPI = container.get(OpenAIAPI);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('chat', () => {
|
|
80
|
+
it('should return correct response for non-streaming request', async () => {
|
|
81
|
+
const request = new ChatCompletionRequest();
|
|
82
|
+
request.model = 'gpt-3.5-turbo';
|
|
83
|
+
request.messages = [{ role: Role.USER, content: 'Hello' }];
|
|
84
|
+
|
|
85
|
+
const response = await openaiAPI.chat(request);
|
|
86
|
+
|
|
87
|
+
expect(response.body.id).to.equal('chatcmpl-123');
|
|
88
|
+
expect(response.body.choices[0].message.content).to.equal('Hello! How can I help you today?');
|
|
89
|
+
expect(response.body.usage.totalTokens).to.equal(21);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should throw error when stream is enabled', async () => {
|
|
93
|
+
const request = new ChatCompletionRequest();
|
|
94
|
+
request.model = 'gpt-3.5-turbo';
|
|
95
|
+
request.messages = [{ role: Role.USER, content: 'Hello' }];
|
|
96
|
+
request.stream = true;
|
|
97
|
+
try {
|
|
98
|
+
await openaiAPI.chat(request);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
expect(e).to.be.instanceOf(Error);
|
|
101
|
+
expect(e.message).to.include('Request must set the stream property to false.');
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('chat', () => {
|
|
107
|
+
it('should throw error when stream is disabled', async () => {
|
|
108
|
+
const request = new ChatCompletionRequest();
|
|
109
|
+
request.model = 'gpt-3.5-turbo';
|
|
110
|
+
request.messages = [{ role: Role.USER, content: 'Hello' }];
|
|
111
|
+
request.stream = false;
|
|
112
|
+
try {
|
|
113
|
+
await openaiAPI.chat(request);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
expect(e).to.be.instanceOf(Error);
|
|
116
|
+
expect(e.message).to.include('Request must set the stream property to true.');
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('streamingChat', () => {
|
|
122
|
+
it('should handle streaming response correctly', done => {
|
|
123
|
+
const request = new ChatCompletionRequest();
|
|
124
|
+
request.model = 'gpt-3.5-turbo';
|
|
125
|
+
request.messages = [{ role: Role.USER, content: 'Hello' }];
|
|
126
|
+
request.stream = true;
|
|
127
|
+
|
|
128
|
+
openaiAPI.streamingChat(request).then(response$ => {
|
|
129
|
+
response$.subscribe({
|
|
130
|
+
next: chunk => {
|
|
131
|
+
expect(chunk.body.id).to.equal('chatcmpl-123');
|
|
132
|
+
expect(chunk.body.choices[0].delta.content).to.equal('Hello');
|
|
133
|
+
done();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should throw error when stream is disabled', async () => {
|
|
140
|
+
const request = new ChatCompletionRequest();
|
|
141
|
+
request.model = 'gpt-3.5-turbo';
|
|
142
|
+
request.messages = [{ role: Role.USER, content: 'Hello' }];
|
|
143
|
+
request.stream = false;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
await openaiAPI.streamingChat(request);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
expect(e).to.be.instanceOf(Error);
|
|
149
|
+
expect(e.message).to.include('Request must set the stream property to true.');
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('embed', () => {
|
|
155
|
+
it('should return correct embedding vectors', async () => {
|
|
156
|
+
const request = EmbeddingRequest.fromInput('Hello world');
|
|
157
|
+
const response = await openaiAPI.embed(request);
|
|
158
|
+
|
|
159
|
+
expect(response.body.object).to.equal('list');
|
|
160
|
+
expect(response.body.data[0].embedding).to.deep.equal([0.1, 0.2, 0.3]);
|
|
161
|
+
expect(response.body.model).to.equal('text-embedding-3-small');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should support batch inputs', async () => {
|
|
165
|
+
const request = EmbeddingRequest.fromInputAndModel(
|
|
166
|
+
['Hello', 'World'],
|
|
167
|
+
'text-embedding-3-small'
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const response = await openaiAPI.embed(request);
|
|
171
|
+
expect(response.body.data[0].embedding).to.deep.equal([0.1, 0.2, 0.3]);
|
|
172
|
+
expect(response.body.usage.totalTokens).to.equal(8);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Assert, Autowired, Component, Optional, PostConstruct, Value } from '@celljs/core';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { map } from 'rxjs/operators';
|
|
4
|
+
import { HttpHeaders, MediaType, ResponseEntity, RestOperations } from '@celljs/http';
|
|
5
|
+
import { ChatCompletionRequest } from './chat-request';
|
|
6
|
+
import { EmbeddingRequest } from './embeddings-request';
|
|
7
|
+
import { EmbeddingResponse } from './embeddings-response';
|
|
8
|
+
import { ChatCompletion, ChatCompletionChunk } from './chat-response';
|
|
9
|
+
import { OpenAIAPI, OpenAIAPIOptions } from './api-protocol';
|
|
10
|
+
import { instanceToPlain, plainToInstance } from 'class-transformer';
|
|
11
|
+
import { SSEUtil } from '@celljs/ai-core';
|
|
12
|
+
|
|
13
|
+
@Component(OpenAIAPI)
|
|
14
|
+
export class OpenAIAPIImpl implements OpenAIAPI {
|
|
15
|
+
|
|
16
|
+
@Autowired(RestOperations)
|
|
17
|
+
protected readonly restOperations: RestOperations;
|
|
18
|
+
|
|
19
|
+
@Value('cell.ai.openai.api')
|
|
20
|
+
protected readonly apiOptions: OpenAIAPIOptions;
|
|
21
|
+
|
|
22
|
+
@Autowired(OpenAIAPIOptions)
|
|
23
|
+
@Optional()
|
|
24
|
+
protected readonly apiOptions2: OpenAIAPIOptions;
|
|
25
|
+
|
|
26
|
+
protected finalApiOptions: Required<OpenAIAPIOptions>;
|
|
27
|
+
|
|
28
|
+
@PostConstruct()
|
|
29
|
+
protected init() {
|
|
30
|
+
this.finalApiOptions = {
|
|
31
|
+
baseUrl: 'https://api.openai.com',
|
|
32
|
+
completionsPath: '/v1/chat/completions',
|
|
33
|
+
embeddingsPath: '/v1/embeddings',
|
|
34
|
+
...this.apiOptions,
|
|
35
|
+
...this.apiOptions2
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async chat(chatRequest: ChatCompletionRequest, additionalHttpHeader?: Record<string, string>): Promise<ResponseEntity<ChatCompletion>> {
|
|
40
|
+
Assert.isTrue(!chatRequest.stream, 'Request must set the stream property to false.');
|
|
41
|
+
const { data, status, headers } = await this.restOperations
|
|
42
|
+
.post(
|
|
43
|
+
this.finalApiOptions.completionsPath,
|
|
44
|
+
instanceToPlain(chatRequest),
|
|
45
|
+
{
|
|
46
|
+
baseURL: this.finalApiOptions.baseUrl,
|
|
47
|
+
headers: additionalHttpHeader
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
status,
|
|
53
|
+
headers,
|
|
54
|
+
body: plainToInstance(ChatCompletion, data)
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
async streamingChat(chatRequest: ChatCompletionRequest, additionalHttpHeader?: Record<string, string>): Promise<Observable<ResponseEntity<ChatCompletionChunk>>> {
|
|
59
|
+
Assert.isTrue(!!chatRequest.stream, 'Request must set the stream property to true.');
|
|
60
|
+
const { data, status, headers } = await this.restOperations
|
|
61
|
+
.post<ReadableStream>(
|
|
62
|
+
this.finalApiOptions.completionsPath,
|
|
63
|
+
instanceToPlain(chatRequest),
|
|
64
|
+
{
|
|
65
|
+
baseURL: this.finalApiOptions.baseUrl,
|
|
66
|
+
headers: {
|
|
67
|
+
[HttpHeaders.ACCEPT]: MediaType.TEXT_EVENT_STREAM,
|
|
68
|
+
...additionalHttpHeader
|
|
69
|
+
|
|
70
|
+
},
|
|
71
|
+
responseType: 'stream'
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
return SSEUtil.toObservable(data).pipe(map(item =>
|
|
75
|
+
({
|
|
76
|
+
status,
|
|
77
|
+
headers,
|
|
78
|
+
body: plainToInstance(ChatCompletionChunk, item.data)
|
|
79
|
+
})
|
|
80
|
+
));
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async embed<T>(embeddingRequest: EmbeddingRequest<T>): Promise<ResponseEntity<EmbeddingResponse>> {
|
|
85
|
+
// Input text to embed, encoded as a string or array of tokens. To embed multiple
|
|
86
|
+
// inputs in a single
|
|
87
|
+
// request, pass an array of strings or array of token arrays.
|
|
88
|
+
Assert.notNull(embeddingRequest.input, 'The input can not be null.');
|
|
89
|
+
Assert.isTrue(typeof embeddingRequest.input === 'string' || Array.isArray(embeddingRequest.input),
|
|
90
|
+
'The input must be either a String, or a List of Strings or List of List of integers.');
|
|
91
|
+
|
|
92
|
+
// The input must not exceed the max input tokens for the model (8192 tokens for
|
|
93
|
+
// text-embedding-ada-002), cannot
|
|
94
|
+
// be an empty string, and any array must be 2048 dimensions or less.
|
|
95
|
+
if (Array.isArray(embeddingRequest.input)) {
|
|
96
|
+
Assert.isTrue(embeddingRequest.input.length > 0, 'The input list can not be empty.');
|
|
97
|
+
Assert.isTrue(embeddingRequest.input.length <= 2048, 'The list must be 2048 dimensions or less');
|
|
98
|
+
Assert.isTrue(
|
|
99
|
+
typeof embeddingRequest.input[0] === 'string' || typeof embeddingRequest.input[0] === 'number' || Array.isArray(embeddingRequest.input[0]),
|
|
100
|
+
'The input must be either a String, or a List of Strings or list of list of integers.');
|
|
101
|
+
}
|
|
102
|
+
const { data, status, headers } = await this.restOperations
|
|
103
|
+
.post<EmbeddingResponse>(this.finalApiOptions.embeddingsPath, instanceToPlain(embeddingRequest), { baseURL: this.finalApiOptions.baseUrl });
|
|
104
|
+
return {
|
|
105
|
+
status,
|
|
106
|
+
headers,
|
|
107
|
+
body: plainToInstance(EmbeddingResponse, data)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* OpenAI Chat Completion Models:
|
|
4
|
+
*
|
|
5
|
+
* - [GPT-4o](https://platform.openai.com/docs/models/gpt-4o)
|
|
6
|
+
* - [GPT-4o mini](https://platform.openai.com/docs/models/gpt-4o-mini)
|
|
7
|
+
* - [GPT-4 and GPT-4 Turbo](https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo)
|
|
8
|
+
* - [GPT-3.5 Turbo](https://platform.openai.com/docs/models/gpt-3-5-turbo)
|
|
9
|
+
* - [DALL·E](https://platform.openai.com/docs/models/dall-e)
|
|
10
|
+
*/
|
|
11
|
+
export enum OpenAIModel {
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Points to the most recent snapshot of the o1 model: o1-preview-2024-09-12
|
|
15
|
+
*/
|
|
16
|
+
O1_PREVIEW = 'o1-preview',
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Latest o1 model snapshot
|
|
20
|
+
*/
|
|
21
|
+
O1_PREVIEW_2024_09_12 = 'o1-preview-2024-09-12',
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Points to the most recent o1-mini snapshot: o1-mini-2024-09-12
|
|
25
|
+
*/
|
|
26
|
+
O1_MINI = 'o1-mini',
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Latest o1-mini model snapshot
|
|
30
|
+
*/
|
|
31
|
+
O1_MINI_2024_09_12 = 'o1-mini-2024-09-12',
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Multimodal flagship model that’s cheaper and faster than GPT-4 Turbo. Currently
|
|
35
|
+
* points to gpt-4o-2024-05-13.
|
|
36
|
+
*/
|
|
37
|
+
GPT_4_O = 'gpt-4o',
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Preview release for audio inputs in chat completions.
|
|
41
|
+
*/
|
|
42
|
+
GPT_4_O_AUDIO_PREVIEW = 'gpt-4o-audio-preview',
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Affordable and intelligent small model for fast, lightweight tasks. GPT-4o mini
|
|
46
|
+
* is cheaper and more capable than GPT-3.5 Turbo. Currently points to
|
|
47
|
+
* gpt-4o-mini-2024-07-18.
|
|
48
|
+
*/
|
|
49
|
+
GPT_4_O_MINI = 'gpt-4o-mini',
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* GPT-4 Turbo with Vision The latest GPT-4 Turbo model with vision capabilities.
|
|
53
|
+
* Vision requests can now use JSON mode and function calling. Currently points to
|
|
54
|
+
* gpt-4-turbo-2024-04-09.
|
|
55
|
+
*/
|
|
56
|
+
GPT_4_TURBO = 'gpt-4-turbo',
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* GPT-4 Turbo with Vision model. Vision requests can now use JSON mode and
|
|
60
|
+
* function calling.
|
|
61
|
+
*/
|
|
62
|
+
GPT_4_TURBO_2024_04_09 = 'gpt-4-turbo-2024-04-09',
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* (New) GPT-4 Turbo - latest GPT-4 model intended to reduce cases of “laziness”
|
|
66
|
+
* where the model doesn’t complete a task. Returns a maximum of 4,096 output
|
|
67
|
+
* tokens. Context window: 128k tokens
|
|
68
|
+
* Currently points to gpt-4-0125-preview
|
|
69
|
+
*/
|
|
70
|
+
GPT_4_0125_PREVIEW = 'gpt-4-0125-preview',
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Currently points to gpt-4-0125-preview - model featuring improved instruction
|
|
74
|
+
* following, JSON mode, reproducible outputs, parallel function calling, and
|
|
75
|
+
* more. Returns a maximum of 4,096 output tokens Context window: 128k tokens
|
|
76
|
+
*/
|
|
77
|
+
GPT_4_TURBO_PREVIEW = 'gpt-4-turbo-preview',
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Currently points to gpt-4-0613. Snapshot of gpt-4 from June 13th 2023 with
|
|
81
|
+
* improved function calling support. Context window: 8k tokens
|
|
82
|
+
* Returns a maximum of 4,096 output tokens Context window: 8k tokens
|
|
83
|
+
*/
|
|
84
|
+
GPT_4 = 'gpt-4',
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Currently points to gpt-3.5-turbo-0125. model with higher accuracy at
|
|
88
|
+
* responding in requested formats and a fix for a bug which caused a text
|
|
89
|
+
* encoding issue for non-English language function calls. Returns a maximum of
|
|
90
|
+
* 4,096 Context window: 16k tokens
|
|
91
|
+
*/
|
|
92
|
+
GPT_3_5_TURBO = 'gpt-3.5-turbo',
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Currently points to gpt-3.5-turbo-0125. model with higher accuracy at
|
|
96
|
+
* responding in requested formats and a fix for a bug which caused a text
|
|
97
|
+
* encoding issue for non-English language function calls. Returns a maximum of
|
|
98
|
+
* 4,096 Context window: 16k tokens
|
|
99
|
+
*/
|
|
100
|
+
GPT_3_5_TURBO_0125 = 'gpt-3.5-turbo-0125',
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* GPT-3.5 Turbo model with improved instruction following, JSON mode,
|
|
104
|
+
* reproducible outputs, parallel function calling, and more. Returns a maximum of
|
|
105
|
+
* 4,096 output tokens. Context window: 16k tokens.
|
|
106
|
+
*/
|
|
107
|
+
GPT_3_5_TURBO_1106 = 'gpt-3.5-turbo-1106',
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Most capable embedding model for both English and non-English tasks. DIMENSION:
|
|
111
|
+
* 3072
|
|
112
|
+
*/
|
|
113
|
+
TEXT_EMBEDDING_3_LARGE = 'text-embedding-3-large',
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Increased performance over 2nd generation ada embedding model. DIMENSION: 1536
|
|
117
|
+
*/
|
|
118
|
+
TEXT_EMBEDDING_3_SMALL = 'text-embedding-3-small',
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Most capable 2nd generation embedding model, replacing 16 first generation
|
|
122
|
+
* models. DIMENSION: 1536
|
|
123
|
+
*/
|
|
124
|
+
TEXT_EMBEDDING_ADA_002 = 'text-embedding-ada-002',
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The latest DALL·E model released in Nov 2023.
|
|
128
|
+
*/
|
|
129
|
+
DALL_E_3 = 'dall-e-3',
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The previous DALL·E model released in Nov 2022. The 2nd iteration of DALL·E
|
|
133
|
+
* with more realistic, accurate, and 4x greater resolution images than the
|
|
134
|
+
* original model.
|
|
135
|
+
*/
|
|
136
|
+
DALL_E_2 = 'dall-e-2'
|
|
137
|
+
}
|