@ai-sdk/luma
Version:
The **Luma provider** for the [AI SDK](https://ai-sdk.dev/docs) contains support for Luma AI's state-of-the-art image generation models - Photon and Photon Flash.
225 lines (223 loc) • 6.9 kB
JavaScript
// src/luma-provider.ts
import { NoSuchModelError } from "@ai-sdk/provider";
import {
loadApiKey,
withoutTrailingSlash
} from "@ai-sdk/provider-utils";
// src/luma-image-model.ts
import {
InvalidResponseDataError
} from "@ai-sdk/provider";
import {
combineHeaders,
createBinaryResponseHandler,
createJsonResponseHandler,
createJsonErrorResponseHandler,
createStatusCodeErrorResponseHandler,
delay,
getFromApi,
postJsonToApi
} from "@ai-sdk/provider-utils";
import { z } from "zod/v4";
var DEFAULT_POLL_INTERVAL_MILLIS = 500;
var DEFAULT_MAX_POLL_ATTEMPTS = 6e4 / DEFAULT_POLL_INTERVAL_MILLIS;
var LumaImageModel = class {
constructor(modelId, config) {
this.modelId = modelId;
this.config = config;
this.specificationVersion = "v2";
this.maxImagesPerCall = 1;
this.pollIntervalMillis = DEFAULT_POLL_INTERVAL_MILLIS;
this.maxPollAttempts = DEFAULT_MAX_POLL_ATTEMPTS;
}
get provider() {
return this.config.provider;
}
async doGenerate({
prompt,
n,
size,
aspectRatio,
seed,
providerOptions,
headers,
abortSignal
}) {
var _a, _b, _c, _d;
const warnings = [];
if (seed != null) {
warnings.push({
type: "unsupported-setting",
setting: "seed",
details: "This model does not support the `seed` option."
});
}
if (size != null) {
warnings.push({
type: "unsupported-setting",
setting: "size",
details: "This model does not support the `size` option. Use `aspectRatio` instead."
});
}
const { pollIntervalMillis, maxPollAttempts, ...providerRequestOptions } = (_a = providerOptions.luma) != null ? _a : {};
const currentDate = (_d = (_c = (_b = this.config._internal) == null ? void 0 : _b.currentDate) == null ? void 0 : _c.call(_b)) != null ? _d : /* @__PURE__ */ new Date();
const fullHeaders = combineHeaders(this.config.headers(), headers);
const { value: generationResponse, responseHeaders } = await postJsonToApi({
url: this.getLumaGenerationsUrl(),
headers: fullHeaders,
body: {
prompt,
...aspectRatio ? { aspect_ratio: aspectRatio } : {},
model: this.modelId,
...providerRequestOptions
},
abortSignal,
fetch: this.config.fetch,
failedResponseHandler: this.createLumaErrorHandler(),
successfulResponseHandler: createJsonResponseHandler(
lumaGenerationResponseSchema
)
});
const imageUrl = await this.pollForImageUrl(
generationResponse.id,
fullHeaders,
abortSignal,
providerOptions.luma
);
const downloadedImage = await this.downloadImage(imageUrl, abortSignal);
return {
images: [downloadedImage],
warnings,
response: {
modelId: this.modelId,
timestamp: currentDate,
headers: responseHeaders
}
};
}
async pollForImageUrl(generationId, headers, abortSignal, imageSettings) {
var _a, _b, _c;
const url = this.getLumaGenerationsUrl(generationId);
const maxPollAttempts = (_a = imageSettings == null ? void 0 : imageSettings.maxPollAttempts) != null ? _a : this.maxPollAttempts;
const pollIntervalMillis = (_b = imageSettings == null ? void 0 : imageSettings.pollIntervalMillis) != null ? _b : this.pollIntervalMillis;
for (let i = 0; i < maxPollAttempts; i++) {
const { value: statusResponse } = await getFromApi({
url,
headers,
abortSignal,
fetch: this.config.fetch,
failedResponseHandler: this.createLumaErrorHandler(),
successfulResponseHandler: createJsonResponseHandler(
lumaGenerationResponseSchema
)
});
switch (statusResponse.state) {
case "completed":
if (!((_c = statusResponse.assets) == null ? void 0 : _c.image)) {
throw new InvalidResponseDataError({
data: statusResponse,
message: `Image generation completed but no image was found.`
});
}
return statusResponse.assets.image;
case "failed":
throw new InvalidResponseDataError({
data: statusResponse,
message: `Image generation failed.`
});
}
await delay(pollIntervalMillis);
}
throw new Error(
`Image generation timed out after ${this.maxPollAttempts} attempts.`
);
}
createLumaErrorHandler() {
return createJsonErrorResponseHandler({
errorSchema: lumaErrorSchema,
errorToMessage: (error) => {
var _a;
return (_a = error.detail[0].msg) != null ? _a : "Unknown error";
}
});
}
getLumaGenerationsUrl(generationId) {
return `${this.config.baseURL}/dream-machine/v1/generations/${generationId != null ? generationId : "image"}`;
}
async downloadImage(url, abortSignal) {
const { value: response } = await getFromApi({
url,
// No specific headers should be needed for this request as it's a
// generated image provided by Luma.
abortSignal,
failedResponseHandler: createStatusCodeErrorResponseHandler(),
successfulResponseHandler: createBinaryResponseHandler(),
fetch: this.config.fetch
});
return response;
}
};
var lumaGenerationResponseSchema = z.object({
id: z.string(),
state: z.enum(["queued", "dreaming", "completed", "failed"]),
failure_reason: z.string().nullish(),
assets: z.object({
image: z.string()
// URL of the generated image
}).nullish()
});
var lumaErrorSchema = z.object({
detail: z.array(
z.object({
type: z.string(),
loc: z.array(z.string()),
msg: z.string(),
input: z.string(),
ctx: z.object({
expected: z.string()
}).nullish()
})
)
});
// src/luma-provider.ts
var defaultBaseURL = "https://api.lumalabs.ai";
function createLuma(options = {}) {
var _a;
const baseURL = withoutTrailingSlash((_a = options.baseURL) != null ? _a : defaultBaseURL);
const getHeaders = () => ({
Authorization: `Bearer ${loadApiKey({
apiKey: options.apiKey,
environmentVariableName: "LUMA_API_KEY",
description: "Luma"
})}`,
...options.headers
});
const createImageModel = (modelId) => new LumaImageModel(modelId, {
provider: "luma.image",
baseURL: baseURL != null ? baseURL : defaultBaseURL,
headers: getHeaders,
fetch: options.fetch
});
return {
image: createImageModel,
imageModel: createImageModel,
languageModel: () => {
throw new NoSuchModelError({
modelId: "languageModel",
modelType: "languageModel"
});
},
textEmbeddingModel: () => {
throw new NoSuchModelError({
modelId: "textEmbeddingModel",
modelType: "textEmbeddingModel"
});
}
};
}
var luma = createLuma();
export {
createLuma,
luma
};
//# sourceMappingURL=index.mjs.map