@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
191 lines • 8.75 kB
JavaScript
import { StabilityModels } from "../constants/enums.js";
import { BaseProvider } from "../core/baseProvider.js";
import { isNeuroLink } from "../neurolink.js";
import { createProxyFetch } from "../proxy/proxyFetch.js";
import { AuthenticationError, InvalidModelError, ProviderError, RateLimitError, } from "../types/index.js";
import { logger } from "../utils/logger.js";
import { getProviderModel } from "../utils/providerConfig.js";
const STABILITY_DEFAULT_BASE_URL = "https://api.stability.ai";
const REQUEST_TIMEOUT_MS = 120_000;
/**
* Returns the Stability AI API key from env vars, or `undefined` when none
* is set. The constructor stores `undefined` instead of throwing so that
* callers who only supply per-call credentials via `options.credentials` can
* still instantiate the provider. Validation is deferred to the actual
* call site inside `executeImageGeneration`.
*/
const getStabilityApiKeyOptional = () => (process.env.STABILITY_API_KEY ??
process.env.STABILITY_AI_API_KEY ??
"").trim() || undefined;
const getDefaultStabilityModel = () => getProviderModel("STABILITY_MODEL", StabilityModels.STABLE_IMAGE_ULTRA);
/**
* Stability AI Provider — direct image generation.
*
* Hits api.stability.ai/v2beta/stable-image/generate/{model}. Returns
* base64 PNG. No chat / streaming / tool calling.
*
* The constructor no longer throws when `STABILITY_API_KEY` is absent so
* that per-call credentials (passed via `options.credentials.stability`) can
* be used without requiring a global env var at startup.
*
* @see https://platform.stability.ai/docs/api-reference
*/
export class StabilityProvider extends BaseProvider {
apiKey;
baseURL;
proxyFetch;
constructor(modelName, sdk, _region, credentials) {
const validatedNeurolink = isNeuroLink(sdk) ? sdk : undefined;
super(modelName, "stability", validatedNeurolink);
const overrideKey = credentials?.apiKey?.trim();
this.apiKey =
overrideKey && overrideKey.length > 0
? overrideKey
: getStabilityApiKeyOptional();
this.baseURL =
credentials?.baseURL ??
process.env.STABILITY_BASE_URL ??
STABILITY_DEFAULT_BASE_URL;
this.proxyFetch = createProxyFetch();
logger.debug("Stability AI Provider initialized (image-gen only)", {
modelName: this.modelName,
baseURL: this.baseURL,
});
}
getProviderName() {
return this.providerName;
}
getDefaultModel() {
return getDefaultStabilityModel();
}
supportsTools() {
return false;
}
getAISDKModel() {
throw new Error("Stability AI is an image-generation-only provider; chat completions are not available.");
}
async executeStream(_options, _analysisSchema) {
throw new Error("Stability AI is an image-generation-only provider; streaming chat is not available. Use generate({output:{format:'binary'}}) with a Stable Image / SD 3.5 model.");
}
formatProviderError(error) {
const message = error instanceof Error
? error.message
: typeof error === "string"
? error
: "Unknown error";
if (message.includes("401") ||
message.toLowerCase().includes("unauthorized")) {
return new AuthenticationError("Invalid Stability AI API key. Get one at https://platform.stability.ai/account/keys", "stability");
}
if (message.includes("429") ||
message.toLowerCase().includes("rate limit")) {
return new RateLimitError("Stability AI rate limit exceeded. Back off and retry.", "stability");
}
if (message.includes("content_filtered") ||
message.includes("CONTENT_FILTERED")) {
return new ProviderError("Stability AI declined the request due to content policy. Adjust the prompt and retry.", "stability");
}
if (message.includes("404")) {
return new InvalidModelError(`Stability AI model '${this.modelName}' not found. Use stable-image-ultra, stable-image-core, sd3.5-large, sd3.5-large-turbo, or sd3.5-medium.`, "stability");
}
return new ProviderError(`Stability AI error: ${message}`, "stability");
}
async executeImageGeneration(options) {
const startTime = Date.now();
// Resolve per-call credentials first, then fall back to instance-level.
const perCallCreds = options.credentials?.stability;
const resolvedApiKey = perCallCreds?.apiKey?.trim() || this.apiKey;
if (!resolvedApiKey) {
throw new Error("Stability AI API key is required. Set STABILITY_API_KEY or pass credentials.stability.apiKey per-call.");
}
const effectiveApiKey = resolvedApiKey;
const effectiveBaseURL = perCallCreds?.baseURL || this.baseURL;
const prompt = options.prompt ?? options.input?.text ?? "";
if (!prompt.trim()) {
throw new Error("Stability AI image generation requires a prompt (input.text or prompt)");
}
// Stability's URL slugs are `ultra` / `core` / `sd3` — not the user-facing
// `stable-image-ultra` / `stable-image-core` model identifiers we expose
// via StabilityModels. Map both shapes onto the actual path here.
const modelPath = this.modelName.startsWith("sd3.5-")
? "sd3"
: this.modelName === "stable-image-ultra"
? "ultra"
: this.modelName === "stable-image-core"
? "core"
: this.modelName;
// Image-gen extras live in `output` / image-specific fields. Cast
// to a permissive shape so we can read aspectRatio / negativePrompt
// / seed without polluting TextGenerationOptions.
const extras = options;
const form = new FormData();
form.append("prompt", prompt);
form.append("output_format", "png");
if (extras.aspectRatio) {
form.append("aspect_ratio", String(extras.aspectRatio));
}
if (extras.negativePrompt) {
form.append("negative_prompt", extras.negativePrompt);
}
if (this.modelName.startsWith("sd3.5-")) {
form.append("model", this.modelName);
}
if (extras.seed !== undefined) {
form.append("seed", String(extras.seed));
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
let response;
try {
response = await this.proxyFetch(`${effectiveBaseURL}/v2beta/stable-image/generate/${modelPath}`, {
method: "POST",
headers: {
Authorization: `Bearer ${effectiveApiKey}`,
Accept: "application/json", // base64 in body
},
body: form,
signal: controller.signal,
});
}
catch (err) {
if (err instanceof Error && err.name === "AbortError") {
throw this.formatProviderError(new Error(`Stability image-gen request timed out after ${REQUEST_TIMEOUT_MS / 1000}s`));
}
throw this.formatProviderError(err);
}
finally {
clearTimeout(timeoutId);
}
if (!response.ok) {
const text = await response.text();
throw this.formatProviderError(new Error(`Stability image-gen failed: ${response.status} — ${text}`));
}
const data = (await response.json());
if (!data.image) {
throw new Error(`Stability AI returned no image (finish_reason: ${data.finish_reason ?? "unknown"})`);
}
const generationTimeMs = Date.now() - startTime;
logger.info(`[StabilityProvider] Generated image (${data.image.length} base64 chars) in ${generationTimeMs}ms — model ${this.modelName}`);
return {
content: prompt,
provider: this.providerName,
model: this.modelName,
// output: 1000 = sentinel for per-image pricing (see pricing.ts)
usage: { input: 0, output: 1000, total: 1000 },
imageOutput: { base64: data.image },
};
}
async validateConfiguration() {
return this.apiKey !== undefined && this.apiKey.trim().length > 0;
}
getConfiguration() {
return {
provider: this.providerName,
model: this.modelName,
defaultModel: getDefaultStabilityModel(),
baseURL: this.baseURL,
};
}
}
export default StabilityProvider;
//# sourceMappingURL=stability.js.map