@ai-sdk/luma 2.0.8 → 2.0.10

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,441 @@
1
+ import {
2
+ ImageModelV3,
3
+ ImageModelV3File,
4
+ SharedV3Warning,
5
+ InvalidResponseDataError,
6
+ } from '@ai-sdk/provider';
7
+ import {
8
+ FetchFunction,
9
+ combineHeaders,
10
+ createBinaryResponseHandler,
11
+ createJsonResponseHandler,
12
+ createJsonErrorResponseHandler,
13
+ createStatusCodeErrorResponseHandler,
14
+ delay,
15
+ getFromApi,
16
+ postJsonToApi,
17
+ InferSchema,
18
+ lazySchema,
19
+ parseProviderOptions,
20
+ zodSchema,
21
+ } from '@ai-sdk/provider-utils';
22
+ import { LumaImageSettings, LumaReferenceType } from './luma-image-settings';
23
+ import { z } from 'zod/v4';
24
+
25
+ const DEFAULT_POLL_INTERVAL_MILLIS = 500;
26
+ const DEFAULT_MAX_POLL_ATTEMPTS = 60000 / DEFAULT_POLL_INTERVAL_MILLIS;
27
+
28
+ interface LumaImageModelConfig {
29
+ provider: string;
30
+ baseURL: string;
31
+ headers: () => Record<string, string>;
32
+ fetch?: FetchFunction;
33
+ _internal?: {
34
+ currentDate?: () => Date;
35
+ };
36
+ }
37
+
38
+ export class LumaImageModel implements ImageModelV3 {
39
+ readonly specificationVersion = 'v3';
40
+ readonly maxImagesPerCall = 1;
41
+ readonly pollIntervalMillis = DEFAULT_POLL_INTERVAL_MILLIS;
42
+ readonly maxPollAttempts = DEFAULT_MAX_POLL_ATTEMPTS;
43
+
44
+ get provider(): string {
45
+ return this.config.provider;
46
+ }
47
+
48
+ constructor(
49
+ readonly modelId: string,
50
+ private readonly config: LumaImageModelConfig,
51
+ ) {}
52
+
53
+ async doGenerate({
54
+ prompt,
55
+ n,
56
+ size,
57
+ aspectRatio,
58
+ seed,
59
+ providerOptions,
60
+ headers,
61
+ abortSignal,
62
+ files,
63
+ mask,
64
+ }: Parameters<ImageModelV3['doGenerate']>[0]): Promise<
65
+ Awaited<ReturnType<ImageModelV3['doGenerate']>>
66
+ > {
67
+ const warnings: Array<SharedV3Warning> = [];
68
+
69
+ if (seed != null) {
70
+ warnings.push({
71
+ type: 'unsupported',
72
+ feature: 'seed',
73
+ details: 'This model does not support the `seed` option.',
74
+ });
75
+ }
76
+
77
+ if (size != null) {
78
+ warnings.push({
79
+ type: 'unsupported',
80
+ feature: 'size',
81
+ details:
82
+ 'This model does not support the `size` option. Use `aspectRatio` instead.',
83
+ });
84
+ }
85
+
86
+ // Parse and validate provider options
87
+ const lumaOptions = await parseProviderOptions({
88
+ provider: 'luma',
89
+ providerOptions,
90
+ schema: lumaImageProviderOptionsSchema,
91
+ });
92
+
93
+ // Extract non-request options
94
+ const {
95
+ pollIntervalMillis,
96
+ maxPollAttempts,
97
+ referenceType,
98
+ images: imageConfigs,
99
+ ...providerRequestOptions
100
+ } = lumaOptions ?? {};
101
+
102
+ // Handle image editing via files with reference type support
103
+ const editingOptions = this.getEditingOptions(
104
+ files,
105
+ mask,
106
+ referenceType ?? undefined,
107
+ imageConfigs ?? [],
108
+ );
109
+
110
+ const currentDate = this.config._internal?.currentDate?.() ?? new Date();
111
+ const fullHeaders = combineHeaders(this.config.headers(), headers);
112
+ const { value: generationResponse, responseHeaders } = await postJsonToApi({
113
+ url: this.getLumaGenerationsUrl(),
114
+ headers: fullHeaders,
115
+ body: {
116
+ prompt,
117
+ ...(aspectRatio ? { aspect_ratio: aspectRatio } : {}),
118
+ model: this.modelId,
119
+ ...editingOptions,
120
+ ...providerRequestOptions,
121
+ },
122
+ abortSignal,
123
+ fetch: this.config.fetch,
124
+ failedResponseHandler: this.createLumaErrorHandler(),
125
+ successfulResponseHandler: createJsonResponseHandler(
126
+ lumaGenerationResponseSchema,
127
+ ),
128
+ });
129
+
130
+ const imageUrl = await this.pollForImageUrl(
131
+ generationResponse.id,
132
+ fullHeaders,
133
+ abortSignal,
134
+ {
135
+ pollIntervalMillis: pollIntervalMillis ?? undefined,
136
+ maxPollAttempts: maxPollAttempts ?? undefined,
137
+ },
138
+ );
139
+
140
+ const downloadedImage = await this.downloadImage(imageUrl, abortSignal);
141
+
142
+ return {
143
+ images: [downloadedImage],
144
+ warnings,
145
+ response: {
146
+ modelId: this.modelId,
147
+ timestamp: currentDate,
148
+ headers: responseHeaders,
149
+ },
150
+ };
151
+ }
152
+
153
+ private async pollForImageUrl(
154
+ generationId: string,
155
+ headers: Record<string, string | undefined>,
156
+ abortSignal: AbortSignal | undefined,
157
+ pollSettings?: { pollIntervalMillis?: number; maxPollAttempts?: number },
158
+ ): Promise<string> {
159
+ const url = this.getLumaGenerationsUrl(generationId);
160
+ const maxPollAttempts =
161
+ pollSettings?.maxPollAttempts ?? this.maxPollAttempts;
162
+ const pollIntervalMillis =
163
+ pollSettings?.pollIntervalMillis ?? this.pollIntervalMillis;
164
+
165
+ for (let i = 0; i < maxPollAttempts; i++) {
166
+ const { value: statusResponse } = await getFromApi({
167
+ url,
168
+ headers,
169
+ abortSignal,
170
+ fetch: this.config.fetch,
171
+ failedResponseHandler: this.createLumaErrorHandler(),
172
+ successfulResponseHandler: createJsonResponseHandler(
173
+ lumaGenerationResponseSchema,
174
+ ),
175
+ });
176
+
177
+ switch (statusResponse.state) {
178
+ case 'completed':
179
+ if (!statusResponse.assets?.image) {
180
+ throw new InvalidResponseDataError({
181
+ data: statusResponse,
182
+ message: `Image generation completed but no image was found.`,
183
+ });
184
+ }
185
+ return statusResponse.assets.image;
186
+ case 'failed':
187
+ throw new InvalidResponseDataError({
188
+ data: statusResponse,
189
+ message: `Image generation failed.`,
190
+ });
191
+ }
192
+ await delay(pollIntervalMillis);
193
+ }
194
+
195
+ throw new Error(
196
+ `Image generation timed out after ${this.maxPollAttempts} attempts.`,
197
+ );
198
+ }
199
+
200
+ private createLumaErrorHandler() {
201
+ return createJsonErrorResponseHandler({
202
+ errorSchema: lumaErrorSchema,
203
+ errorToMessage: (error: LumaErrorData) =>
204
+ error.detail[0].msg ?? 'Unknown error',
205
+ });
206
+ }
207
+
208
+ private getEditingOptions(
209
+ files: ImageModelV3File[] | undefined,
210
+ mask: ImageModelV3File | undefined,
211
+ referenceType: LumaReferenceType = 'image',
212
+ imageConfigs: Array<{ weight?: number | null; id?: string | null }> = [],
213
+ ): Record<string, unknown> {
214
+ const options: Record<string, unknown> = {};
215
+
216
+ // Luma does not support mask-based inpainting
217
+ if (mask != null) {
218
+ throw new Error(
219
+ 'Luma AI does not support mask-based image editing. ' +
220
+ 'Use the prompt to describe the changes you want to make, along with ' +
221
+ '`prompt.images` containing the source image URL.',
222
+ );
223
+ }
224
+
225
+ if (files == null || files.length === 0) {
226
+ return options;
227
+ }
228
+
229
+ // Validate all files are URL-based
230
+ for (const file of files) {
231
+ if (file.type !== 'url') {
232
+ throw new Error(
233
+ 'Luma AI only supports URL-based images. ' +
234
+ 'Please provide image URLs using `prompt.images` with publicly accessible URLs. ' +
235
+ 'Base64 and Uint8Array data are not supported.',
236
+ );
237
+ }
238
+ }
239
+
240
+ // Default weights per reference type
241
+ const defaultWeights: Record<LumaReferenceType, number> = {
242
+ image: 0.85,
243
+ style: 0.8,
244
+ character: 1.0, // Not used, but defined for completeness
245
+ modify_image: 1.0,
246
+ };
247
+
248
+ switch (referenceType) {
249
+ case 'image': {
250
+ // Supports up to 4 images
251
+ if (files.length > 4) {
252
+ throw new Error(
253
+ 'Luma AI image supports up to 4 reference images. ' +
254
+ `You provided ${files.length} images.`,
255
+ );
256
+ }
257
+ options.image = files.map((file, index) => ({
258
+ url: (file as { type: 'url'; url: string }).url,
259
+ weight: imageConfigs[index]?.weight ?? defaultWeights.image,
260
+ }));
261
+ break;
262
+ }
263
+
264
+ case 'style': {
265
+ // Style ref accepts an array but typically uses one style image
266
+ options.style = files.map((file, index) => ({
267
+ url: (file as { type: 'url'; url: string }).url,
268
+ weight: imageConfigs[index]?.weight ?? defaultWeights.style,
269
+ }));
270
+ break;
271
+ }
272
+
273
+ case 'character': {
274
+ // Group images by identity id
275
+ const identities: Record<string, string[]> = {};
276
+ for (let i = 0; i < files.length; i++) {
277
+ const file = files[i] as { type: 'url'; url: string };
278
+ const identityId = imageConfigs[i]?.id ?? 'identity0';
279
+ if (!identities[identityId]) {
280
+ identities[identityId] = [];
281
+ }
282
+ identities[identityId].push(file.url);
283
+ }
284
+
285
+ // Validate each identity has at most 4 images
286
+ for (const [identityId, images] of Object.entries(identities)) {
287
+ if (images.length > 4) {
288
+ throw new Error(
289
+ `Luma AI character supports up to 4 images per identity. ` +
290
+ `Identity '${identityId}' has ${images.length} images.`,
291
+ );
292
+ }
293
+ }
294
+
295
+ options.character = Object.fromEntries(
296
+ Object.entries(identities).map(([id, images]) => [id, { images }]),
297
+ );
298
+ break;
299
+ }
300
+
301
+ case 'modify_image': {
302
+ // Only supports a single image
303
+ if (files.length > 1) {
304
+ throw new Error(
305
+ 'Luma AI modify_image only supports a single input image. ' +
306
+ `You provided ${files.length} images.`,
307
+ );
308
+ }
309
+ options.modify_image = {
310
+ url: (files[0] as { type: 'url'; url: string }).url,
311
+ weight: imageConfigs[0]?.weight ?? defaultWeights.modify_image,
312
+ };
313
+ break;
314
+ }
315
+ }
316
+
317
+ return options;
318
+ }
319
+
320
+ private getLumaGenerationsUrl(generationId?: string) {
321
+ return `${this.config.baseURL}/dream-machine/v1/generations/${
322
+ generationId ?? 'image'
323
+ }`;
324
+ }
325
+
326
+ private async downloadImage(
327
+ url: string,
328
+ abortSignal: AbortSignal | undefined,
329
+ ): Promise<Uint8Array> {
330
+ const { value: response } = await getFromApi({
331
+ url,
332
+ // No specific headers should be needed for this request as it's a
333
+ // generated image provided by Luma.
334
+ abortSignal,
335
+ failedResponseHandler: createStatusCodeErrorResponseHandler(),
336
+ successfulResponseHandler: createBinaryResponseHandler(),
337
+ fetch: this.config.fetch,
338
+ });
339
+ return response;
340
+ }
341
+ }
342
+
343
+ // limited version of the schema, focussed on what is needed for the implementation
344
+ // this approach limits breakages when the API changes and increases efficiency
345
+ const lumaGenerationResponseSchema = lazySchema(() =>
346
+ zodSchema(
347
+ z.object({
348
+ id: z.string(),
349
+ state: z.enum(['queued', 'dreaming', 'completed', 'failed']),
350
+ failure_reason: z.string().nullish(),
351
+ assets: z
352
+ .object({
353
+ image: z.string(), // URL of the generated image
354
+ })
355
+ .nullish(),
356
+ }),
357
+ ),
358
+ );
359
+
360
+ const lumaErrorSchema = z.object({
361
+ detail: z.array(
362
+ z.object({
363
+ type: z.string(),
364
+ loc: z.array(z.string()),
365
+ msg: z.string(),
366
+ input: z.string(),
367
+ ctx: z
368
+ .object({
369
+ expected: z.string(),
370
+ })
371
+ .nullish(),
372
+ }),
373
+ ),
374
+ });
375
+
376
+ export type LumaErrorData = z.infer<typeof lumaErrorSchema>;
377
+
378
+ /**
379
+ * Provider options schema for Luma image generation.
380
+ *
381
+ * @see https://docs.lumalabs.ai/docs/image-generation
382
+ */
383
+ export const lumaImageProviderOptionsSchema = lazySchema(() =>
384
+ zodSchema(
385
+ z
386
+ .object({
387
+ /**
388
+ * The type of image reference to use when providing input images.
389
+ * - `image`: Guide generation using reference images (up to 4). Default.
390
+ * - `style`: Apply a specific style from reference image(s).
391
+ * - `character`: Create consistent characters from reference images (up to 4).
392
+ * - `modify_image`: Transform a single input image with prompt guidance.
393
+ */
394
+ referenceType: z
395
+ .enum(['image', 'style', 'character', 'modify_image'])
396
+ .nullish(),
397
+
398
+ /**
399
+ * Per-image configuration array. Each entry corresponds to an image in `prompt.images`.
400
+ * Allows setting individual weights for each reference image.
401
+ */
402
+ images: z
403
+ .array(
404
+ z.object({
405
+ /**
406
+ * The weight of this image's influence on the generation.
407
+ * - For `image`: Higher weight = closer to reference (default: 0.85)
408
+ * - For `style`: Higher weight = stronger style influence (default: 0.8)
409
+ * - For `modify_image`: Higher weight = closer to input, lower = more creative (default: 1.0)
410
+ */
411
+ weight: z.number().min(0).max(1).nullish(),
412
+
413
+ /**
414
+ * The identity name for character references.
415
+ * Used with `character` to specify which identity group the image belongs to.
416
+ * Luma supports multiple identities (e.g., 'identity0', 'identity1') for generating
417
+ * images with multiple consistent characters.
418
+ * Default: 'identity0'
419
+ */
420
+ id: z.string().nullish(),
421
+ }),
422
+ )
423
+ .nullish(),
424
+
425
+ /**
426
+ * Override the polling interval in milliseconds (default 500).
427
+ */
428
+ pollIntervalMillis: z.number().nullish(),
429
+
430
+ /**
431
+ * Override the maximum number of polling attempts (default 120).
432
+ */
433
+ maxPollAttempts: z.number().nullish(),
434
+ })
435
+ .passthrough(),
436
+ ),
437
+ );
438
+
439
+ export type LumaImageProviderOptions = InferSchema<
440
+ typeof lumaImageProviderOptionsSchema
441
+ >;
@@ -0,0 +1,96 @@
1
+ // https://luma.ai/models?type=image
2
+ export type LumaImageModelId = 'photon-1' | 'photon-flash-1' | (string & {});
3
+
4
+ /**
5
+ * The type of image reference to use when providing input images.
6
+ *
7
+ * - `image`: Guide generation using reference images (up to 4). Default.
8
+ * - `style`: Apply a specific style from reference image(s).
9
+ * - `character`: Create consistent characters from reference images (up to 4).
10
+ * - `modify_image`: Transform a single input image with prompt guidance.
11
+ */
12
+ export type LumaReferenceType =
13
+ | 'image'
14
+ | 'style'
15
+ | 'character'
16
+ | 'modify_image';
17
+
18
+ /**
19
+ * Per-image configuration for Luma image references.
20
+ */
21
+ export interface LumaImageConfig {
22
+ /**
23
+ * The weight of this image's influence on the generation.
24
+ *
25
+ * - For `image`: Higher weight = closer to reference (default: 0.85)
26
+ * - For `style`: Higher weight = stronger style influence (default: 0.8)
27
+ * - For `modify_image`: Higher weight = closer to input, lower = more creative (default: 1.0)
28
+ *
29
+ * Note: Not applicable to `character`.
30
+ */
31
+ weight?: number;
32
+
33
+ /**
34
+ * The identity name for character references.
35
+ *
36
+ * Used with `character` to specify which identity group the image belongs to.
37
+ * Luma supports multiple identities (e.g., 'identity0', 'identity1') for generating
38
+ * images with multiple consistent characters.
39
+ *
40
+ * Default: 'identity0'
41
+ */
42
+ id?: string;
43
+ }
44
+
45
+ /**
46
+ * Configuration settings for Luma image generation.
47
+ *
48
+ * Since the Luma API processes images through an asynchronous queue system, these
49
+ * settings allow you to tune the polling behavior when waiting for image
50
+ * generation to complete.
51
+ */
52
+ export interface LumaImageSettings {
53
+ /**
54
+ * Override the polling interval in milliseconds (default 500). This controls how
55
+ * frequently the API is checked for completed images while they are being
56
+ * processed in Luma's queue.
57
+ */
58
+ pollIntervalMillis?: number;
59
+
60
+ /**
61
+ * Override the maximum number of polling attempts (default 120). Since image
62
+ * generation is queued and processed asynchronously, this limits how long to wait
63
+ * for results before timing out.
64
+ */
65
+ maxPollAttempts?: number;
66
+
67
+ /**
68
+ * The type of image reference to use when providing input images via `prompt.images`.
69
+ * Default is `image`.
70
+ *
71
+ * - `image`: Guide generation using reference images (up to 4)
72
+ * - `style`: Apply a specific style from reference image(s)
73
+ * - `character`: Create consistent characters from reference images (up to 4)
74
+ * - `modify_image`: Transform a single input image with prompt guidance
75
+ */
76
+ referenceType?: LumaReferenceType;
77
+
78
+ /**
79
+ * Per-image configuration array. Each entry corresponds to an image in `prompt.images`.
80
+ * Allows setting individual weights for each reference image.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * providerOptions: {
85
+ * luma: {
86
+ * referenceType: 'image',
87
+ * images: [
88
+ * { weight: 0.9 },
89
+ * { weight: 0.5 },
90
+ * ],
91
+ * },
92
+ * }
93
+ * ```
94
+ */
95
+ images?: LumaImageConfig[];
96
+ }
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { createLuma } from './luma-provider';
3
+ import { LumaImageModel } from './luma-image-model';
4
+
5
+ vi.mock('./luma-image-model', () => ({
6
+ LumaImageModel: vi.fn(),
7
+ }));
8
+
9
+ describe('createLuma', () => {
10
+ beforeEach(() => {
11
+ vi.clearAllMocks();
12
+ });
13
+
14
+ describe('image', () => {
15
+ it('should construct an image model with default configuration', () => {
16
+ const provider = createLuma();
17
+ const modelId = 'luma-v1';
18
+
19
+ const model = provider.image(modelId);
20
+
21
+ expect(model).toBeInstanceOf(LumaImageModel);
22
+ expect(LumaImageModel).toHaveBeenCalledWith(
23
+ modelId,
24
+ expect.objectContaining({
25
+ provider: 'luma.image',
26
+ baseURL: 'https://api.lumalabs.ai',
27
+ }),
28
+ );
29
+ });
30
+
31
+ it('should respect custom configuration options', () => {
32
+ const customBaseURL = 'https://custom-api.lumalabs.ai';
33
+ const customHeaders = { 'X-Custom-Header': 'value' };
34
+ const mockFetch = vi.fn();
35
+
36
+ const provider = createLuma({
37
+ apiKey: 'custom-api-key',
38
+ baseURL: customBaseURL,
39
+ headers: customHeaders,
40
+ fetch: mockFetch,
41
+ });
42
+ const modelId = 'luma-v1';
43
+
44
+ provider.image(modelId);
45
+
46
+ expect(LumaImageModel).toHaveBeenCalledWith(
47
+ modelId,
48
+ expect.objectContaining({
49
+ baseURL: customBaseURL,
50
+ headers: expect.any(Function),
51
+ fetch: mockFetch,
52
+ provider: 'luma.image',
53
+ }),
54
+ );
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,97 @@
1
+ import { ImageModelV3, NoSuchModelError, ProviderV3 } from '@ai-sdk/provider';
2
+ import {
3
+ FetchFunction,
4
+ loadApiKey,
5
+ withoutTrailingSlash,
6
+ withUserAgentSuffix,
7
+ } from '@ai-sdk/provider-utils';
8
+ import { LumaImageModel } from './luma-image-model';
9
+ import { LumaImageModelId } from './luma-image-settings';
10
+ import { VERSION } from './version';
11
+
12
+ export interface LumaProviderSettings {
13
+ /**
14
+ Luma API key. Default value is taken from the `LUMA_API_KEY` environment
15
+ variable.
16
+ */
17
+ apiKey?: string;
18
+ /**
19
+ Base URL for the API calls.
20
+ */
21
+ baseURL?: string;
22
+ /**
23
+ Custom headers to include in the requests.
24
+ */
25
+ headers?: Record<string, string>;
26
+ /**
27
+ Custom fetch implementation. You can use it as a middleware to intercept requests,
28
+ or to provide a custom fetch implementation for e.g. testing.
29
+ */
30
+ fetch?: FetchFunction;
31
+ }
32
+
33
+ export interface LumaProvider extends ProviderV3 {
34
+ /**
35
+ Creates a model for image generation.
36
+ */
37
+ image(modelId: LumaImageModelId): ImageModelV3;
38
+
39
+ /**
40
+ Creates a model for image generation.
41
+ */
42
+ imageModel(modelId: LumaImageModelId): ImageModelV3;
43
+
44
+ /**
45
+ * @deprecated Use `embeddingModel` instead.
46
+ */
47
+ textEmbeddingModel(modelId: string): never;
48
+ }
49
+
50
+ const defaultBaseURL = 'https://api.lumalabs.ai';
51
+
52
+ export function createLuma(options: LumaProviderSettings = {}): LumaProvider {
53
+ const baseURL = withoutTrailingSlash(options.baseURL ?? defaultBaseURL);
54
+ const getHeaders = () =>
55
+ withUserAgentSuffix(
56
+ {
57
+ Authorization: `Bearer ${loadApiKey({
58
+ apiKey: options.apiKey,
59
+ environmentVariableName: 'LUMA_API_KEY',
60
+ description: 'Luma',
61
+ })}`,
62
+ ...options.headers,
63
+ },
64
+ `ai-sdk/luma/${VERSION}`,
65
+ );
66
+
67
+ const createImageModel = (modelId: LumaImageModelId) =>
68
+ new LumaImageModel(modelId, {
69
+ provider: 'luma.image',
70
+ baseURL: baseURL ?? defaultBaseURL,
71
+ headers: getHeaders,
72
+ fetch: options.fetch,
73
+ });
74
+
75
+ const embeddingModel = (modelId: string) => {
76
+ throw new NoSuchModelError({
77
+ modelId,
78
+ modelType: 'embeddingModel',
79
+ });
80
+ };
81
+
82
+ return {
83
+ specificationVersion: 'v3' as const,
84
+ image: createImageModel,
85
+ imageModel: createImageModel,
86
+ languageModel: (modelId: string) => {
87
+ throw new NoSuchModelError({
88
+ modelId,
89
+ modelType: 'languageModel',
90
+ });
91
+ },
92
+ embeddingModel,
93
+ textEmbeddingModel: embeddingModel,
94
+ };
95
+ }
96
+
97
+ export const luma = createLuma();
package/src/version.ts ADDED
@@ -0,0 +1,6 @@
1
+ // Version string of this package injected at build time.
2
+ declare const __PACKAGE_VERSION__: string | undefined;
3
+ export const VERSION: string =
4
+ typeof __PACKAGE_VERSION__ !== 'undefined'
5
+ ? __PACKAGE_VERSION__
6
+ : '0.0.0-test';