@ai-sdk/openai-compatible 2.0.15 → 2.0.17
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/CHANGELOG.md +12 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +23 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +23 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/chat/convert-openai-compatible-chat-usage.ts +55 -0
- package/src/chat/convert-to-openai-compatible-chat-messages.test.ts +1238 -0
- package/src/chat/convert-to-openai-compatible-chat-messages.ts +246 -0
- package/src/chat/get-response-metadata.ts +15 -0
- package/src/chat/map-openai-compatible-finish-reason.ts +19 -0
- package/src/chat/openai-compatible-api-types.ts +86 -0
- package/src/chat/openai-compatible-chat-language-model.test.ts +3292 -0
- package/src/chat/openai-compatible-chat-language-model.ts +830 -0
- package/src/chat/openai-compatible-chat-options.ts +34 -0
- package/src/chat/openai-compatible-metadata-extractor.ts +48 -0
- package/src/chat/openai-compatible-prepare-tools.test.ts +336 -0
- package/src/chat/openai-compatible-prepare-tools.ts +98 -0
- package/src/completion/convert-openai-compatible-completion-usage.ts +46 -0
- package/src/completion/convert-to-openai-compatible-completion-prompt.ts +93 -0
- package/src/completion/get-response-metadata.ts +15 -0
- package/src/completion/map-openai-compatible-finish-reason.ts +19 -0
- package/src/completion/openai-compatible-completion-language-model.test.ts +773 -0
- package/src/completion/openai-compatible-completion-language-model.ts +390 -0
- package/src/completion/openai-compatible-completion-options.ts +33 -0
- package/src/embedding/openai-compatible-embedding-model.test.ts +171 -0
- package/src/embedding/openai-compatible-embedding-model.ts +166 -0
- package/src/embedding/openai-compatible-embedding-options.ts +21 -0
- package/src/image/openai-compatible-image-model.test.ts +494 -0
- package/src/image/openai-compatible-image-model.ts +205 -0
- package/src/image/openai-compatible-image-settings.ts +1 -0
- package/src/index.ts +27 -0
- package/src/internal/index.ts +4 -0
- package/src/openai-compatible-error.ts +30 -0
- package/src/openai-compatible-provider.test.ts +329 -0
- package/src/openai-compatible-provider.ts +189 -0
- package/src/version.ts +5 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import {
|
|
2
|
+
APICallError,
|
|
3
|
+
LanguageModelV3,
|
|
4
|
+
LanguageModelV3CallOptions,
|
|
5
|
+
LanguageModelV3Content,
|
|
6
|
+
LanguageModelV3FinishReason,
|
|
7
|
+
LanguageModelV3GenerateResult,
|
|
8
|
+
LanguageModelV3StreamPart,
|
|
9
|
+
LanguageModelV3StreamResult,
|
|
10
|
+
SharedV3Warning,
|
|
11
|
+
} from '@ai-sdk/provider';
|
|
12
|
+
import {
|
|
13
|
+
combineHeaders,
|
|
14
|
+
createEventSourceResponseHandler,
|
|
15
|
+
createJsonErrorResponseHandler,
|
|
16
|
+
createJsonResponseHandler,
|
|
17
|
+
FetchFunction,
|
|
18
|
+
parseProviderOptions,
|
|
19
|
+
ParseResult,
|
|
20
|
+
postJsonToApi,
|
|
21
|
+
ResponseHandler,
|
|
22
|
+
} from '@ai-sdk/provider-utils';
|
|
23
|
+
import { z } from 'zod/v4';
|
|
24
|
+
import {
|
|
25
|
+
defaultOpenAICompatibleErrorStructure,
|
|
26
|
+
ProviderErrorStructure,
|
|
27
|
+
} from '../openai-compatible-error';
|
|
28
|
+
import { convertOpenAICompatibleCompletionUsage } from './convert-openai-compatible-completion-usage';
|
|
29
|
+
import { convertToOpenAICompatibleCompletionPrompt } from './convert-to-openai-compatible-completion-prompt';
|
|
30
|
+
import { getResponseMetadata } from './get-response-metadata';
|
|
31
|
+
import { mapOpenAICompatibleFinishReason } from './map-openai-compatible-finish-reason';
|
|
32
|
+
import {
|
|
33
|
+
OpenAICompatibleCompletionModelId,
|
|
34
|
+
openaiCompatibleCompletionProviderOptions,
|
|
35
|
+
} from './openai-compatible-completion-options';
|
|
36
|
+
|
|
37
|
+
type OpenAICompatibleCompletionConfig = {
|
|
38
|
+
provider: string;
|
|
39
|
+
includeUsage?: boolean;
|
|
40
|
+
headers: () => Record<string, string | undefined>;
|
|
41
|
+
url: (options: { modelId: string; path: string }) => string;
|
|
42
|
+
fetch?: FetchFunction;
|
|
43
|
+
errorStructure?: ProviderErrorStructure<any>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The supported URLs for the model.
|
|
47
|
+
*/
|
|
48
|
+
supportedUrls?: () => LanguageModelV3['supportedUrls'];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export class OpenAICompatibleCompletionLanguageModel
|
|
52
|
+
implements LanguageModelV3
|
|
53
|
+
{
|
|
54
|
+
readonly specificationVersion = 'v3';
|
|
55
|
+
|
|
56
|
+
readonly modelId: OpenAICompatibleCompletionModelId;
|
|
57
|
+
private readonly config: OpenAICompatibleCompletionConfig;
|
|
58
|
+
private readonly failedResponseHandler: ResponseHandler<APICallError>;
|
|
59
|
+
private readonly chunkSchema; // type inferred via constructor
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
modelId: OpenAICompatibleCompletionModelId,
|
|
63
|
+
config: OpenAICompatibleCompletionConfig,
|
|
64
|
+
) {
|
|
65
|
+
this.modelId = modelId;
|
|
66
|
+
this.config = config;
|
|
67
|
+
|
|
68
|
+
// initialize error handling:
|
|
69
|
+
const errorStructure =
|
|
70
|
+
config.errorStructure ?? defaultOpenAICompatibleErrorStructure;
|
|
71
|
+
this.chunkSchema = createOpenAICompatibleCompletionChunkSchema(
|
|
72
|
+
errorStructure.errorSchema,
|
|
73
|
+
);
|
|
74
|
+
this.failedResponseHandler = createJsonErrorResponseHandler(errorStructure);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get provider(): string {
|
|
78
|
+
return this.config.provider;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private get providerOptionsName(): string {
|
|
82
|
+
return this.config.provider.split('.')[0].trim();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get supportedUrls() {
|
|
86
|
+
return this.config.supportedUrls?.() ?? {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async getArgs({
|
|
90
|
+
prompt,
|
|
91
|
+
maxOutputTokens,
|
|
92
|
+
temperature,
|
|
93
|
+
topP,
|
|
94
|
+
topK,
|
|
95
|
+
frequencyPenalty,
|
|
96
|
+
presencePenalty,
|
|
97
|
+
stopSequences: userStopSequences,
|
|
98
|
+
responseFormat,
|
|
99
|
+
seed,
|
|
100
|
+
providerOptions,
|
|
101
|
+
tools,
|
|
102
|
+
toolChoice,
|
|
103
|
+
}: LanguageModelV3CallOptions) {
|
|
104
|
+
const warnings: SharedV3Warning[] = [];
|
|
105
|
+
|
|
106
|
+
// Parse provider options
|
|
107
|
+
const completionOptions =
|
|
108
|
+
(await parseProviderOptions({
|
|
109
|
+
provider: this.providerOptionsName,
|
|
110
|
+
providerOptions,
|
|
111
|
+
schema: openaiCompatibleCompletionProviderOptions,
|
|
112
|
+
})) ?? {};
|
|
113
|
+
|
|
114
|
+
if (topK != null) {
|
|
115
|
+
warnings.push({ type: 'unsupported', feature: 'topK' });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (tools?.length) {
|
|
119
|
+
warnings.push({ type: 'unsupported', feature: 'tools' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (toolChoice != null) {
|
|
123
|
+
warnings.push({ type: 'unsupported', feature: 'toolChoice' });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (responseFormat != null && responseFormat.type !== 'text') {
|
|
127
|
+
warnings.push({
|
|
128
|
+
type: 'unsupported',
|
|
129
|
+
feature: 'responseFormat',
|
|
130
|
+
details: 'JSON response format is not supported.',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const { prompt: completionPrompt, stopSequences } =
|
|
135
|
+
convertToOpenAICompatibleCompletionPrompt({ prompt });
|
|
136
|
+
|
|
137
|
+
const stop = [...(stopSequences ?? []), ...(userStopSequences ?? [])];
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
args: {
|
|
141
|
+
// model id:
|
|
142
|
+
model: this.modelId,
|
|
143
|
+
|
|
144
|
+
// model specific settings:
|
|
145
|
+
echo: completionOptions.echo,
|
|
146
|
+
logit_bias: completionOptions.logitBias,
|
|
147
|
+
suffix: completionOptions.suffix,
|
|
148
|
+
user: completionOptions.user,
|
|
149
|
+
|
|
150
|
+
// standardized settings:
|
|
151
|
+
max_tokens: maxOutputTokens,
|
|
152
|
+
temperature,
|
|
153
|
+
top_p: topP,
|
|
154
|
+
frequency_penalty: frequencyPenalty,
|
|
155
|
+
presence_penalty: presencePenalty,
|
|
156
|
+
seed,
|
|
157
|
+
...providerOptions?.[this.providerOptionsName],
|
|
158
|
+
|
|
159
|
+
// prompt:
|
|
160
|
+
prompt: completionPrompt,
|
|
161
|
+
|
|
162
|
+
// stop sequences:
|
|
163
|
+
stop: stop.length > 0 ? stop : undefined,
|
|
164
|
+
},
|
|
165
|
+
warnings,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async doGenerate(
|
|
170
|
+
options: LanguageModelV3CallOptions,
|
|
171
|
+
): Promise<LanguageModelV3GenerateResult> {
|
|
172
|
+
const { args, warnings } = await this.getArgs(options);
|
|
173
|
+
|
|
174
|
+
const {
|
|
175
|
+
responseHeaders,
|
|
176
|
+
value: response,
|
|
177
|
+
rawValue: rawResponse,
|
|
178
|
+
} = await postJsonToApi({
|
|
179
|
+
url: this.config.url({
|
|
180
|
+
path: '/completions',
|
|
181
|
+
modelId: this.modelId,
|
|
182
|
+
}),
|
|
183
|
+
headers: combineHeaders(this.config.headers(), options.headers),
|
|
184
|
+
body: args,
|
|
185
|
+
failedResponseHandler: this.failedResponseHandler,
|
|
186
|
+
successfulResponseHandler: createJsonResponseHandler(
|
|
187
|
+
openaiCompatibleCompletionResponseSchema,
|
|
188
|
+
),
|
|
189
|
+
abortSignal: options.abortSignal,
|
|
190
|
+
fetch: this.config.fetch,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const choice = response.choices[0];
|
|
194
|
+
const content: Array<LanguageModelV3Content> = [];
|
|
195
|
+
|
|
196
|
+
// text content:
|
|
197
|
+
if (choice.text != null && choice.text.length > 0) {
|
|
198
|
+
content.push({ type: 'text', text: choice.text });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
content,
|
|
203
|
+
usage: convertOpenAICompatibleCompletionUsage(response.usage),
|
|
204
|
+
finishReason: {
|
|
205
|
+
unified: mapOpenAICompatibleFinishReason(choice.finish_reason),
|
|
206
|
+
raw: choice.finish_reason,
|
|
207
|
+
},
|
|
208
|
+
request: { body: args },
|
|
209
|
+
response: {
|
|
210
|
+
...getResponseMetadata(response),
|
|
211
|
+
headers: responseHeaders,
|
|
212
|
+
body: rawResponse,
|
|
213
|
+
},
|
|
214
|
+
warnings,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async doStream(
|
|
219
|
+
options: LanguageModelV3CallOptions,
|
|
220
|
+
): Promise<LanguageModelV3StreamResult> {
|
|
221
|
+
const { args, warnings } = await this.getArgs(options);
|
|
222
|
+
|
|
223
|
+
const body = {
|
|
224
|
+
...args,
|
|
225
|
+
stream: true,
|
|
226
|
+
|
|
227
|
+
// only include stream_options when in strict compatibility mode:
|
|
228
|
+
stream_options: this.config.includeUsage
|
|
229
|
+
? { include_usage: true }
|
|
230
|
+
: undefined,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const { responseHeaders, value: response } = await postJsonToApi({
|
|
234
|
+
url: this.config.url({
|
|
235
|
+
path: '/completions',
|
|
236
|
+
modelId: this.modelId,
|
|
237
|
+
}),
|
|
238
|
+
headers: combineHeaders(this.config.headers(), options.headers),
|
|
239
|
+
body,
|
|
240
|
+
failedResponseHandler: this.failedResponseHandler,
|
|
241
|
+
successfulResponseHandler: createEventSourceResponseHandler(
|
|
242
|
+
this.chunkSchema,
|
|
243
|
+
),
|
|
244
|
+
abortSignal: options.abortSignal,
|
|
245
|
+
fetch: this.config.fetch,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
let finishReason: LanguageModelV3FinishReason = {
|
|
249
|
+
unified: 'other',
|
|
250
|
+
raw: undefined,
|
|
251
|
+
};
|
|
252
|
+
let usage:
|
|
253
|
+
| {
|
|
254
|
+
prompt_tokens: number | undefined;
|
|
255
|
+
completion_tokens: number | undefined;
|
|
256
|
+
total_tokens: number | undefined;
|
|
257
|
+
}
|
|
258
|
+
| undefined = undefined;
|
|
259
|
+
let isFirstChunk = true;
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
stream: response.pipeThrough(
|
|
263
|
+
new TransformStream<
|
|
264
|
+
ParseResult<z.infer<typeof this.chunkSchema>>,
|
|
265
|
+
LanguageModelV3StreamPart
|
|
266
|
+
>({
|
|
267
|
+
start(controller) {
|
|
268
|
+
controller.enqueue({ type: 'stream-start', warnings });
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
transform(chunk, controller) {
|
|
272
|
+
if (options.includeRawChunks) {
|
|
273
|
+
controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// handle failed chunk parsing / validation:
|
|
277
|
+
if (!chunk.success) {
|
|
278
|
+
finishReason = { unified: 'error', raw: undefined };
|
|
279
|
+
controller.enqueue({ type: 'error', error: chunk.error });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const value = chunk.value;
|
|
284
|
+
|
|
285
|
+
// handle error chunks:
|
|
286
|
+
if ('error' in value) {
|
|
287
|
+
finishReason = { unified: 'error', raw: undefined };
|
|
288
|
+
controller.enqueue({ type: 'error', error: value.error });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (isFirstChunk) {
|
|
293
|
+
isFirstChunk = false;
|
|
294
|
+
|
|
295
|
+
controller.enqueue({
|
|
296
|
+
type: 'response-metadata',
|
|
297
|
+
...getResponseMetadata(value),
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
controller.enqueue({
|
|
301
|
+
type: 'text-start',
|
|
302
|
+
id: '0',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (value.usage != null) {
|
|
307
|
+
usage = value.usage;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const choice = value.choices[0];
|
|
311
|
+
|
|
312
|
+
if (choice?.finish_reason != null) {
|
|
313
|
+
finishReason = {
|
|
314
|
+
unified: mapOpenAICompatibleFinishReason(choice.finish_reason),
|
|
315
|
+
raw: choice.finish_reason ?? undefined,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (choice?.text != null) {
|
|
320
|
+
controller.enqueue({
|
|
321
|
+
type: 'text-delta',
|
|
322
|
+
id: '0',
|
|
323
|
+
delta: choice.text,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
flush(controller) {
|
|
329
|
+
if (!isFirstChunk) {
|
|
330
|
+
controller.enqueue({ type: 'text-end', id: '0' });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
controller.enqueue({
|
|
334
|
+
type: 'finish',
|
|
335
|
+
finishReason,
|
|
336
|
+
usage: convertOpenAICompatibleCompletionUsage(usage),
|
|
337
|
+
});
|
|
338
|
+
},
|
|
339
|
+
}),
|
|
340
|
+
),
|
|
341
|
+
request: { body },
|
|
342
|
+
response: { headers: responseHeaders },
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const usageSchema = z.object({
|
|
348
|
+
prompt_tokens: z.number(),
|
|
349
|
+
completion_tokens: z.number(),
|
|
350
|
+
total_tokens: z.number(),
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// limited version of the schema, focussed on what is needed for the implementation
|
|
354
|
+
// this approach limits breakages when the API changes and increases efficiency
|
|
355
|
+
const openaiCompatibleCompletionResponseSchema = z.object({
|
|
356
|
+
id: z.string().nullish(),
|
|
357
|
+
created: z.number().nullish(),
|
|
358
|
+
model: z.string().nullish(),
|
|
359
|
+
choices: z.array(
|
|
360
|
+
z.object({
|
|
361
|
+
text: z.string(),
|
|
362
|
+
finish_reason: z.string(),
|
|
363
|
+
}),
|
|
364
|
+
),
|
|
365
|
+
usage: usageSchema.nullish(),
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// limited version of the schema, focussed on what is needed for the implementation
|
|
369
|
+
// this approach limits breakages when the API changes and increases efficiency
|
|
370
|
+
const createOpenAICompatibleCompletionChunkSchema = <
|
|
371
|
+
ERROR_SCHEMA extends z.core.$ZodType,
|
|
372
|
+
>(
|
|
373
|
+
errorSchema: ERROR_SCHEMA,
|
|
374
|
+
) =>
|
|
375
|
+
z.union([
|
|
376
|
+
z.object({
|
|
377
|
+
id: z.string().nullish(),
|
|
378
|
+
created: z.number().nullish(),
|
|
379
|
+
model: z.string().nullish(),
|
|
380
|
+
choices: z.array(
|
|
381
|
+
z.object({
|
|
382
|
+
text: z.string(),
|
|
383
|
+
finish_reason: z.string().nullish(),
|
|
384
|
+
index: z.number(),
|
|
385
|
+
}),
|
|
386
|
+
),
|
|
387
|
+
usage: usageSchema.nullish(),
|
|
388
|
+
}),
|
|
389
|
+
errorSchema,
|
|
390
|
+
]);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
|
|
3
|
+
export type OpenAICompatibleCompletionModelId = string;
|
|
4
|
+
|
|
5
|
+
export const openaiCompatibleCompletionProviderOptions = z.object({
|
|
6
|
+
/**
|
|
7
|
+
* Echo back the prompt in addition to the completion.
|
|
8
|
+
*/
|
|
9
|
+
echo: z.boolean().optional(),
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Modify the likelihood of specified tokens appearing in the completion.
|
|
13
|
+
*
|
|
14
|
+
* Accepts a JSON object that maps tokens (specified by their token ID in
|
|
15
|
+
* the GPT tokenizer) to an associated bias value from -100 to 100.
|
|
16
|
+
*/
|
|
17
|
+
logitBias: z.record(z.string(), z.number()).optional(),
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The suffix that comes after a completion of inserted text.
|
|
21
|
+
*/
|
|
22
|
+
suffix: z.string().optional(),
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A unique identifier representing your end-user, which can help providers to
|
|
26
|
+
* monitor and detect abuse.
|
|
27
|
+
*/
|
|
28
|
+
user: z.string().optional(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type OpenAICompatibleCompletionProviderOptions = z.infer<
|
|
32
|
+
typeof openaiCompatibleCompletionProviderOptions
|
|
33
|
+
>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { EmbeddingModelV3Embedding } from '@ai-sdk/provider';
|
|
3
|
+
import { createTestServer } from '@ai-sdk/test-server/with-vitest';
|
|
4
|
+
import { createOpenAICompatible } from '../openai-compatible-provider';
|
|
5
|
+
|
|
6
|
+
const dummyEmbeddings = [
|
|
7
|
+
[0.1, 0.2, 0.3, 0.4, 0.5],
|
|
8
|
+
[0.6, 0.7, 0.8, 0.9, 1.0],
|
|
9
|
+
];
|
|
10
|
+
const testValues = ['sunny day at the beach', 'rainy day in the city'];
|
|
11
|
+
|
|
12
|
+
const provider = createOpenAICompatible({
|
|
13
|
+
baseURL: 'https://my.api.com/v1/',
|
|
14
|
+
name: 'test-provider',
|
|
15
|
+
headers: {
|
|
16
|
+
Authorization: `Bearer test-api-key`,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
const model = provider.embeddingModel('text-embedding-3-large');
|
|
20
|
+
|
|
21
|
+
const server = createTestServer({
|
|
22
|
+
'https://my.api.com/v1/embeddings': {},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('doEmbed', () => {
|
|
26
|
+
function prepareJsonResponse({
|
|
27
|
+
embeddings = dummyEmbeddings,
|
|
28
|
+
usage = { prompt_tokens: 8, total_tokens: 8 },
|
|
29
|
+
headers,
|
|
30
|
+
}: {
|
|
31
|
+
embeddings?: EmbeddingModelV3Embedding[];
|
|
32
|
+
usage?: { prompt_tokens: number; total_tokens: number };
|
|
33
|
+
headers?: Record<string, string>;
|
|
34
|
+
} = {}) {
|
|
35
|
+
server.urls['https://my.api.com/v1/embeddings'].response = {
|
|
36
|
+
type: 'json-value',
|
|
37
|
+
headers,
|
|
38
|
+
body: {
|
|
39
|
+
object: 'list',
|
|
40
|
+
data: embeddings.map((embedding, i) => ({
|
|
41
|
+
object: 'embedding',
|
|
42
|
+
index: i,
|
|
43
|
+
embedding,
|
|
44
|
+
})),
|
|
45
|
+
model: 'text-embedding-3-large',
|
|
46
|
+
usage,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
it('should extract embedding', async () => {
|
|
52
|
+
prepareJsonResponse();
|
|
53
|
+
|
|
54
|
+
const { embeddings } = await model.doEmbed({ values: testValues });
|
|
55
|
+
|
|
56
|
+
expect(embeddings).toStrictEqual(dummyEmbeddings);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should expose the raw response headers', async () => {
|
|
60
|
+
prepareJsonResponse({
|
|
61
|
+
headers: { 'test-header': 'test-value' },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const { response } = await model.doEmbed({ values: testValues });
|
|
65
|
+
|
|
66
|
+
expect(response?.headers).toStrictEqual({
|
|
67
|
+
// default headers:
|
|
68
|
+
'content-length': '236',
|
|
69
|
+
'content-type': 'application/json',
|
|
70
|
+
|
|
71
|
+
// custom header
|
|
72
|
+
'test-header': 'test-value',
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should extract usage', async () => {
|
|
77
|
+
prepareJsonResponse({
|
|
78
|
+
usage: { prompt_tokens: 20, total_tokens: 20 },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const { usage } = await model.doEmbed({ values: testValues });
|
|
82
|
+
|
|
83
|
+
expect(usage).toStrictEqual({ tokens: 20 });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should pass the model and the values', async () => {
|
|
87
|
+
prepareJsonResponse();
|
|
88
|
+
|
|
89
|
+
await model.doEmbed({ values: testValues });
|
|
90
|
+
|
|
91
|
+
expect(await server.calls[0].requestBodyJson).toStrictEqual({
|
|
92
|
+
model: 'text-embedding-3-large',
|
|
93
|
+
input: testValues,
|
|
94
|
+
encoding_format: 'float',
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should pass the dimensions setting', async () => {
|
|
99
|
+
prepareJsonResponse();
|
|
100
|
+
|
|
101
|
+
await provider.embeddingModel('text-embedding-3-large').doEmbed({
|
|
102
|
+
values: testValues,
|
|
103
|
+
providerOptions: {
|
|
104
|
+
openaiCompatible: {
|
|
105
|
+
dimensions: 64,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(await server.calls[0].requestBodyJson).toStrictEqual({
|
|
111
|
+
model: 'text-embedding-3-large',
|
|
112
|
+
input: testValues,
|
|
113
|
+
encoding_format: 'float',
|
|
114
|
+
dimensions: 64,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should pass settings with deprecated openai-compatible key and emit warning', async () => {
|
|
119
|
+
prepareJsonResponse();
|
|
120
|
+
|
|
121
|
+
const result = await provider
|
|
122
|
+
.embeddingModel('text-embedding-3-large')
|
|
123
|
+
.doEmbed({
|
|
124
|
+
values: testValues,
|
|
125
|
+
providerOptions: {
|
|
126
|
+
'openai-compatible': {
|
|
127
|
+
dimensions: 64,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(await server.calls[0].requestBodyJson).toStrictEqual({
|
|
133
|
+
model: 'text-embedding-3-large',
|
|
134
|
+
input: testValues,
|
|
135
|
+
encoding_format: 'float',
|
|
136
|
+
dimensions: 64,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(result.warnings).toContainEqual({
|
|
140
|
+
type: 'other',
|
|
141
|
+
message: `The 'openai-compatible' key in providerOptions is deprecated. Use 'openaiCompatible' instead.`,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should pass headers', async () => {
|
|
146
|
+
prepareJsonResponse();
|
|
147
|
+
|
|
148
|
+
const provider = createOpenAICompatible({
|
|
149
|
+
baseURL: 'https://my.api.com/v1/',
|
|
150
|
+
name: 'test-provider',
|
|
151
|
+
headers: {
|
|
152
|
+
Authorization: `Bearer test-api-key`,
|
|
153
|
+
'Custom-Provider-Header': 'provider-header-value',
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await provider.embeddingModel('text-embedding-3-large').doEmbed({
|
|
158
|
+
values: testValues,
|
|
159
|
+
headers: {
|
|
160
|
+
'Custom-Request-Header': 'request-header-value',
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(server.calls[0].requestHeaders).toStrictEqual({
|
|
165
|
+
authorization: 'Bearer test-api-key',
|
|
166
|
+
'content-type': 'application/json',
|
|
167
|
+
'custom-provider-header': 'provider-header-value',
|
|
168
|
+
'custom-request-header': 'request-header-value',
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|