UNPKG

@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 (190 loc) 8.71 kB
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;