@ai-sdk/togetherai 2.0.16 → 2.0.18

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.
@@ -0,0 +1,101 @@
1
+ import { RerankingModelV3 } from '@ai-sdk/provider';
2
+ import {
3
+ combineHeaders,
4
+ createJsonErrorResponseHandler,
5
+ createJsonResponseHandler,
6
+ FetchFunction,
7
+ parseProviderOptions,
8
+ postJsonToApi,
9
+ } from '@ai-sdk/provider-utils';
10
+ import {
11
+ togetheraiErrorSchema,
12
+ TogetherAIRerankingInput,
13
+ togetheraiRerankingResponseSchema,
14
+ } from './togetherai-reranking-api';
15
+ import {
16
+ TogetherAIRerankingModelId,
17
+ togetheraiRerankingOptionsSchema,
18
+ } from './togetherai-reranking-options';
19
+
20
+ type TogetherAIRerankingConfig = {
21
+ provider: string;
22
+ baseURL: string;
23
+ headers: () => Record<string, string | undefined>;
24
+ fetch?: FetchFunction;
25
+ };
26
+
27
+ export class TogetherAIRerankingModel implements RerankingModelV3 {
28
+ readonly specificationVersion = 'v3';
29
+ readonly modelId: TogetherAIRerankingModelId;
30
+
31
+ private readonly config: TogetherAIRerankingConfig;
32
+
33
+ constructor(
34
+ modelId: TogetherAIRerankingModelId,
35
+ config: TogetherAIRerankingConfig,
36
+ ) {
37
+ this.modelId = modelId;
38
+ this.config = config;
39
+ }
40
+
41
+ get provider(): string {
42
+ return this.config.provider;
43
+ }
44
+
45
+ // see https://docs.together.ai/reference/rerank-1
46
+ async doRerank({
47
+ documents,
48
+ headers,
49
+ query,
50
+ topN,
51
+ abortSignal,
52
+ providerOptions,
53
+ }: Parameters<RerankingModelV3['doRerank']>[0]): Promise<
54
+ Awaited<ReturnType<RerankingModelV3['doRerank']>>
55
+ > {
56
+ const rerankingOptions = await parseProviderOptions({
57
+ provider: 'togetherai',
58
+ providerOptions,
59
+ schema: togetheraiRerankingOptionsSchema,
60
+ });
61
+
62
+ const {
63
+ responseHeaders,
64
+ value: response,
65
+ rawValue,
66
+ } = await postJsonToApi({
67
+ url: `${this.config.baseURL}/rerank`,
68
+ headers: combineHeaders(this.config.headers(), headers),
69
+ body: {
70
+ model: this.modelId,
71
+ documents: documents.values,
72
+ query,
73
+ top_n: topN,
74
+ rank_fields: rerankingOptions?.rankFields,
75
+ return_documents: false, // reduce response size
76
+ } satisfies TogetherAIRerankingInput,
77
+ failedResponseHandler: createJsonErrorResponseHandler({
78
+ errorSchema: togetheraiErrorSchema,
79
+ errorToMessage: data => data.error.message,
80
+ }),
81
+ successfulResponseHandler: createJsonResponseHandler(
82
+ togetheraiRerankingResponseSchema,
83
+ ),
84
+ abortSignal,
85
+ fetch: this.config.fetch,
86
+ });
87
+
88
+ return {
89
+ ranking: response.results.map(result => ({
90
+ index: result.index,
91
+ relevanceScore: result.relevance_score,
92
+ })),
93
+ response: {
94
+ id: response.id ?? undefined,
95
+ modelId: response.model ?? undefined,
96
+ headers: responseHeaders,
97
+ body: rawValue,
98
+ },
99
+ };
100
+ }
101
+ }
@@ -0,0 +1,27 @@
1
+ import { FlexibleSchema, lazySchema, zodSchema } from '@ai-sdk/provider-utils';
2
+ import { z } from 'zod/v4';
3
+
4
+ // see https://docs.together.ai/docs/serverless-models#rerank-models
5
+ export type TogetherAIRerankingModelId =
6
+ | 'Salesforce/Llama-Rank-v1'
7
+ | 'mixedbread-ai/Mxbai-Rerank-Large-V2'
8
+ | (string & {});
9
+
10
+ export type TogetherAIRerankingOptions = {
11
+ /**
12
+ * List of keys in the JSON Object document to rank by.
13
+ * Defaults to use all supplied keys for ranking.
14
+ *
15
+ * @example ["title", "text"]
16
+ */
17
+ rankFields?: string[];
18
+ };
19
+
20
+ export const togetheraiRerankingOptionsSchema: FlexibleSchema<TogetherAIRerankingOptions> =
21
+ lazySchema(() =>
22
+ zodSchema(
23
+ z.object({
24
+ rankFields: z.array(z.string()).optional(),
25
+ }),
26
+ ),
27
+ );
@@ -0,0 +1,36 @@
1
+ // https://docs.together.ai/docs/serverless-models#chat-models
2
+ export type TogetherAIChatModelId =
3
+ | 'meta-llama/Llama-3.3-70B-Instruct-Turbo'
4
+ | 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo'
5
+ | 'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo'
6
+ | 'meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo'
7
+ | 'meta-llama/Meta-Llama-3-8B-Instruct-Turbo'
8
+ | 'meta-llama/Meta-Llama-3-70B-Instruct-Turbo'
9
+ | 'meta-llama/Llama-3.2-3B-Instruct-Turbo'
10
+ | 'meta-llama/Meta-Llama-3-8B-Instruct-Lite'
11
+ | 'meta-llama/Meta-Llama-3-70B-Instruct-Lite'
12
+ | 'meta-llama/Llama-3-8b-chat-hf'
13
+ | 'meta-llama/Llama-3-70b-chat-hf'
14
+ | 'nvidia/Llama-3.1-Nemotron-70B-Instruct-HF'
15
+ | 'Qwen/Qwen2.5-Coder-32B-Instruct'
16
+ | 'Qwen/QwQ-32B-Preview'
17
+ | 'microsoft/WizardLM-2-8x22B'
18
+ | 'google/gemma-2-27b-it'
19
+ | 'google/gemma-2-9b-it'
20
+ | 'databricks/dbrx-instruct'
21
+ | 'deepseek-ai/deepseek-llm-67b-chat'
22
+ | 'deepseek-ai/DeepSeek-V3'
23
+ | 'google/gemma-2b-it'
24
+ | 'Gryphe/MythoMax-L2-13b'
25
+ | 'meta-llama/Llama-2-13b-chat-hf'
26
+ | 'mistralai/Mistral-7B-Instruct-v0.1'
27
+ | 'mistralai/Mistral-7B-Instruct-v0.2'
28
+ | 'mistralai/Mistral-7B-Instruct-v0.3'
29
+ | 'mistralai/Mixtral-8x7B-Instruct-v0.1'
30
+ | 'mistralai/Mixtral-8x22B-Instruct-v0.1'
31
+ | 'NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO'
32
+ | 'Qwen/Qwen2.5-7B-Instruct-Turbo'
33
+ | 'Qwen/Qwen2.5-72B-Instruct-Turbo'
34
+ | 'Qwen/Qwen2-72B-Instruct'
35
+ | 'upstage/SOLAR-10.7B-Instruct-v1.0'
36
+ | (string & {});
@@ -0,0 +1,9 @@
1
+ // https://docs.together.ai/docs/serverless-models#language-models
2
+ export type TogetherAICompletionModelId =
3
+ | 'meta-llama/Llama-2-70b-hf'
4
+ | 'mistralai/Mistral-7B-v0.1'
5
+ | 'mistralai/Mixtral-8x7B-v0.1'
6
+ | 'Meta-Llama/Llama-Guard-7b'
7
+ | 'codellama/CodeLlama-34b-Instruct-hf'
8
+ | 'Qwen/Qwen2.5-Coder-32B-Instruct'
9
+ | (string & {});
@@ -0,0 +1,11 @@
1
+ // https://docs.together.ai/docs/serverless-models#embedding-models
2
+ export type TogetherAIEmbeddingModelId =
3
+ | 'togethercomputer/m2-bert-80M-2k-retrieval'
4
+ | 'togethercomputer/m2-bert-80M-32k-retrieval'
5
+ | 'togethercomputer/m2-bert-80M-8k-retrieval'
6
+ | 'WhereIsAI/UAE-Large-V1'
7
+ | 'BAAI/bge-large-en-v1.5'
8
+ | 'BAAI/bge-base-en-v1.5'
9
+ | 'sentence-transformers/msmarco-bert-base-dot-v5'
10
+ | 'bert-base-uncased'
11
+ | (string & {});
@@ -0,0 +1,488 @@
1
+ import { FetchFunction } from '@ai-sdk/provider-utils';
2
+ import { createTestServer } from '@ai-sdk/test-server/with-vitest';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { TogetherAIImageModel } from './togetherai-image-model';
5
+
6
+ const prompt = 'A cute baby sea otter';
7
+
8
+ function createBasicModel({
9
+ headers,
10
+ fetch,
11
+ currentDate,
12
+ }: {
13
+ headers?: () => Record<string, string>;
14
+ fetch?: FetchFunction;
15
+ currentDate?: () => Date;
16
+ } = {}) {
17
+ return new TogetherAIImageModel('stabilityai/stable-diffusion-xl', {
18
+ provider: 'togetherai',
19
+ baseURL: 'https://api.example.com',
20
+ headers: headers ?? (() => ({ 'api-key': 'test-key' })),
21
+ fetch,
22
+ _internal: {
23
+ currentDate,
24
+ },
25
+ });
26
+ }
27
+
28
+ const server = createTestServer({
29
+ 'https://api.example.com/*': {
30
+ response: {
31
+ type: 'json-value',
32
+ body: {
33
+ id: 'test-id',
34
+ data: [{ index: 0, b64_json: 'test-base64-content' }],
35
+ model: 'stabilityai/stable-diffusion-xl',
36
+ object: 'list',
37
+ },
38
+ },
39
+ },
40
+ });
41
+
42
+ describe('doGenerate', () => {
43
+ it('should pass the correct parameters including size and seed', async () => {
44
+ const model = createBasicModel();
45
+
46
+ await model.doGenerate({
47
+ prompt,
48
+ files: undefined,
49
+ mask: undefined,
50
+ n: 1,
51
+ size: '1024x1024',
52
+ seed: 42,
53
+ providerOptions: { togetherai: { additional_param: 'value' } },
54
+ aspectRatio: undefined,
55
+ });
56
+
57
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
58
+ model: 'stabilityai/stable-diffusion-xl',
59
+ prompt,
60
+ seed: 42,
61
+ width: 1024,
62
+ height: 1024,
63
+ response_format: 'base64',
64
+ additional_param: 'value',
65
+ });
66
+ });
67
+
68
+ it('should include n parameter when requesting multiple images', async () => {
69
+ const model = createBasicModel();
70
+
71
+ await model.doGenerate({
72
+ prompt,
73
+ files: undefined,
74
+ mask: undefined,
75
+ n: 3,
76
+ size: '1024x1024',
77
+ seed: 42,
78
+ providerOptions: {},
79
+ aspectRatio: undefined,
80
+ });
81
+
82
+ expect(await server.calls[0].requestBodyJson).toStrictEqual({
83
+ model: 'stabilityai/stable-diffusion-xl',
84
+ prompt,
85
+ seed: 42,
86
+ n: 3,
87
+ width: 1024,
88
+ height: 1024,
89
+ response_format: 'base64',
90
+ });
91
+ });
92
+
93
+ it('should call the correct url', async () => {
94
+ const model = createBasicModel();
95
+
96
+ await model.doGenerate({
97
+ prompt,
98
+ files: undefined,
99
+ mask: undefined,
100
+ n: 1,
101
+ size: '1024x1024',
102
+ seed: 42,
103
+ providerOptions: {},
104
+ aspectRatio: undefined,
105
+ });
106
+
107
+ expect(server.calls[0].requestMethod).toStrictEqual('POST');
108
+ expect(server.calls[0].requestUrl).toStrictEqual(
109
+ 'https://api.example.com/images/generations',
110
+ );
111
+ });
112
+
113
+ it('should pass headers', async () => {
114
+ const modelWithHeaders = createBasicModel({
115
+ headers: () => ({
116
+ 'Custom-Provider-Header': 'provider-header-value',
117
+ }),
118
+ });
119
+
120
+ await modelWithHeaders.doGenerate({
121
+ prompt,
122
+ files: undefined,
123
+ mask: undefined,
124
+ n: 1,
125
+ size: undefined,
126
+ seed: undefined,
127
+ providerOptions: {},
128
+ aspectRatio: undefined,
129
+ headers: {
130
+ 'Custom-Request-Header': 'request-header-value',
131
+ },
132
+ });
133
+
134
+ expect(server.calls[0].requestHeaders).toStrictEqual({
135
+ 'content-type': 'application/json',
136
+ 'custom-provider-header': 'provider-header-value',
137
+ 'custom-request-header': 'request-header-value',
138
+ });
139
+ });
140
+
141
+ it('should handle API errors', async () => {
142
+ server.urls['https://api.example.com/*'].response = {
143
+ type: 'error',
144
+ status: 400,
145
+ body: JSON.stringify({
146
+ error: {
147
+ message: 'Bad Request',
148
+ },
149
+ }),
150
+ };
151
+
152
+ const model = createBasicModel();
153
+ await expect(
154
+ model.doGenerate({
155
+ prompt,
156
+ files: undefined,
157
+ mask: undefined,
158
+ n: 1,
159
+ size: undefined,
160
+ seed: undefined,
161
+ providerOptions: {},
162
+ aspectRatio: undefined,
163
+ }),
164
+ ).rejects.toMatchObject({
165
+ message: 'Bad Request',
166
+ });
167
+ });
168
+
169
+ describe('warnings', () => {
170
+ it('should return aspectRatio warning when aspectRatio is provided', async () => {
171
+ const model = createBasicModel();
172
+
173
+ const result = await model.doGenerate({
174
+ prompt,
175
+ files: undefined,
176
+ mask: undefined,
177
+ n: 1,
178
+ size: '1024x1024',
179
+ aspectRatio: '1:1',
180
+ seed: 123,
181
+ providerOptions: {},
182
+ });
183
+
184
+ expect(result.warnings).toMatchInlineSnapshot(`
185
+ [
186
+ {
187
+ "details": "This model does not support the \`aspectRatio\` option. Use \`size\` instead.",
188
+ "feature": "aspectRatio",
189
+ "type": "unsupported",
190
+ },
191
+ ]
192
+ `);
193
+ });
194
+ });
195
+
196
+ it('should respect the abort signal', async () => {
197
+ const model = createBasicModel();
198
+ const controller = new AbortController();
199
+
200
+ const generatePromise = model.doGenerate({
201
+ prompt,
202
+ files: undefined,
203
+ mask: undefined,
204
+ n: 1,
205
+ size: undefined,
206
+ seed: undefined,
207
+ providerOptions: {},
208
+ aspectRatio: undefined,
209
+ abortSignal: controller.signal,
210
+ });
211
+
212
+ controller.abort();
213
+
214
+ await expect(generatePromise).rejects.toThrow('This operation was aborted');
215
+ });
216
+
217
+ describe('response metadata', () => {
218
+ it('should include timestamp, headers and modelId in response', async () => {
219
+ const testDate = new Date('2024-01-01T00:00:00Z');
220
+ const model = createBasicModel({
221
+ currentDate: () => testDate,
222
+ });
223
+
224
+ const result = await model.doGenerate({
225
+ prompt,
226
+ files: undefined,
227
+ mask: undefined,
228
+ n: 1,
229
+ size: undefined,
230
+ seed: undefined,
231
+ providerOptions: {},
232
+ aspectRatio: undefined,
233
+ });
234
+
235
+ expect(result.response).toStrictEqual({
236
+ timestamp: testDate,
237
+ modelId: 'stabilityai/stable-diffusion-xl',
238
+ headers: expect.any(Object),
239
+ });
240
+ });
241
+
242
+ it('should include response headers from API call', async () => {
243
+ server.urls['https://api.example.com/*'].response = {
244
+ type: 'json-value',
245
+ body: {
246
+ id: 'test-id',
247
+ data: [{ index: 0, b64_json: 'test-base64-content' }],
248
+ model: 'stabilityai/stable-diffusion-xl',
249
+ object: 'list',
250
+ },
251
+ headers: {
252
+ 'x-request-id': 'test-request-id',
253
+ 'content-length': '128',
254
+ },
255
+ };
256
+
257
+ const model = createBasicModel();
258
+ const result = await model.doGenerate({
259
+ prompt,
260
+ files: undefined,
261
+ mask: undefined,
262
+ n: 1,
263
+ size: undefined,
264
+ seed: undefined,
265
+ providerOptions: {},
266
+ aspectRatio: undefined,
267
+ });
268
+
269
+ expect(result.response.headers).toStrictEqual({
270
+ 'x-request-id': 'test-request-id',
271
+ 'content-type': 'application/json',
272
+ 'content-length': '128',
273
+ });
274
+ });
275
+ });
276
+ });
277
+
278
+ describe('constructor', () => {
279
+ it('should expose correct provider and model information', () => {
280
+ const model = createBasicModel();
281
+
282
+ expect(model.provider).toBe('togetherai');
283
+ expect(model.modelId).toBe('stabilityai/stable-diffusion-xl');
284
+ expect(model.specificationVersion).toBe('v3');
285
+ expect(model.maxImagesPerCall).toBe(1);
286
+ });
287
+ });
288
+
289
+ describe('Image Editing', () => {
290
+ const server = createTestServer({
291
+ 'https://api.example.com/*': {
292
+ response: {
293
+ type: 'json-value',
294
+ body: {
295
+ id: 'test-id',
296
+ data: [{ index: 0, b64_json: 'test-base64-content' }],
297
+ model: 'black-forest-labs/FLUX.1-kontext-pro',
298
+ object: 'list',
299
+ },
300
+ },
301
+ },
302
+ });
303
+
304
+ it('should send image_url when URL file is provided', async () => {
305
+ const model = createBasicModel();
306
+
307
+ await model.doGenerate({
308
+ prompt: 'Make the shirt yellow',
309
+ files: [
310
+ {
311
+ type: 'url',
312
+ url: 'https://example.com/input.jpg',
313
+ },
314
+ ],
315
+ mask: undefined,
316
+ n: 1,
317
+ size: undefined,
318
+ aspectRatio: undefined,
319
+ seed: undefined,
320
+ providerOptions: {},
321
+ });
322
+
323
+ const requestBody = await server.calls[0].requestBodyJson;
324
+ expect(requestBody).toMatchInlineSnapshot(`
325
+ {
326
+ "image_url": "https://example.com/input.jpg",
327
+ "model": "stabilityai/stable-diffusion-xl",
328
+ "prompt": "Make the shirt yellow",
329
+ "response_format": "base64",
330
+ }
331
+ `);
332
+ });
333
+
334
+ it('should convert Uint8Array file to data URI', async () => {
335
+ const model = createBasicModel();
336
+ const testImageData = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
337
+
338
+ await model.doGenerate({
339
+ prompt: 'Transform this image',
340
+ files: [
341
+ {
342
+ type: 'file',
343
+ data: testImageData,
344
+ mediaType: 'image/png',
345
+ },
346
+ ],
347
+ mask: undefined,
348
+ n: 1,
349
+ size: undefined,
350
+ aspectRatio: undefined,
351
+ seed: undefined,
352
+ providerOptions: {},
353
+ });
354
+
355
+ const requestBody = await server.calls[0].requestBodyJson;
356
+ expect(requestBody.image_url).toMatch(/^data:image\/png;base64,/);
357
+ expect(requestBody.prompt).toBe('Transform this image');
358
+ });
359
+
360
+ it('should convert file with base64 string data to data URI', async () => {
361
+ const model = createBasicModel();
362
+
363
+ await model.doGenerate({
364
+ prompt: 'Edit this',
365
+ files: [
366
+ {
367
+ type: 'file',
368
+ data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
369
+ mediaType: 'image/png',
370
+ },
371
+ ],
372
+ mask: undefined,
373
+ n: 1,
374
+ size: undefined,
375
+ aspectRatio: undefined,
376
+ seed: undefined,
377
+ providerOptions: {},
378
+ });
379
+
380
+ const requestBody = await server.calls[0].requestBodyJson;
381
+ expect(requestBody.image_url).toBe(
382
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
383
+ );
384
+ });
385
+
386
+ it('should throw error when mask is provided', async () => {
387
+ const model = createBasicModel();
388
+
389
+ await expect(
390
+ model.doGenerate({
391
+ prompt: 'Inpaint this area',
392
+ files: [
393
+ {
394
+ type: 'url',
395
+ url: 'https://example.com/input.jpg',
396
+ },
397
+ ],
398
+ mask: {
399
+ type: 'url',
400
+ url: 'https://example.com/mask.png',
401
+ },
402
+ n: 1,
403
+ size: undefined,
404
+ aspectRatio: undefined,
405
+ seed: undefined,
406
+ providerOptions: {},
407
+ }),
408
+ ).rejects.toThrow(
409
+ 'Together AI does not support mask-based image editing. ' +
410
+ 'Use FLUX Kontext models (e.g., black-forest-labs/FLUX.1-kontext-pro) ' +
411
+ 'with a reference image and descriptive prompt instead.',
412
+ );
413
+ });
414
+
415
+ it('should warn when multiple files are provided', async () => {
416
+ const model = createBasicModel();
417
+
418
+ const result = await model.doGenerate({
419
+ prompt: 'Edit multiple images',
420
+ files: [
421
+ {
422
+ type: 'url',
423
+ url: 'https://example.com/input1.jpg',
424
+ },
425
+ {
426
+ type: 'url',
427
+ url: 'https://example.com/input2.jpg',
428
+ },
429
+ ],
430
+ mask: undefined,
431
+ n: 1,
432
+ size: undefined,
433
+ aspectRatio: undefined,
434
+ seed: undefined,
435
+ providerOptions: {},
436
+ });
437
+
438
+ expect(result.warnings).toMatchInlineSnapshot(`
439
+ [
440
+ {
441
+ "message": "Together AI only supports a single input image. Additional images are ignored.",
442
+ "type": "other",
443
+ },
444
+ ]
445
+ `);
446
+
447
+ // Should only use the first image
448
+ const requestBody = await server.calls[0].requestBodyJson;
449
+ expect(requestBody.image_url).toBe('https://example.com/input1.jpg');
450
+ });
451
+
452
+ it('should pass provider options with image editing', async () => {
453
+ const model = createBasicModel();
454
+
455
+ await model.doGenerate({
456
+ prompt: 'Transform the style',
457
+ files: [
458
+ {
459
+ type: 'url',
460
+ url: 'https://example.com/input.jpg',
461
+ },
462
+ ],
463
+ mask: undefined,
464
+ n: 1,
465
+ size: undefined,
466
+ aspectRatio: undefined,
467
+ seed: undefined,
468
+ providerOptions: {
469
+ togetherai: {
470
+ steps: 28,
471
+ guidance: 3.5,
472
+ },
473
+ },
474
+ });
475
+
476
+ const requestBody = await server.calls[0].requestBodyJson;
477
+ expect(requestBody).toMatchInlineSnapshot(`
478
+ {
479
+ "guidance": 3.5,
480
+ "image_url": "https://example.com/input.jpg",
481
+ "model": "stabilityai/stable-diffusion-xl",
482
+ "prompt": "Transform the style",
483
+ "response_format": "base64",
484
+ "steps": 28,
485
+ }
486
+ `);
487
+ });
488
+ });