@happyvertical/ai 0.86.4 → 0.87.0
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.
- package/README.md +60 -2
- package/dist/chunks/{anthropic-DuYUnw8j.js → anthropic-ByuqC5mC.js} +4 -4
- package/dist/chunks/{anthropic-DuYUnw8j.js.map → anthropic-ByuqC5mC.js.map} +1 -1
- package/dist/chunks/{bedrock-ikQJAwWB.js → bedrock-BeVCmkrS.js} +3 -3
- package/dist/chunks/{bedrock-ikQJAwWB.js.map → bedrock-BeVCmkrS.js.map} +1 -1
- package/dist/chunks/{bifrost-CaYhelVL.js → bifrost-DvYZ3LNf.js} +4 -4
- package/dist/chunks/{bifrost-CaYhelVL.js.map → bifrost-DvYZ3LNf.js.map} +1 -1
- package/dist/chunks/{byteplus-modelark-B8l8sPXu.js → byteplus-modelark-DaF0U4n1.js} +4 -4
- package/dist/chunks/{byteplus-modelark-B8l8sPXu.js.map → byteplus-modelark-DaF0U4n1.js.map} +1 -1
- package/dist/chunks/{claude-cli-DeTKk9nu.js → claude-cli-BH_C62dV.js} +3 -3
- package/dist/chunks/{claude-cli-DeTKk9nu.js.map → claude-cli-BH_C62dV.js.map} +1 -1
- package/dist/chunks/{gateway-admin-DJXEDpUP.js → gateway-admin-B4RSBDmo.js} +2 -2
- package/dist/chunks/{gateway-admin-DJXEDpUP.js.map → gateway-admin-B4RSBDmo.js.map} +1 -1
- package/dist/chunks/{gemini-BT8fjXWz.js → gemini-GQdugon1.js} +4 -4
- package/dist/chunks/{gemini-BT8fjXWz.js.map → gemini-GQdugon1.js.map} +1 -1
- package/dist/chunks/{huggingface-CqzyxeLB.js → huggingface-DxPspM6Q.js} +3 -3
- package/dist/chunks/{huggingface-CqzyxeLB.js.map → huggingface-DxPspM6Q.js.map} +1 -1
- package/dist/chunks/{litellm-viqTuSY8.js → litellm-nOCNI7Vd.js} +4 -4
- package/dist/chunks/{litellm-viqTuSY8.js.map → litellm-nOCNI7Vd.js.map} +1 -1
- package/dist/chunks/{ollama-CdWqwZ05.js → ollama-BrB6bjnz.js} +3 -3
- package/dist/chunks/{ollama-CdWqwZ05.js.map → ollama-BrB6bjnz.js.map} +1 -1
- package/dist/chunks/{openai-BgrK3OfW.js → openai-BuwvBruA.js} +4 -4
- package/dist/chunks/{openai-BgrK3OfW.js.map → openai-BuwvBruA.js.map} +1 -1
- package/dist/chunks/{openai-compat-video-D_U7jo0d.js → openai-compat-video-DNE1Yvp6.js} +4 -4
- package/dist/chunks/{openai-compat-video-D_U7jo0d.js.map → openai-compat-video-DNE1Yvp6.js.map} +1 -1
- package/dist/chunks/{qwen-tts-BXjzY7DX.js → qwen-tts-CAh1upii.js} +2 -2
- package/dist/chunks/{qwen-tts-BXjzY7DX.js.map → qwen-tts-CAh1upii.js.map} +1 -1
- package/dist/chunks/{rate-limit-DgEhanDL.js → rate-limit-C2oPwLGI.js} +6 -5
- package/dist/chunks/rate-limit-C2oPwLGI.js.map +1 -0
- package/dist/chunks/{safety-C415clBm.js → safety-ClfpyKEN.js} +2 -2
- package/dist/chunks/{safety-C415clBm.js.map → safety-ClfpyKEN.js.map} +1 -1
- package/dist/chunks/seevio-Bbu_hTUF.js +451 -0
- package/dist/chunks/seevio-Bbu_hTUF.js.map +1 -0
- package/dist/chunks/{src-CU4qWJ2c.js → src-B091S0I4.js} +23 -16
- package/dist/chunks/src-B091S0I4.js.map +1 -0
- package/dist/chunks/{types-CINwUPDo.js → types-sSecpd_N.js} +3 -2
- package/dist/chunks/types-sSecpd_N.js.map +1 -0
- package/dist/index.js +3 -3
- package/dist/node/factory.d.ts +2 -1
- package/dist/node/factory.d.ts.map +1 -1
- package/dist/node.js +21 -6
- package/dist/node.js.map +1 -1
- package/dist/shared/factory.d.ts.map +1 -1
- package/dist/shared/providers/seevio.d.ts +48 -0
- package/dist/shared/providers/seevio.d.ts.map +1 -0
- package/dist/shared/rate-limit.d.ts.map +1 -1
- package/dist/shared/types.d.ts +61 -2
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/chunks/rate-limit-DgEhanDL.js.map +0 -1
- package/dist/chunks/src-CU4qWJ2c.js.map +0 -1
- package/dist/chunks/types-CINwUPDo.js.map +0 -1
package/dist/chunks/{openai-compat-video-D_U7jo0d.js.map → openai-compat-video-DNE1Yvp6.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai-compat-video-D_U7jo0d.js","names":[],"sources":["../../src/shared/providers/openai-compat-video.ts"],"sourcesContent":["/**\n * OpenAI-compatible video-generation provider implementation\n *\n * Thin, raw-HTTP adapter over a `/v1/videos`-shaped REST surface (create /\n * retrieve / cancel / download content) configured by base URL + key. This\n * is the shape LiteLLM normalizes Veo behind, and the shape used by\n * Sora-style gateways. The surface is young and still churning across\n * vendors, so this adapter is deliberately thin: it forwards the fields the\n * OpenAI `videos` API documents (model, prompt, seconds, size, a single\n * image reference) and leaves everything else to the gateway's defaults.\n *\n * Only video-generation methods are implemented; use the `openai` or\n * `litellm` provider types against the same gateway for chat/embeddings.\n *\n * Request bodies are sent as JSON, not the `multipart/form-data` the\n * official `openai` npm package uses for `input_reference` uploads. This is\n * a deliberate choice for this thin adapter: LiteLLM (the primary target\n * gateway) accepts `input_reference.image_url` as a URL or base64 data URL\n * in a JSON body, and staying JSON-only keeps this file a plain HTTP client\n * with no multipart-encoding dependency. A raw OpenAI Sora endpoint that\n * requires actual multipart file uploads is not supported by this adapter.\n */\n\nimport { ValidationError } from '@happyvertical/utils';\nimport { extractRetryAfterSeconds } from '../rate-limit';\nimport { normalizeBaseAIOptions } from '../safety';\nimport type {\n AICapabilities,\n AIInterface,\n AIMessage,\n AIModel,\n AIResponse,\n ChatOptions,\n CompletionOptions,\n EmbeddingOptions,\n EmbeddingResponse,\n ImageDescriptionOptions,\n ImageEmbeddingOptions,\n ImageGenerationOptions,\n ImageGenerationResponse,\n MessageOptions,\n OpenAICompatVideoOptions,\n TTSOptions,\n TTSResponse,\n VideoGenerationJob,\n VideoGenerationOptions,\n VideoGenerationResult,\n VideoGenerationStatusResult,\n Voice,\n VoiceCloneOptions,\n VoiceDesignOptions,\n VoiceListOptions,\n} from '../types';\nimport { AIError, AuthenticationError, RateLimitError } from '../types';\nimport { emitUsage } from './usage';\n\n/** Raw video-job object shape, following the OpenAI `videos` API. */\ninterface CompatVideoObject {\n id: string;\n status: string;\n model: string;\n progress?: number;\n seconds?: string;\n size?: string;\n error?: { code?: string; message?: string } | null;\n /** Some gateways (LiteLLM's Veo passthrough) include a direct download URL. */\n url?: string;\n}\n\nfunction ensureOpenAICompatVideoOptions(\n options: OpenAICompatVideoOptions,\n): OpenAICompatVideoOptions {\n if (!options.baseUrl?.trim()) {\n throw new ValidationError('openai-compat-video baseUrl is required', {\n provider: 'openai-compat-video',\n hint: 'Pass baseUrl like https://llm.happyvertical.com/v1 or set OPENAI_COMPAT_VIDEO_BASE_URL',\n });\n }\n\n return {\n ...options,\n baseUrl: options.baseUrl.trim().replace(/\\/+$/, ''),\n };\n}\n\nfunction parseSize(size: string | undefined): {\n width?: number;\n height?: number;\n} {\n const match = size?.match(/^(\\d+)x(\\d+)$/);\n if (!match) return {};\n return { width: Number(match[1]), height: Number(match[2]) };\n}\n\n/**\n * Map a gateway's raw status string to our status enum.\n *\n * LiteLLM (the primary target gateway) documents `'processing'` as its\n * mid-render status rather than OpenAI's own `'in_progress'`; both, plus\n * `'pending'`, map to `'running'`. Only an explicit `'failed'` maps to a\n * terminal failure — an unrecognized status is never assumed to be\n * terminal, since misreading a mid-render job as failed would cause a\n * caller to resubmit (and double-bill) a job that was actually still\n * rendering. Unrecognized values map to `'running'` with the raw string\n * preserved via `rawStatus` so callers can still see and log it.\n */\nfunction mapStatus(status: string): {\n status: VideoGenerationStatusResult['status'];\n rawStatus?: string;\n} {\n switch (status) {\n case 'queued':\n return { status: 'queued' };\n case 'in_progress':\n case 'processing':\n case 'pending':\n return { status: 'running' };\n case 'completed':\n return { status: 'succeeded' };\n case 'failed':\n return { status: 'failed' };\n case 'cancelled':\n case 'canceled':\n return { status: 'cancelled' };\n default:\n return { status: 'running', rawStatus: status };\n }\n}\n\n/**\n * OpenAI-compatible video-generation provider for `/v1/videos`-shaped\n * gateways (LiteLLM, Sora-shaped providers).\n *\n * @example\n * ```typescript\n * import { getAI } from '@happyvertical/ai';\n *\n * const video = await getAI({\n * type: 'openai-compat-video',\n * baseUrl: 'https://llm.happyvertical.com/v1',\n * apiKey: process.env.OPENAI_COMPAT_VIDEO_API_KEY!,\n * });\n *\n * const job = await video.submitVideoGenerationJob({\n * prompt: 'A time-lapse of clouds over a mountain range',\n * durationSeconds: 8,\n * });\n * ```\n */\nexport class OpenAICompatVideoProvider implements AIInterface {\n private options: OpenAICompatVideoOptions;\n\n constructor(options: OpenAICompatVideoOptions) {\n this.options = normalizeBaseAIOptions(\n ensureOpenAICompatVideoOptions(options),\n );\n }\n\n private get baseUrl(): string {\n return this.options.baseUrl as string;\n }\n\n private async request<T>(\n path: string,\n options: {\n method?: 'GET' | 'POST';\n body?: unknown;\n query?: Record<string, string | number>;\n signal?: AbortSignal;\n timeout?: number;\n } = {},\n ): Promise<T> {\n const response = await this.rawRequest(path, options);\n const text = await response.text();\n return text ? (JSON.parse(text) as T) : ({} as T);\n }\n\n private async rawRequest(\n path: string,\n options: {\n method?: 'GET' | 'POST';\n body?: unknown;\n query?: Record<string, string | number>;\n signal?: AbortSignal;\n timeout?: number;\n /** When true, a 404 response is returned as-is instead of throwing. */\n allowNotFound?: boolean;\n } = {},\n ): Promise<Response> {\n if (options.signal?.aborted) {\n throw new AIError(\n 'openai-compat-video request aborted by caller',\n 'AI_ABORTED',\n 'openai-compat-video',\n );\n }\n\n const url = new URL(`${this.baseUrl}${path}`);\n for (const [key, value] of Object.entries(options.query ?? {})) {\n url.searchParams.set(key, String(value));\n }\n\n const controller = new AbortController();\n const timeoutMs = options.timeout ?? this.options.timeout;\n let timedOut = false;\n const timer = timeoutMs\n ? setTimeout(() => {\n timedOut = true;\n controller.abort();\n }, timeoutMs)\n : undefined;\n // Mirrors `prepareRequestControls`'s pattern (packages/ai/src/shared/safety.ts):\n // addEventListener on an already-aborted signal never fires, so an\n // already-aborted `signal` must invoke the abort immediately instead of\n // only listening for a future 'abort' event.\n const onExternalAbort = () => controller.abort(options.signal?.reason);\n if (options.signal?.aborted) {\n onExternalAbort();\n } else {\n options.signal?.addEventListener('abort', onExternalAbort, {\n once: true,\n });\n }\n\n try {\n const headers: Record<string, string> = {\n ...(this.options.headers || {}),\n };\n if (this.options.apiKey) {\n headers.Authorization = `Bearer ${this.options.apiKey}`;\n }\n if (options.body !== undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n let response: Response;\n try {\n response = await fetch(url, {\n method: options.method || 'GET',\n headers,\n body:\n options.body !== undefined\n ? JSON.stringify(options.body)\n : undefined,\n signal: controller.signal,\n });\n } catch (error) {\n // The fetch() call itself failed (network error, DNS failure,\n // timeout/abort) — wrap it into the AIError taxonomy so callers get\n // a consistent, retryable-vs-terminal-distinguishable error rather\n // than a raw TypeError/DOMException.\n if (timedOut) {\n throw new AIError(\n `openai-compat-video request to ${path} timed out after ${timeoutMs}ms`,\n 'AI_TIMEOUT',\n 'openai-compat-video',\n undefined,\n true,\n );\n }\n if (options.signal?.aborted) {\n throw new AIError(\n 'openai-compat-video request aborted by caller',\n 'AI_ABORTED',\n 'openai-compat-video',\n );\n }\n throw new AIError(\n `Network error calling openai-compat-video ${path}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n 'NETWORK_ERROR',\n 'openai-compat-video',\n undefined,\n true,\n );\n }\n\n if (!response.ok) {\n if (response.status === 404 && options.allowNotFound) {\n return response;\n }\n\n const errorText = await response.text();\n\n if (response.status === 401 || response.status === 403) {\n throw new AuthenticationError('openai-compat-video');\n }\n if (response.status === 429) {\n throw new RateLimitError(\n 'openai-compat-video',\n extractRetryAfterSeconds({ headers: response.headers }),\n );\n }\n\n throw new AIError(\n `openai-compat-video request to ${path} failed: ${response.status} ${errorText}`,\n 'OPENAI_COMPAT_VIDEO_REQUEST_FAILED',\n 'openai-compat-video',\n );\n }\n\n return response;\n } finally {\n if (timer) clearTimeout(timer);\n options.signal?.removeEventListener('abort', onExternalAbort);\n }\n }\n\n // ============================================================================\n // Video Generation Methods (Implemented)\n // ============================================================================\n\n /**\n * Submit an asynchronous video-generation job.\n *\n * `resolution` is forwarded as the gateway's `size` field only when it is\n * already in `WIDTHxHEIGHT` form (e.g. `'1280x720'`); other resolution\n * labels (`'720p'`) are not translated and the gateway's default size\n * applies instead — keeping this adapter thin rather than guessing a\n * pixel size per vendor. `negativePrompt`, `seed`, and `fps` are not part\n * of the documented OpenAI videos schema and are not forwarded.\n */\n async submitVideoGenerationJob(\n options: VideoGenerationOptions,\n ): Promise<VideoGenerationJob> {\n const startTime = Date.now();\n const model = options.model || this.options.defaultModel || 'sora-2';\n\n const body: Record<string, unknown> = {\n model,\n prompt: options.prompt,\n };\n if (options.durationSeconds !== undefined) {\n body.seconds = String(options.durationSeconds);\n }\n if (options.resolution?.includes('x')) {\n body.size = options.resolution;\n }\n const firstReference = options.referenceImages?.[0];\n if (firstReference) {\n body.input_reference = {\n image_url:\n typeof firstReference.image === 'string'\n ? firstReference.image\n : `data:${firstReference.mimeType || 'image/png'};base64,${firstReference.image.toString('base64')}`,\n };\n }\n\n const video = await this.request<CompatVideoObject>('/videos', {\n method: 'POST',\n body,\n signal: options.signal,\n timeout: options.timeout,\n });\n\n emitUsage(\n this.options,\n 'openai-compat-video',\n 'submitVideoGenerationJob',\n video.model || model,\n undefined,\n startTime,\n {\n ...(options.durationSeconds !== undefined\n ? { durationSeconds: String(options.durationSeconds) }\n : {}),\n ...(options.resolution ? { resolution: options.resolution } : {}),\n ...(options.aspectRatio ? { aspectRatio: options.aspectRatio } : {}),\n ...options.usageTags,\n },\n );\n\n return {\n jobId: video.id,\n provider: 'openai-compat-video',\n model: video.model || model,\n createdAt: new Date().toISOString(),\n };\n }\n\n /**\n * Poll the status of a video-generation job.\n *\n * The gateway's status response does not include the video bytes, so\n * `result` on a succeeded job carries only metadata (mimeType, duration,\n * dimensions) plus a `url` when the gateway happens to include one\n * directly. Call {@link fetchVideoGenerationResult} to download the bytes.\n */\n async getVideoGenerationJob(\n handle: VideoGenerationJob,\n ): Promise<VideoGenerationStatusResult> {\n const video = await this.request<CompatVideoObject>(\n `/videos/${handle.jobId}`,\n );\n return this.mapVideo(video);\n }\n\n /**\n * Fetch the result of a completed video-generation job, downloading the\n * rendered bytes from the gateway's content endpoint.\n *\n * @throws {AIError} When the job has not succeeded yet\n */\n async fetchVideoGenerationResult(\n handle: VideoGenerationJob,\n ): Promise<VideoGenerationResult> {\n const video = await this.request<CompatVideoObject>(\n `/videos/${handle.jobId}`,\n );\n const status = this.mapVideo(video);\n if (status.status !== 'succeeded') {\n throw new AIError(\n `openai-compat-video job ${handle.jobId} has not succeeded (status: ${status.status})`,\n 'VIDEO_JOB_NOT_READY',\n 'openai-compat-video',\n handle.model,\n );\n }\n\n if (video.url) {\n return status.result as VideoGenerationResult;\n }\n\n const response = await this.rawRequest(`/videos/${handle.jobId}/content`);\n const arrayBuffer = await response.arrayBuffer();\n const { width, height } = parseSize(video.size);\n\n return {\n data: Buffer.from(arrayBuffer),\n mimeType: response.headers.get('content-type') || 'video/mp4',\n durationSeconds: video.seconds ? Number(video.seconds) : undefined,\n width,\n height,\n };\n }\n\n /**\n * Cancel a video-generation job. Cancellation of an in-flight Sora-shaped\n * job is not part of the documented OpenAI `videos` API and is\n * gateway-dependent (e.g. LiteLLM's Veo passthrough may support it); this\n * attempts the conventional `/videos/{id}/cancel` REST call and surfaces\n * an {@link AIError} if the gateway rejects or does not implement it.\n */\n async cancelVideoGenerationJob(handle: VideoGenerationJob): Promise<void> {\n await this.rawRequest(`/videos/${handle.jobId}/cancel`, {\n method: 'POST',\n });\n }\n\n /**\n * Cheap auth-shaped check for video-generation access.\n *\n * This probes `GET /videos/{sentinel-id}` rather than listing videos:\n * LiteLLM (the primary target gateway) requires a `custom_llm_provider`\n * parameter on its `GET /v1/videos` list route that this generic adapter\n * has no context to supply, so a list-based check would fail with valid\n * credentials on the flagship gateway. A single-resource GET works\n * uniformly instead, classified as:\n * - `401`/`403` → invalid credentials, throws {@link AuthenticationError}\n * - `404` → authenticated successfully, the sentinel id just doesn't\n * exist — this is a **valid** result, not a failure\n * - `2xx` → valid (the sentinel id coincidentally exists)\n *\n * Callers on a hot path must cache the result themselves.\n */\n async validateVideoGenerationAccess(): Promise<boolean> {\n const sentinelId = 'have-ai-validate-video-access';\n const response = await this.rawRequest(`/videos/${sentinelId}`, {\n allowNotFound: true,\n });\n return response.ok || response.status === 404;\n }\n\n private mapVideo(video: CompatVideoObject): VideoGenerationStatusResult {\n const { status, rawStatus } = mapStatus(video.status);\n\n if (status === 'failed') {\n return {\n status: 'failed',\n error:\n video.error?.message ||\n `Video generation failed (status: ${video.status})`,\n };\n }\n\n if (status !== 'succeeded') {\n return { status, progress: video.progress, rawStatus };\n }\n\n const { width, height } = parseSize(video.size);\n const result: VideoGenerationResult = {\n url: video.url,\n mimeType: 'video/mp4',\n durationSeconds: video.seconds ? Number(video.seconds) : undefined,\n width,\n height,\n };\n\n return { status: 'succeeded', result };\n }\n\n async getModels(): Promise<AIModel[]> {\n return [\n {\n id: 'sora-2',\n name: 'Sora 2',\n description: 'OpenAI-compatible Sora 2 video-generation model',\n contextLength: 0,\n capabilities: ['video_generation'],\n supportsFunctions: false,\n supportsVision: true,\n },\n {\n id: 'sora-2-pro',\n name: 'Sora 2 Pro',\n description: 'OpenAI-compatible Sora 2 Pro video-generation model',\n contextLength: 0,\n capabilities: ['video_generation'],\n supportsFunctions: false,\n supportsVision: true,\n },\n ];\n }\n\n async getCapabilities(): Promise<AICapabilities> {\n return {\n chat: false,\n completion: false,\n embeddings: false,\n streaming: false,\n functions: false,\n vision: false,\n fineTuning: false,\n imageEmbeddings: false,\n imageGeneration: false,\n videoGeneration: true,\n tts: false,\n voiceCloning: false,\n voiceDesign: false,\n maxContextLength: 0,\n supportedOperations: [\n 'submitVideoGenerationJob',\n 'getVideoGenerationJob',\n 'fetchVideoGenerationResult',\n 'cancelVideoGenerationJob',\n ],\n };\n }\n\n // ============================================================================\n // Text Generation / TTS Methods (Not Implemented - Use another provider)\n // ============================================================================\n\n async chat(\n _messages: AIMessage[],\n _options?: ChatOptions,\n ): Promise<AIResponse> {\n throw new AIError(\n 'Chat is not supported by the openai-compat-video provider. Use the openai or litellm provider for text generation.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async complete(\n _prompt: string,\n _options?: CompletionOptions,\n ): Promise<AIResponse> {\n throw new AIError(\n 'Complete is not supported by the openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async message(_text: string, _options?: MessageOptions): Promise<string> {\n throw new AIError(\n 'Message is not supported by the openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async embed(\n _text: string | string[],\n _options?: EmbeddingOptions,\n ): Promise<EmbeddingResponse> {\n throw new AIError(\n 'Embeddings are not supported by the openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async embedImage(\n _image: string | Buffer,\n _options?: ImageEmbeddingOptions,\n ): Promise<EmbeddingResponse> {\n throw new AIError(\n 'Image embeddings are not supported by the openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async describeImage(\n _image: string | Buffer,\n _prompt?: string,\n _options?: ImageDescriptionOptions,\n ): Promise<string> {\n throw new AIError(\n 'Image description is not supported by the openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async generateImage(\n _prompt: string,\n _options?: ImageGenerationOptions,\n ): Promise<ImageGenerationResponse> {\n throw new AIError(\n 'Image generation is not supported by the openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n stream(\n _messages: AIMessage[],\n _options?: ChatOptions,\n ): AsyncIterable<string> {\n const error = new AIError(\n 'Chat streaming is not supported by the openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n return {\n [Symbol.asyncIterator]: () => ({\n next: () => Promise.reject(error),\n }),\n };\n }\n\n async countTokens(_text: string): Promise<number> {\n throw new AIError(\n 'Token counting is not supported by the openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async synthesizeSpeech(\n _text: string,\n _options?: TTSOptions,\n ): Promise<TTSResponse> {\n throw new AIError(\n 'TTS is not supported by the openai-compat-video provider. Use Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n streamSpeech(_text: string, _options?: TTSOptions): AsyncIterable<Buffer> {\n const error = new AIError(\n 'TTS streaming is not supported by the openai-compat-video provider. Use Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n return {\n [Symbol.asyncIterator]: () => ({\n next: () => Promise.reject(error),\n }),\n };\n }\n\n async cloneVoice(_options: VoiceCloneOptions): Promise<Voice> {\n throw new AIError(\n 'Voice cloning is not supported by the openai-compat-video provider. Use Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async designVoice(_options: VoiceDesignOptions): Promise<Voice> {\n throw new AIError(\n 'Voice design is not supported by the openai-compat-video provider. Use Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async getVoices(_options?: VoiceListOptions): Promise<Voice[]> {\n throw new AIError(\n 'Voice listing is not supported by the openai-compat-video provider. Use Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqEA,SAAS,+BACP,SAC0B;CAC1B,IAAI,CAAC,QAAQ,SAAS,KAAK,GACzB,MAAM,IAAI,gBAAgB,2CAA2C;EACnE,UAAU;EACV,MAAM;CACR,CAAC;CAGH,OAAO;EACL,GAAG;EACH,SAAS,QAAQ,QAAQ,KAAK,CAAC,CAAC,QAAQ,QAAQ,EAAE;CACpD;AACF;AAEA,SAAS,UAAU,MAGjB;CACA,MAAM,QAAQ,MAAM,MAAM,eAAe;CACzC,IAAI,CAAC,OAAO,OAAO,CAAC;CACpB,OAAO;EAAE,OAAO,OAAO,MAAM,EAAE;EAAG,QAAQ,OAAO,MAAM,EAAE;CAAE;AAC7D;;;;;;;;;;;;;AAcA,SAAS,UAAU,QAGjB;CACA,QAAQ,QAAR;EACE,KAAK,UACH,OAAO,EAAE,QAAQ,SAAS;EAC5B,KAAK;EACL,KAAK;EACL,KAAK,WACH,OAAO,EAAE,QAAQ,UAAU;EAC7B,KAAK,aACH,OAAO,EAAE,QAAQ,YAAY;EAC/B,KAAK,UACH,OAAO,EAAE,QAAQ,SAAS;EAC5B,KAAK;EACL,KAAK,YACH,OAAO,EAAE,QAAQ,YAAY;EAC/B,SACE,OAAO;GAAE,QAAQ;GAAW,WAAW;EAAO;CAClD;AACF;;;;;;;;;;;;;;;;;;;;;AAsBA,IAAa,4BAAb,MAA8D;CAC5D;CAEA,YAAY,SAAmC;EAC7C,KAAK,UAAU,uBACb,+BAA+B,OAAO,CACxC;CACF;CAEA,IAAY,UAAkB;EAC5B,OAAO,KAAK,QAAQ;CACtB;CAEA,MAAc,QACZ,MACA,UAMI,CAAC,GACO;EAEZ,MAAM,OAAO,OAAM,MADI,KAAK,WAAW,MAAM,OAAO,EAAA,CACxB,KAAK;EACjC,OAAO,OAAQ,KAAK,MAAM,IAAI,IAAW,CAAC;CAC5C;CAEA,MAAc,WACZ,MACA,UAQI,CAAC,GACc;EACnB,IAAI,QAAQ,QAAQ,SAClB,MAAM,IAAI,QACR,iDACA,cACA,qBACF;EAGF,MAAM,MAAM,IAAI,IAAI,GAAG,KAAK,UAAU,MAAM;EAC5C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,SAAS,CAAC,CAAC,GAC3D,IAAI,aAAa,IAAI,KAAK,OAAO,KAAK,CAAC;EAGzC,MAAM,aAAa,IAAI,gBAAgB;EACvC,MAAM,YAAY,QAAQ,WAAW,KAAK,QAAQ;EAClD,IAAI,WAAW;EACf,MAAM,QAAQ,YACV,iBAAiB;GACf,WAAW;GACX,WAAW,MAAM;EACnB,GAAG,SAAS,IACZ,KAAA;EAKJ,MAAM,wBAAwB,WAAW,MAAM,QAAQ,QAAQ,MAAM;EACrE,IAAI,QAAQ,QAAQ,SAClB,gBAAgB;OAEhB,QAAQ,QAAQ,iBAAiB,SAAS,iBAAiB,EACzD,MAAM,KACR,CAAC;EAGH,IAAI;GACF,MAAM,UAAkC,EACtC,GAAI,KAAK,QAAQ,WAAW,CAAC,EAC/B;GACA,IAAI,KAAK,QAAQ,QACf,QAAQ,gBAAgB,UAAU,KAAK,QAAQ;GAEjD,IAAI,QAAQ,SAAS,KAAA,GACnB,QAAQ,kBAAkB;GAG5B,IAAI;GACJ,IAAI;IACF,WAAW,MAAM,MAAM,KAAK;KAC1B,QAAQ,QAAQ,UAAU;KAC1B;KACA,MACE,QAAQ,SAAS,KAAA,IACb,KAAK,UAAU,QAAQ,IAAI,IAC3B,KAAA;KACN,QAAQ,WAAW;IACrB,CAAC;GACH,SAAS,OAAO;IAKd,IAAI,UACF,MAAM,IAAI,QACR,kCAAkC,KAAK,mBAAmB,UAAU,KACpE,cACA,uBACA,KAAA,GACA,IACF;IAEF,IAAI,QAAQ,QAAQ,SAClB,MAAM,IAAI,QACR,iDACA,cACA,qBACF;IAEF,MAAM,IAAI,QACR,6CAA6C,KAAK,IAChD,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,KAEvD,iBACA,uBACA,KAAA,GACA,IACF;GACF;GAEA,IAAI,CAAC,SAAS,IAAI;IAChB,IAAI,SAAS,WAAW,OAAO,QAAQ,eACrC,OAAO;IAGT,MAAM,YAAY,MAAM,SAAS,KAAK;IAEtC,IAAI,SAAS,WAAW,OAAO,SAAS,WAAW,KACjD,MAAM,IAAI,oBAAoB,qBAAqB;IAErD,IAAI,SAAS,WAAW,KACtB,MAAM,IAAI,eACR,uBACA,yBAAyB,EAAE,SAAS,SAAS,QAAQ,CAAC,CACxD;IAGF,MAAM,IAAI,QACR,kCAAkC,KAAK,WAAW,SAAS,OAAO,GAAG,aACrE,sCACA,qBACF;GACF;GAEA,OAAO;EACT,UAAU;GACR,IAAI,OAAO,aAAa,KAAK;GAC7B,QAAQ,QAAQ,oBAAoB,SAAS,eAAe;EAC9D;CACF;;;;;;;;;;;CAgBA,MAAM,yBACJ,SAC6B;EAC7B,MAAM,YAAY,KAAK,IAAI;EAC3B,MAAM,QAAQ,QAAQ,SAAS,KAAK,QAAQ,gBAAgB;EAE5D,MAAM,OAAgC;GACpC;GACA,QAAQ,QAAQ;EAClB;EACA,IAAI,QAAQ,oBAAoB,KAAA,GAC9B,KAAK,UAAU,OAAO,QAAQ,eAAe;EAE/C,IAAI,QAAQ,YAAY,SAAS,GAAG,GAClC,KAAK,OAAO,QAAQ;EAEtB,MAAM,iBAAiB,QAAQ,kBAAkB;EACjD,IAAI,gBACF,KAAK,kBAAkB,EACrB,WACE,OAAO,eAAe,UAAU,WAC5B,eAAe,QACf,QAAQ,eAAe,YAAY,YAAY,UAAU,eAAe,MAAM,SAAS,QAAQ,IACvG;EAGF,MAAM,QAAQ,MAAM,KAAK,QAA2B,WAAW;GAC7D,QAAQ;GACR;GACA,QAAQ,QAAQ;GAChB,SAAS,QAAQ;EACnB,CAAC;EAED,UACE,KAAK,SACL,uBACA,4BACA,MAAM,SAAS,OACf,KAAA,GACA,WACA;GACE,GAAI,QAAQ,oBAAoB,KAAA,IAC5B,EAAE,iBAAiB,OAAO,QAAQ,eAAe,EAAE,IACnD,CAAC;GACL,GAAI,QAAQ,aAAa,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;GAC/D,GAAI,QAAQ,cAAc,EAAE,aAAa,QAAQ,YAAY,IAAI,CAAC;GAClE,GAAG,QAAQ;EACb,CACF;EAEA,OAAO;GACL,OAAO,MAAM;GACb,UAAU;GACV,OAAO,MAAM,SAAS;GACtB,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC;CACF;;;;;;;;;CAUA,MAAM,sBACJ,QACsC;EACtC,MAAM,QAAQ,MAAM,KAAK,QACvB,WAAW,OAAO,OACpB;EACA,OAAO,KAAK,SAAS,KAAK;CAC5B;;;;;;;CAQA,MAAM,2BACJ,QACgC;EAChC,MAAM,QAAQ,MAAM,KAAK,QACvB,WAAW,OAAO,OACpB;EACA,MAAM,SAAS,KAAK,SAAS,KAAK;EAClC,IAAI,OAAO,WAAW,aACpB,MAAM,IAAI,QACR,2BAA2B,OAAO,MAAM,8BAA8B,OAAO,OAAO,IACpF,uBACA,uBACA,OAAO,KACT;EAGF,IAAI,MAAM,KACR,OAAO,OAAO;EAGhB,MAAM,WAAW,MAAM,KAAK,WAAW,WAAW,OAAO,MAAM,SAAS;EACxE,MAAM,cAAc,MAAM,SAAS,YAAY;EAC/C,MAAM,EAAE,OAAO,WAAW,UAAU,MAAM,IAAI;EAE9C,OAAO;GACL,MAAM,OAAO,KAAK,WAAW;GAC7B,UAAU,SAAS,QAAQ,IAAI,cAAc,KAAK;GAClD,iBAAiB,MAAM,UAAU,OAAO,MAAM,OAAO,IAAI,KAAA;GACzD;GACA;EACF;CACF;;;;;;;;CASA,MAAM,yBAAyB,QAA2C;EACxE,MAAM,KAAK,WAAW,WAAW,OAAO,MAAM,UAAU,EACtD,QAAQ,OACV,CAAC;CACH;;;;;;;;;;;;;;;;;CAkBA,MAAM,gCAAkD;EAEtD,MAAM,WAAW,MAAM,KAAK,WAAW,yCAAyB,EAC9D,eAAe,KACjB,CAAC;EACD,OAAO,SAAS,MAAM,SAAS,WAAW;CAC5C;CAEA,SAAiB,OAAuD;EACtE,MAAM,EAAE,QAAQ,cAAc,UAAU,MAAM,MAAM;EAEpD,IAAI,WAAW,UACb,OAAO;GACL,QAAQ;GACR,OACE,MAAM,OAAO,WACb,oCAAoC,MAAM,OAAO;EACrD;EAGF,IAAI,WAAW,aACb,OAAO;GAAE;GAAQ,UAAU,MAAM;GAAU;EAAU;EAGvD,MAAM,EAAE,OAAO,WAAW,UAAU,MAAM,IAAI;EAS9C,OAAO;GAAE,QAAQ;GAAa,QAAA;IAP5B,KAAK,MAAM;IACX,UAAU;IACV,iBAAiB,MAAM,UAAU,OAAO,MAAM,OAAO,IAAI,KAAA;IACzD;IACA;GAG4B;EAAO;CACvC;CAEA,MAAM,YAAgC;EACpC,OAAO,CACL;GACE,IAAI;GACJ,MAAM;GACN,aAAa;GACb,eAAe;GACf,cAAc,CAAC,kBAAkB;GACjC,mBAAmB;GACnB,gBAAgB;EAClB,GACA;GACE,IAAI;GACJ,MAAM;GACN,aAAa;GACb,eAAe;GACf,cAAc,CAAC,kBAAkB;GACjC,mBAAmB;GACnB,gBAAgB;EAClB,CACF;CACF;CAEA,MAAM,kBAA2C;EAC/C,OAAO;GACL,MAAM;GACN,YAAY;GACZ,YAAY;GACZ,WAAW;GACX,WAAW;GACX,QAAQ;GACR,YAAY;GACZ,iBAAiB;GACjB,iBAAiB;GACjB,iBAAiB;GACjB,KAAK;GACL,cAAc;GACd,aAAa;GACb,kBAAkB;GAClB,qBAAqB;IACnB;IACA;IACA;IACA;GACF;EACF;CACF;CAMA,MAAM,KACJ,WACA,UACqB;EACrB,MAAM,IAAI,QACR,sHACA,mBACA,qBACF;CACF;CAEA,MAAM,SACJ,SACA,UACqB;EACrB,MAAM,IAAI,QACR,kEACA,mBACA,qBACF;CACF;CAEA,MAAM,QAAQ,OAAe,UAA4C;EACvE,MAAM,IAAI,QACR,iEACA,mBACA,qBACF;CACF;CAEA,MAAM,MACJ,OACA,UAC4B;EAC5B,MAAM,IAAI,QACR,qEACA,mBACA,qBACF;CACF;CAEA,MAAM,WACJ,QACA,UAC4B;EAC5B,MAAM,IAAI,QACR,2EACA,mBACA,qBACF;CACF;CAEA,MAAM,cACJ,QACA,SACA,UACiB;EACjB,MAAM,IAAI,QACR,2EACA,mBACA,qBACF;CACF;CAEA,MAAM,cACJ,SACA,UACkC;EAClC,MAAM,IAAI,QACR,0EACA,mBACA,qBACF;CACF;CAEA,OACE,WACA,UACuB;EACvB,MAAM,QAAQ,IAAI,QAChB,wEACA,mBACA,qBACF;EACA,OAAO,GACJ,OAAO,uBAAuB,EAC7B,YAAY,QAAQ,OAAO,KAAK,EAClC,GACF;CACF;CAEA,MAAM,YAAY,OAAgC;EAChD,MAAM,IAAI,QACR,wEACA,mBACA,qBACF;CACF;CAEA,MAAM,iBACJ,OACA,UACsB;EACtB,MAAM,IAAI,QACR,qFACA,mBACA,qBACF;CACF;CAEA,aAAa,OAAe,UAA8C;EACxE,MAAM,QAAQ,IAAI,QAChB,+FACA,mBACA,qBACF;EACA,OAAO,GACJ,OAAO,uBAAuB,EAC7B,YAAY,QAAQ,OAAO,KAAK,EAClC,GACF;CACF;CAEA,MAAM,WAAW,UAA6C;EAC5D,MAAM,IAAI,QACR,+FACA,mBACA,qBACF;CACF;CAEA,MAAM,YAAY,UAA8C;EAC9D,MAAM,IAAI,QACR,8FACA,mBACA,qBACF;CACF;CAEA,MAAM,UAAU,UAA+C;EAC7D,MAAM,IAAI,QACR,+FACA,mBACA,qBACF;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"openai-compat-video-DNE1Yvp6.js","names":[],"sources":["../../src/shared/providers/openai-compat-video.ts"],"sourcesContent":["/**\n * OpenAI-compatible video-generation provider implementation\n *\n * Thin, raw-HTTP adapter over a `/v1/videos`-shaped REST surface (create /\n * retrieve / cancel / download content) configured by base URL + key. This\n * is the shape LiteLLM normalizes Veo behind, and the shape used by\n * Sora-style gateways. The surface is young and still churning across\n * vendors, so this adapter is deliberately thin: it forwards the fields the\n * OpenAI `videos` API documents (model, prompt, seconds, size, a single\n * image reference) and leaves everything else to the gateway's defaults.\n *\n * Only video-generation methods are implemented; use the `openai` or\n * `litellm` provider types against the same gateway for chat/embeddings.\n *\n * Request bodies are sent as JSON, not the `multipart/form-data` the\n * official `openai` npm package uses for `input_reference` uploads. This is\n * a deliberate choice for this thin adapter: LiteLLM (the primary target\n * gateway) accepts `input_reference.image_url` as a URL or base64 data URL\n * in a JSON body, and staying JSON-only keeps this file a plain HTTP client\n * with no multipart-encoding dependency. A raw OpenAI Sora endpoint that\n * requires actual multipart file uploads is not supported by this adapter.\n */\n\nimport { ValidationError } from '@happyvertical/utils';\nimport { extractRetryAfterSeconds } from '../rate-limit';\nimport { normalizeBaseAIOptions } from '../safety';\nimport type {\n AICapabilities,\n AIInterface,\n AIMessage,\n AIModel,\n AIResponse,\n ChatOptions,\n CompletionOptions,\n EmbeddingOptions,\n EmbeddingResponse,\n ImageDescriptionOptions,\n ImageEmbeddingOptions,\n ImageGenerationOptions,\n ImageGenerationResponse,\n MessageOptions,\n OpenAICompatVideoOptions,\n TTSOptions,\n TTSResponse,\n VideoGenerationJob,\n VideoGenerationOptions,\n VideoGenerationResult,\n VideoGenerationStatusResult,\n Voice,\n VoiceCloneOptions,\n VoiceDesignOptions,\n VoiceListOptions,\n} from '../types';\nimport { AIError, AuthenticationError, RateLimitError } from '../types';\nimport { emitUsage } from './usage';\n\n/** Raw video-job object shape, following the OpenAI `videos` API. */\ninterface CompatVideoObject {\n id: string;\n status: string;\n model: string;\n progress?: number;\n seconds?: string;\n size?: string;\n error?: { code?: string; message?: string } | null;\n /** Some gateways (LiteLLM's Veo passthrough) include a direct download URL. */\n url?: string;\n}\n\nfunction ensureOpenAICompatVideoOptions(\n options: OpenAICompatVideoOptions,\n): OpenAICompatVideoOptions {\n if (!options.baseUrl?.trim()) {\n throw new ValidationError('openai-compat-video baseUrl is required', {\n provider: 'openai-compat-video',\n hint: 'Pass baseUrl like https://llm.happyvertical.com/v1 or set OPENAI_COMPAT_VIDEO_BASE_URL',\n });\n }\n\n return {\n ...options,\n baseUrl: options.baseUrl.trim().replace(/\\/+$/, ''),\n };\n}\n\nfunction parseSize(size: string | undefined): {\n width?: number;\n height?: number;\n} {\n const match = size?.match(/^(\\d+)x(\\d+)$/);\n if (!match) return {};\n return { width: Number(match[1]), height: Number(match[2]) };\n}\n\n/**\n * Map a gateway's raw status string to our status enum.\n *\n * LiteLLM (the primary target gateway) documents `'processing'` as its\n * mid-render status rather than OpenAI's own `'in_progress'`; both, plus\n * `'pending'`, map to `'running'`. Only an explicit `'failed'` maps to a\n * terminal failure — an unrecognized status is never assumed to be\n * terminal, since misreading a mid-render job as failed would cause a\n * caller to resubmit (and double-bill) a job that was actually still\n * rendering. Unrecognized values map to `'running'` with the raw string\n * preserved via `rawStatus` so callers can still see and log it.\n */\nfunction mapStatus(status: string): {\n status: VideoGenerationStatusResult['status'];\n rawStatus?: string;\n} {\n switch (status) {\n case 'queued':\n return { status: 'queued' };\n case 'in_progress':\n case 'processing':\n case 'pending':\n return { status: 'running' };\n case 'completed':\n return { status: 'succeeded' };\n case 'failed':\n return { status: 'failed' };\n case 'cancelled':\n case 'canceled':\n return { status: 'cancelled' };\n default:\n return { status: 'running', rawStatus: status };\n }\n}\n\n/**\n * OpenAI-compatible video-generation provider for `/v1/videos`-shaped\n * gateways (LiteLLM, Sora-shaped providers).\n *\n * @example\n * ```typescript\n * import { getAI } from '@happyvertical/ai';\n *\n * const video = await getAI({\n * type: 'openai-compat-video',\n * baseUrl: 'https://llm.happyvertical.com/v1',\n * apiKey: process.env.OPENAI_COMPAT_VIDEO_API_KEY!,\n * });\n *\n * const job = await video.submitVideoGenerationJob({\n * prompt: 'A time-lapse of clouds over a mountain range',\n * durationSeconds: 8,\n * });\n * ```\n */\nexport class OpenAICompatVideoProvider implements AIInterface {\n private options: OpenAICompatVideoOptions;\n\n constructor(options: OpenAICompatVideoOptions) {\n this.options = normalizeBaseAIOptions(\n ensureOpenAICompatVideoOptions(options),\n );\n }\n\n private get baseUrl(): string {\n return this.options.baseUrl as string;\n }\n\n private async request<T>(\n path: string,\n options: {\n method?: 'GET' | 'POST';\n body?: unknown;\n query?: Record<string, string | number>;\n signal?: AbortSignal;\n timeout?: number;\n } = {},\n ): Promise<T> {\n const response = await this.rawRequest(path, options);\n const text = await response.text();\n return text ? (JSON.parse(text) as T) : ({} as T);\n }\n\n private async rawRequest(\n path: string,\n options: {\n method?: 'GET' | 'POST';\n body?: unknown;\n query?: Record<string, string | number>;\n signal?: AbortSignal;\n timeout?: number;\n /** When true, a 404 response is returned as-is instead of throwing. */\n allowNotFound?: boolean;\n } = {},\n ): Promise<Response> {\n if (options.signal?.aborted) {\n throw new AIError(\n 'openai-compat-video request aborted by caller',\n 'AI_ABORTED',\n 'openai-compat-video',\n );\n }\n\n const url = new URL(`${this.baseUrl}${path}`);\n for (const [key, value] of Object.entries(options.query ?? {})) {\n url.searchParams.set(key, String(value));\n }\n\n const controller = new AbortController();\n const timeoutMs = options.timeout ?? this.options.timeout;\n let timedOut = false;\n const timer = timeoutMs\n ? setTimeout(() => {\n timedOut = true;\n controller.abort();\n }, timeoutMs)\n : undefined;\n // Mirrors `prepareRequestControls`'s pattern (packages/ai/src/shared/safety.ts):\n // addEventListener on an already-aborted signal never fires, so an\n // already-aborted `signal` must invoke the abort immediately instead of\n // only listening for a future 'abort' event.\n const onExternalAbort = () => controller.abort(options.signal?.reason);\n if (options.signal?.aborted) {\n onExternalAbort();\n } else {\n options.signal?.addEventListener('abort', onExternalAbort, {\n once: true,\n });\n }\n\n try {\n const headers: Record<string, string> = {\n ...(this.options.headers || {}),\n };\n if (this.options.apiKey) {\n headers.Authorization = `Bearer ${this.options.apiKey}`;\n }\n if (options.body !== undefined) {\n headers['Content-Type'] = 'application/json';\n }\n\n let response: Response;\n try {\n response = await fetch(url, {\n method: options.method || 'GET',\n headers,\n body:\n options.body !== undefined\n ? JSON.stringify(options.body)\n : undefined,\n signal: controller.signal,\n });\n } catch (error) {\n // The fetch() call itself failed (network error, DNS failure,\n // timeout/abort) — wrap it into the AIError taxonomy so callers get\n // a consistent, retryable-vs-terminal-distinguishable error rather\n // than a raw TypeError/DOMException.\n if (timedOut) {\n throw new AIError(\n `openai-compat-video request to ${path} timed out after ${timeoutMs}ms`,\n 'AI_TIMEOUT',\n 'openai-compat-video',\n undefined,\n true,\n );\n }\n if (options.signal?.aborted) {\n throw new AIError(\n 'openai-compat-video request aborted by caller',\n 'AI_ABORTED',\n 'openai-compat-video',\n );\n }\n throw new AIError(\n `Network error calling openai-compat-video ${path}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n 'NETWORK_ERROR',\n 'openai-compat-video',\n undefined,\n true,\n );\n }\n\n if (!response.ok) {\n if (response.status === 404 && options.allowNotFound) {\n return response;\n }\n\n const errorText = await response.text();\n\n if (response.status === 401 || response.status === 403) {\n throw new AuthenticationError('openai-compat-video');\n }\n if (response.status === 429) {\n throw new RateLimitError(\n 'openai-compat-video',\n extractRetryAfterSeconds({ headers: response.headers }),\n );\n }\n\n throw new AIError(\n `openai-compat-video request to ${path} failed: ${response.status} ${errorText}`,\n 'OPENAI_COMPAT_VIDEO_REQUEST_FAILED',\n 'openai-compat-video',\n );\n }\n\n return response;\n } finally {\n if (timer) clearTimeout(timer);\n options.signal?.removeEventListener('abort', onExternalAbort);\n }\n }\n\n // ============================================================================\n // Video Generation Methods (Implemented)\n // ============================================================================\n\n /**\n * Submit an asynchronous video-generation job.\n *\n * `resolution` is forwarded as the gateway's `size` field only when it is\n * already in `WIDTHxHEIGHT` form (e.g. `'1280x720'`); other resolution\n * labels (`'720p'`) are not translated and the gateway's default size\n * applies instead — keeping this adapter thin rather than guessing a\n * pixel size per vendor. `negativePrompt`, `seed`, and `fps` are not part\n * of the documented OpenAI videos schema and are not forwarded.\n */\n async submitVideoGenerationJob(\n options: VideoGenerationOptions,\n ): Promise<VideoGenerationJob> {\n const startTime = Date.now();\n const model = options.model || this.options.defaultModel || 'sora-2';\n\n const body: Record<string, unknown> = {\n model,\n prompt: options.prompt,\n };\n if (options.durationSeconds !== undefined) {\n body.seconds = String(options.durationSeconds);\n }\n if (options.resolution?.includes('x')) {\n body.size = options.resolution;\n }\n const firstReference = options.referenceImages?.[0];\n if (firstReference) {\n body.input_reference = {\n image_url:\n typeof firstReference.image === 'string'\n ? firstReference.image\n : `data:${firstReference.mimeType || 'image/png'};base64,${firstReference.image.toString('base64')}`,\n };\n }\n\n const video = await this.request<CompatVideoObject>('/videos', {\n method: 'POST',\n body,\n signal: options.signal,\n timeout: options.timeout,\n });\n\n emitUsage(\n this.options,\n 'openai-compat-video',\n 'submitVideoGenerationJob',\n video.model || model,\n undefined,\n startTime,\n {\n ...(options.durationSeconds !== undefined\n ? { durationSeconds: String(options.durationSeconds) }\n : {}),\n ...(options.resolution ? { resolution: options.resolution } : {}),\n ...(options.aspectRatio ? { aspectRatio: options.aspectRatio } : {}),\n ...options.usageTags,\n },\n );\n\n return {\n jobId: video.id,\n provider: 'openai-compat-video',\n model: video.model || model,\n createdAt: new Date().toISOString(),\n };\n }\n\n /**\n * Poll the status of a video-generation job.\n *\n * The gateway's status response does not include the video bytes, so\n * `result` on a succeeded job carries only metadata (mimeType, duration,\n * dimensions) plus a `url` when the gateway happens to include one\n * directly. Call {@link fetchVideoGenerationResult} to download the bytes.\n */\n async getVideoGenerationJob(\n handle: VideoGenerationJob,\n ): Promise<VideoGenerationStatusResult> {\n const video = await this.request<CompatVideoObject>(\n `/videos/${handle.jobId}`,\n );\n return this.mapVideo(video);\n }\n\n /**\n * Fetch the result of a completed video-generation job, downloading the\n * rendered bytes from the gateway's content endpoint.\n *\n * @throws {AIError} When the job has not succeeded yet\n */\n async fetchVideoGenerationResult(\n handle: VideoGenerationJob,\n ): Promise<VideoGenerationResult> {\n const video = await this.request<CompatVideoObject>(\n `/videos/${handle.jobId}`,\n );\n const status = this.mapVideo(video);\n if (status.status !== 'succeeded') {\n throw new AIError(\n `openai-compat-video job ${handle.jobId} has not succeeded (status: ${status.status})`,\n 'VIDEO_JOB_NOT_READY',\n 'openai-compat-video',\n handle.model,\n );\n }\n\n if (video.url) {\n return status.result as VideoGenerationResult;\n }\n\n const response = await this.rawRequest(`/videos/${handle.jobId}/content`);\n const arrayBuffer = await response.arrayBuffer();\n const { width, height } = parseSize(video.size);\n\n return {\n data: Buffer.from(arrayBuffer),\n mimeType: response.headers.get('content-type') || 'video/mp4',\n durationSeconds: video.seconds ? Number(video.seconds) : undefined,\n width,\n height,\n };\n }\n\n /**\n * Cancel a video-generation job. Cancellation of an in-flight Sora-shaped\n * job is not part of the documented OpenAI `videos` API and is\n * gateway-dependent (e.g. LiteLLM's Veo passthrough may support it); this\n * attempts the conventional `/videos/{id}/cancel` REST call and surfaces\n * an {@link AIError} if the gateway rejects or does not implement it.\n */\n async cancelVideoGenerationJob(handle: VideoGenerationJob): Promise<void> {\n await this.rawRequest(`/videos/${handle.jobId}/cancel`, {\n method: 'POST',\n });\n }\n\n /**\n * Cheap auth-shaped check for video-generation access.\n *\n * This probes `GET /videos/{sentinel-id}` rather than listing videos:\n * LiteLLM (the primary target gateway) requires a `custom_llm_provider`\n * parameter on its `GET /v1/videos` list route that this generic adapter\n * has no context to supply, so a list-based check would fail with valid\n * credentials on the flagship gateway. A single-resource GET works\n * uniformly instead, classified as:\n * - `401`/`403` → invalid credentials, throws {@link AuthenticationError}\n * - `404` → authenticated successfully, the sentinel id just doesn't\n * exist — this is a **valid** result, not a failure\n * - `2xx` → valid (the sentinel id coincidentally exists)\n *\n * Callers on a hot path must cache the result themselves.\n */\n async validateVideoGenerationAccess(): Promise<boolean> {\n const sentinelId = 'have-ai-validate-video-access';\n const response = await this.rawRequest(`/videos/${sentinelId}`, {\n allowNotFound: true,\n });\n return response.ok || response.status === 404;\n }\n\n private mapVideo(video: CompatVideoObject): VideoGenerationStatusResult {\n const { status, rawStatus } = mapStatus(video.status);\n\n if (status === 'failed') {\n return {\n status: 'failed',\n error:\n video.error?.message ||\n `Video generation failed (status: ${video.status})`,\n };\n }\n\n if (status !== 'succeeded') {\n return { status, progress: video.progress, rawStatus };\n }\n\n const { width, height } = parseSize(video.size);\n const result: VideoGenerationResult = {\n url: video.url,\n mimeType: 'video/mp4',\n durationSeconds: video.seconds ? Number(video.seconds) : undefined,\n width,\n height,\n };\n\n return { status: 'succeeded', result };\n }\n\n async getModels(): Promise<AIModel[]> {\n return [\n {\n id: 'sora-2',\n name: 'Sora 2',\n description: 'OpenAI-compatible Sora 2 video-generation model',\n contextLength: 0,\n capabilities: ['video_generation'],\n supportsFunctions: false,\n supportsVision: true,\n },\n {\n id: 'sora-2-pro',\n name: 'Sora 2 Pro',\n description: 'OpenAI-compatible Sora 2 Pro video-generation model',\n contextLength: 0,\n capabilities: ['video_generation'],\n supportsFunctions: false,\n supportsVision: true,\n },\n ];\n }\n\n async getCapabilities(): Promise<AICapabilities> {\n return {\n chat: false,\n completion: false,\n embeddings: false,\n streaming: false,\n functions: false,\n vision: false,\n fineTuning: false,\n imageEmbeddings: false,\n imageGeneration: false,\n videoGeneration: true,\n tts: false,\n voiceCloning: false,\n voiceDesign: false,\n maxContextLength: 0,\n supportedOperations: [\n 'submitVideoGenerationJob',\n 'getVideoGenerationJob',\n 'fetchVideoGenerationResult',\n 'cancelVideoGenerationJob',\n ],\n };\n }\n\n // ============================================================================\n // Text Generation / TTS Methods (Not Implemented - Use another provider)\n // ============================================================================\n\n async chat(\n _messages: AIMessage[],\n _options?: ChatOptions,\n ): Promise<AIResponse> {\n throw new AIError(\n 'Chat is not supported by the openai-compat-video provider. Use the openai or litellm provider for text generation.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async complete(\n _prompt: string,\n _options?: CompletionOptions,\n ): Promise<AIResponse> {\n throw new AIError(\n 'Complete is not supported by the openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async message(_text: string, _options?: MessageOptions): Promise<string> {\n throw new AIError(\n 'Message is not supported by the openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async embed(\n _text: string | string[],\n _options?: EmbeddingOptions,\n ): Promise<EmbeddingResponse> {\n throw new AIError(\n 'Embeddings are not supported by the openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async embedImage(\n _image: string | Buffer,\n _options?: ImageEmbeddingOptions,\n ): Promise<EmbeddingResponse> {\n throw new AIError(\n 'Image embeddings are not supported by the openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async describeImage(\n _image: string | Buffer,\n _prompt?: string,\n _options?: ImageDescriptionOptions,\n ): Promise<string> {\n throw new AIError(\n 'Image description is not supported by the openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async generateImage(\n _prompt: string,\n _options?: ImageGenerationOptions,\n ): Promise<ImageGenerationResponse> {\n throw new AIError(\n 'Image generation is not supported by the openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n stream(\n _messages: AIMessage[],\n _options?: ChatOptions,\n ): AsyncIterable<string> {\n const error = new AIError(\n 'Chat streaming is not supported by the openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n return {\n [Symbol.asyncIterator]: () => ({\n next: () => Promise.reject(error),\n }),\n };\n }\n\n async countTokens(_text: string): Promise<number> {\n throw new AIError(\n 'Token counting is not supported by the openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async synthesizeSpeech(\n _text: string,\n _options?: TTSOptions,\n ): Promise<TTSResponse> {\n throw new AIError(\n 'TTS is not supported by the openai-compat-video provider. Use Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n streamSpeech(_text: string, _options?: TTSOptions): AsyncIterable<Buffer> {\n const error = new AIError(\n 'TTS streaming is not supported by the openai-compat-video provider. Use Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n return {\n [Symbol.asyncIterator]: () => ({\n next: () => Promise.reject(error),\n }),\n };\n }\n\n async cloneVoice(_options: VoiceCloneOptions): Promise<Voice> {\n throw new AIError(\n 'Voice cloning is not supported by the openai-compat-video provider. Use Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async designVoice(_options: VoiceDesignOptions): Promise<Voice> {\n throw new AIError(\n 'Voice design is not supported by the openai-compat-video provider. Use Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n\n async getVoices(_options?: VoiceListOptions): Promise<Voice[]> {\n throw new AIError(\n 'Voice listing is not supported by the openai-compat-video provider. Use Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'openai-compat-video',\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as AIError } from "./types-
|
|
1
|
+
import { t as AIError } from "./types-sSecpd_N.js";
|
|
2
2
|
//#region src/shared/providers/qwen-tts.ts
|
|
3
3
|
/**
|
|
4
4
|
* Rate limiter for controlling request frequency
|
|
@@ -347,4 +347,4 @@ var Qwen3TTSProvider = class {
|
|
|
347
347
|
//#endregion
|
|
348
348
|
export { Qwen3TTSProvider };
|
|
349
349
|
|
|
350
|
-
//# sourceMappingURL=qwen-tts-
|
|
350
|
+
//# sourceMappingURL=qwen-tts-CAh1upii.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qwen-tts-BXjzY7DX.js","names":[],"sources":["../../src/shared/providers/qwen-tts.ts"],"sourcesContent":["/**\n * Qwen3-TTS provider implementation\n *\n * Provides text-to-speech synthesis using Qwen3-TTS models.\n * Supports voice cloning from 3-second audio samples and\n * voice design via natural language descriptions.\n *\n * TTS service is typically co-located with ComfyUI for GPU sharing efficiency.\n *\n * @see https://github.com/QwenLM/Qwen3-TTS\n */\n\nimport type {\n AICapabilities,\n AIInterface,\n AIMessage,\n AIModel,\n AIResponse,\n ChatOptions,\n CompletionOptions,\n EmbeddingOptions,\n EmbeddingResponse,\n ImageDescriptionOptions,\n ImageEmbeddingOptions,\n ImageGenerationOptions,\n ImageGenerationResponse,\n MessageOptions,\n Qwen3TTSOptions,\n TTSOptions,\n TTSResponse,\n VideoGenerationJob,\n VideoGenerationOptions,\n VideoGenerationResult,\n VideoGenerationStatusResult,\n Voice,\n VoiceCloneOptions,\n VoiceDesignOptions,\n VoiceListOptions,\n} from '../types';\nimport { AIError } from '../types';\n\n/**\n * Request body for TTS synthesis\n */\ninterface TTSSynthesizeRequest {\n text: string;\n voice?: string;\n language?: string;\n speed?: number;\n pitch?: number;\n output_format?: 'wav' | 'mp3' | 'ogg';\n include_word_timings?: boolean;\n}\n\n/**\n * Response from TTS synthesis\n */\ninterface TTSSynthesizeResponse {\n audio: string; // Base64 encoded audio\n mime_type: string;\n duration: number;\n sample_rate: number;\n word_timings?: Array<{\n word: string;\n start: number;\n end: number;\n }>;\n}\n\n/**\n * Request body for voice cloning\n */\ninterface VoiceCloneRequest {\n sample_audio: string; // Base64 encoded audio\n sample_mime_type?: string;\n name?: string;\n description?: string;\n language?: string;\n}\n\n/**\n * Request body for voice design\n */\ninterface VoiceDesignRequest {\n description: string;\n language?: string;\n gender?: 'male' | 'female' | 'neutral';\n}\n\n/**\n * Voice response from the API\n */\ninterface VoiceResponse {\n id: string;\n name: string;\n language: string;\n gender?: 'male' | 'female' | 'neutral';\n description?: string;\n is_cloned?: boolean;\n is_designed?: boolean;\n sample_url?: string;\n voice_data?: Record<string, any>;\n}\n\n/**\n * Rate limiter for controlling request frequency\n */\nclass RateLimiter {\n private tokens: number;\n private lastRefill: number;\n private readonly maxTokens: number;\n private readonly refillRate: number; // tokens per second\n private activeRequests: number = 0;\n private readonly maxConcurrent: number;\n\n constructor(options: {\n requestsPerMinute?: number;\n maxConcurrent?: number;\n }) {\n this.maxTokens = options.requestsPerMinute ?? 60;\n this.refillRate = this.maxTokens / 60; // Convert to per-second\n this.tokens = this.maxTokens;\n this.lastRefill = Date.now();\n this.maxConcurrent = options.maxConcurrent ?? 5;\n }\n\n async acquire(): Promise<void> {\n // Refill tokens based on elapsed time\n const now = Date.now();\n const elapsed = (now - this.lastRefill) / 1000;\n this.tokens = Math.min(\n this.maxTokens,\n this.tokens + elapsed * this.refillRate,\n );\n this.lastRefill = now;\n\n // Wait for concurrent slot\n while (this.activeRequests >= this.maxConcurrent) {\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n\n // Wait for token\n while (this.tokens < 1) {\n const waitTime = ((1 - this.tokens) / this.refillRate) * 1000;\n await new Promise((resolve) => setTimeout(resolve, waitTime));\n const elapsed = (Date.now() - this.lastRefill) / 1000;\n this.tokens = Math.min(\n this.maxTokens,\n this.tokens + elapsed * this.refillRate,\n );\n this.lastRefill = Date.now();\n }\n\n this.tokens -= 1;\n this.activeRequests += 1;\n }\n\n release(): void {\n this.activeRequests = Math.max(0, this.activeRequests - 1);\n }\n}\n\n/**\n * Qwen3-TTS provider implementation for text-to-speech synthesis.\n *\n * This provider focuses on TTS capabilities and throws NOT_IMPLEMENTED\n * for text generation methods. Use alongside another provider for full\n * AI capabilities.\n *\n * @example\n * ```typescript\n * import { getAI } from '@happyvertical/ai';\n *\n * // Create TTS-only provider\n * const tts = await getAI({\n * type: 'qwen3-tts',\n * endpoint: 'http://localhost:8880'\n * });\n *\n * // Synthesize speech\n * const result = await tts.synthesizeSpeech('Hello, world!', {\n * voice: 'news-anchor',\n * includeWordTimings: true\n * });\n *\n * // Clone a voice\n * const sample = fs.readFileSync('voice-sample.wav');\n * const voice = await tts.cloneVoice({\n * sampleAudio: sample,\n * name: 'My Custom Voice'\n * });\n * ```\n */\nexport class Qwen3TTSProvider implements AIInterface {\n private options: Qwen3TTSOptions;\n private rateLimiter: RateLimiter;\n\n /**\n * Creates a new Qwen3-TTS provider instance\n * @param options - Configuration options for the TTS provider\n */\n constructor(options: Qwen3TTSOptions) {\n this.options = {\n defaultModel: 'qwen3-tts-1.7b',\n endpoint: 'http://localhost:8880',\n ...options,\n };\n\n this.rateLimiter = new RateLimiter(options.rateLimit ?? {});\n }\n\n /**\n * Get the TTS endpoint URL\n */\n private get endpoint(): string {\n return this.options.endpoint || 'http://localhost:8880';\n }\n\n /**\n * Make an HTTP request to the TTS service\n */\n private async request<T>(\n path: string,\n options: {\n method?: 'GET' | 'POST' | 'DELETE';\n body?: unknown;\n } = {},\n ): Promise<T> {\n await this.rateLimiter.acquire();\n\n try {\n const url = `${this.endpoint}${path}`;\n const response = await fetch(url, {\n method: options.method || 'GET',\n headers: {\n 'Content-Type': 'application/json',\n ...(this.options.headers || {}),\n },\n body: options.body ? JSON.stringify(options.body) : undefined,\n signal: this.options.timeout\n ? AbortSignal.timeout(this.options.timeout)\n : undefined,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new AIError(\n `TTS request to ${url} failed: ${response.status} ${errorText}`,\n 'TTS_REQUEST_FAILED',\n 'qwen3-tts',\n );\n }\n\n return await response.json();\n } finally {\n this.rateLimiter.release();\n }\n }\n\n // ============================================================================\n // TTS Methods (Implemented)\n // ============================================================================\n\n /**\n * Synthesize speech from text\n */\n async synthesizeSpeech(\n text: string,\n options: TTSOptions = {},\n ): Promise<TTSResponse> {\n const request: TTSSynthesizeRequest = {\n text,\n voice: options.voice || this.options.defaultVoice,\n language: options.language || this.options.defaultLanguage,\n speed: options.speed,\n pitch: options.pitch,\n output_format: options.outputFormat,\n include_word_timings: options.includeWordTimings,\n };\n\n const response = await this.request<TTSSynthesizeResponse>(\n '/v1/tts/synthesize',\n {\n method: 'POST',\n body: request,\n },\n );\n\n return {\n audio: Buffer.from(response.audio, 'base64'),\n mimeType: response.mime_type,\n duration: response.duration,\n sampleRate: response.sample_rate,\n model: options.model || this.options.defaultModel,\n wordTimings: response.word_timings?.map((wt) => ({\n word: wt.word,\n start: wt.start,\n end: wt.end,\n })),\n };\n }\n\n /**\n * Stream speech synthesis for real-time playback\n */\n async *streamSpeech(\n text: string,\n options: TTSOptions = {},\n ): AsyncIterable<Buffer> {\n await this.rateLimiter.acquire();\n\n try {\n const request: TTSSynthesizeRequest = {\n text,\n voice: options.voice || this.options.defaultVoice,\n language: options.language || this.options.defaultLanguage,\n speed: options.speed,\n pitch: options.pitch,\n output_format: options.outputFormat,\n };\n\n const url = `${this.endpoint}/v1/tts/stream`;\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(this.options.headers || {}),\n },\n body: JSON.stringify(request),\n signal: this.options.timeout\n ? AbortSignal.timeout(this.options.timeout)\n : undefined,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new AIError(\n `TTS stream request failed: ${response.status} ${errorText}`,\n 'TTS_STREAM_FAILED',\n 'qwen3-tts',\n );\n }\n\n if (!response.body) {\n throw new AIError(\n 'No response body for TTS stream',\n 'TTS_STREAM_NO_BODY',\n 'qwen3-tts',\n );\n }\n\n const reader = response.body.getReader();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n yield Buffer.from(value);\n }\n } finally {\n reader.releaseLock();\n }\n } finally {\n this.rateLimiter.release();\n }\n }\n\n /**\n * Clone a voice from an audio sample\n */\n async cloneVoice(options: VoiceCloneOptions): Promise<Voice> {\n const sampleBase64 =\n typeof options.sampleAudio === 'string'\n ? options.sampleAudio\n : options.sampleAudio.toString('base64');\n\n const request: VoiceCloneRequest = {\n sample_audio: sampleBase64,\n sample_mime_type: options.sampleMimeType,\n name: options.name,\n description: options.description,\n language: options.language,\n };\n\n const response = await this.request<VoiceResponse>('/v1/voices/clone', {\n method: 'POST',\n body: request,\n });\n\n return this.mapVoice(response);\n }\n\n /**\n * Design a voice using natural language description\n */\n async designVoice(options: VoiceDesignOptions): Promise<Voice> {\n const request: VoiceDesignRequest = {\n description: options.description,\n language: options.language,\n gender: options.gender,\n };\n\n const response = await this.request<VoiceResponse>('/v1/voices/design', {\n method: 'POST',\n body: request,\n });\n\n return this.mapVoice(response);\n }\n\n /**\n * List available voices\n */\n async getVoices(options: VoiceListOptions = {}): Promise<Voice[]> {\n const params = new URLSearchParams();\n if (options.language) params.set('language', options.language);\n if (options.gender) params.set('gender', options.gender);\n if (options.includeCloned !== undefined)\n params.set('include_cloned', String(options.includeCloned));\n if (options.includeDesigned !== undefined)\n params.set('include_designed', String(options.includeDesigned));\n\n const queryString = params.toString();\n const path = queryString ? `/v1/voices?${queryString}` : '/v1/voices';\n\n const response = await this.request<{ voices: VoiceResponse[] }>(path);\n return response.voices.map((v) => this.mapVoice(v));\n }\n\n /**\n * Map API voice response to Voice interface\n */\n private mapVoice(response: VoiceResponse): Voice {\n return {\n id: response.id,\n name: response.name,\n language: response.language,\n gender: response.gender,\n description: response.description,\n isCloned: response.is_cloned,\n isDesigned: response.is_designed,\n sampleUrl: response.sample_url,\n voiceData: response.voice_data,\n };\n }\n\n // ============================================================================\n // Text Generation Methods (Not Implemented - Use another provider)\n // ============================================================================\n\n async chat(\n _messages: AIMessage[],\n _options?: ChatOptions,\n ): Promise<AIResponse> {\n throw new AIError(\n 'Chat is not supported by Qwen3-TTS provider. Use OpenAI, Anthropic, or another provider for text generation.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async complete(\n _prompt: string,\n _options?: CompletionOptions,\n ): Promise<AIResponse> {\n throw new AIError(\n 'Complete is not supported by Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async message(_text: string, _options?: MessageOptions): Promise<string> {\n throw new AIError(\n 'Message is not supported by Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async embed(\n _text: string | string[],\n _options?: EmbeddingOptions,\n ): Promise<EmbeddingResponse> {\n throw new AIError(\n 'Embeddings are not supported by Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async embedImage(\n _image: string | Buffer,\n _options?: ImageEmbeddingOptions,\n ): Promise<EmbeddingResponse> {\n throw new AIError(\n 'Image embeddings are not supported by Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async describeImage(\n _image: string | Buffer,\n _prompt?: string,\n _options?: ImageDescriptionOptions,\n ): Promise<string> {\n throw new AIError(\n 'Image description is not supported by Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async generateImage(\n _prompt: string,\n _options?: ImageGenerationOptions,\n ): Promise<ImageGenerationResponse> {\n throw new AIError(\n 'Image generation is not supported by Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async submitVideoGenerationJob(\n _options: VideoGenerationOptions,\n ): Promise<VideoGenerationJob> {\n throw new AIError(\n 'Video generation is not supported by Qwen3-TTS provider. Use gemini, byteplus-modelark, or openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async getVideoGenerationJob(\n _handle: VideoGenerationJob,\n ): Promise<VideoGenerationStatusResult> {\n throw new AIError(\n 'Video generation is not supported by Qwen3-TTS provider. Use gemini, byteplus-modelark, or openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async fetchVideoGenerationResult(\n _handle: VideoGenerationJob,\n ): Promise<VideoGenerationResult> {\n throw new AIError(\n 'Video generation is not supported by Qwen3-TTS provider. Use gemini, byteplus-modelark, or openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async cancelVideoGenerationJob(_handle: VideoGenerationJob): Promise<void> {\n throw new AIError(\n 'Video generation is not supported by Qwen3-TTS provider. Use gemini, byteplus-modelark, or openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async validateVideoGenerationAccess(): Promise<boolean> {\n throw new AIError(\n 'Video generation is not supported by Qwen3-TTS provider. Use gemini, byteplus-modelark, or openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n stream(\n _messages: AIMessage[],\n _options?: ChatOptions,\n ): AsyncIterable<string> {\n const error = new AIError(\n 'Chat streaming is not supported by Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n return {\n [Symbol.asyncIterator]: () => ({\n next: () => Promise.reject(error),\n }),\n };\n }\n\n async countTokens(_text: string): Promise<number> {\n throw new AIError(\n 'Token counting is not supported by Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async getModels(): Promise<AIModel[]> {\n return [\n {\n id: 'qwen3-tts-1.7b',\n name: 'Qwen3-TTS 1.7B',\n description: 'Higher quality TTS model (4.54GB VRAM)',\n contextLength: 0,\n capabilities: ['tts', 'voice-cloning', 'voice-design'],\n supportsFunctions: false,\n supportsVision: false,\n },\n {\n id: 'qwen3-tts-0.6b',\n name: 'Qwen3-TTS 0.6B',\n description: 'Faster TTS model with lower VRAM (2.52GB)',\n contextLength: 0,\n capabilities: ['tts', 'voice-cloning', 'voice-design'],\n supportsFunctions: false,\n supportsVision: false,\n },\n ];\n }\n\n async getCapabilities(): Promise<AICapabilities> {\n return {\n chat: false,\n completion: false,\n embeddings: false,\n streaming: false,\n functions: false,\n vision: false,\n fineTuning: false,\n imageEmbeddings: false,\n imageGeneration: false,\n videoGeneration: false,\n tts: true,\n voiceCloning: true,\n voiceDesign: true,\n maxContextLength: 0,\n supportedOperations: [\n 'synthesizeSpeech',\n 'streamSpeech',\n 'cloneVoice',\n 'designVoice',\n 'getVoices',\n ],\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"qwen-tts-CAh1upii.js","names":[],"sources":["../../src/shared/providers/qwen-tts.ts"],"sourcesContent":["/**\n * Qwen3-TTS provider implementation\n *\n * Provides text-to-speech synthesis using Qwen3-TTS models.\n * Supports voice cloning from 3-second audio samples and\n * voice design via natural language descriptions.\n *\n * TTS service is typically co-located with ComfyUI for GPU sharing efficiency.\n *\n * @see https://github.com/QwenLM/Qwen3-TTS\n */\n\nimport type {\n AICapabilities,\n AIInterface,\n AIMessage,\n AIModel,\n AIResponse,\n ChatOptions,\n CompletionOptions,\n EmbeddingOptions,\n EmbeddingResponse,\n ImageDescriptionOptions,\n ImageEmbeddingOptions,\n ImageGenerationOptions,\n ImageGenerationResponse,\n MessageOptions,\n Qwen3TTSOptions,\n TTSOptions,\n TTSResponse,\n VideoGenerationJob,\n VideoGenerationOptions,\n VideoGenerationResult,\n VideoGenerationStatusResult,\n Voice,\n VoiceCloneOptions,\n VoiceDesignOptions,\n VoiceListOptions,\n} from '../types';\nimport { AIError } from '../types';\n\n/**\n * Request body for TTS synthesis\n */\ninterface TTSSynthesizeRequest {\n text: string;\n voice?: string;\n language?: string;\n speed?: number;\n pitch?: number;\n output_format?: 'wav' | 'mp3' | 'ogg';\n include_word_timings?: boolean;\n}\n\n/**\n * Response from TTS synthesis\n */\ninterface TTSSynthesizeResponse {\n audio: string; // Base64 encoded audio\n mime_type: string;\n duration: number;\n sample_rate: number;\n word_timings?: Array<{\n word: string;\n start: number;\n end: number;\n }>;\n}\n\n/**\n * Request body for voice cloning\n */\ninterface VoiceCloneRequest {\n sample_audio: string; // Base64 encoded audio\n sample_mime_type?: string;\n name?: string;\n description?: string;\n language?: string;\n}\n\n/**\n * Request body for voice design\n */\ninterface VoiceDesignRequest {\n description: string;\n language?: string;\n gender?: 'male' | 'female' | 'neutral';\n}\n\n/**\n * Voice response from the API\n */\ninterface VoiceResponse {\n id: string;\n name: string;\n language: string;\n gender?: 'male' | 'female' | 'neutral';\n description?: string;\n is_cloned?: boolean;\n is_designed?: boolean;\n sample_url?: string;\n voice_data?: Record<string, any>;\n}\n\n/**\n * Rate limiter for controlling request frequency\n */\nclass RateLimiter {\n private tokens: number;\n private lastRefill: number;\n private readonly maxTokens: number;\n private readonly refillRate: number; // tokens per second\n private activeRequests: number = 0;\n private readonly maxConcurrent: number;\n\n constructor(options: {\n requestsPerMinute?: number;\n maxConcurrent?: number;\n }) {\n this.maxTokens = options.requestsPerMinute ?? 60;\n this.refillRate = this.maxTokens / 60; // Convert to per-second\n this.tokens = this.maxTokens;\n this.lastRefill = Date.now();\n this.maxConcurrent = options.maxConcurrent ?? 5;\n }\n\n async acquire(): Promise<void> {\n // Refill tokens based on elapsed time\n const now = Date.now();\n const elapsed = (now - this.lastRefill) / 1000;\n this.tokens = Math.min(\n this.maxTokens,\n this.tokens + elapsed * this.refillRate,\n );\n this.lastRefill = now;\n\n // Wait for concurrent slot\n while (this.activeRequests >= this.maxConcurrent) {\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n\n // Wait for token\n while (this.tokens < 1) {\n const waitTime = ((1 - this.tokens) / this.refillRate) * 1000;\n await new Promise((resolve) => setTimeout(resolve, waitTime));\n const elapsed = (Date.now() - this.lastRefill) / 1000;\n this.tokens = Math.min(\n this.maxTokens,\n this.tokens + elapsed * this.refillRate,\n );\n this.lastRefill = Date.now();\n }\n\n this.tokens -= 1;\n this.activeRequests += 1;\n }\n\n release(): void {\n this.activeRequests = Math.max(0, this.activeRequests - 1);\n }\n}\n\n/**\n * Qwen3-TTS provider implementation for text-to-speech synthesis.\n *\n * This provider focuses on TTS capabilities and throws NOT_IMPLEMENTED\n * for text generation methods. Use alongside another provider for full\n * AI capabilities.\n *\n * @example\n * ```typescript\n * import { getAI } from '@happyvertical/ai';\n *\n * // Create TTS-only provider\n * const tts = await getAI({\n * type: 'qwen3-tts',\n * endpoint: 'http://localhost:8880'\n * });\n *\n * // Synthesize speech\n * const result = await tts.synthesizeSpeech('Hello, world!', {\n * voice: 'news-anchor',\n * includeWordTimings: true\n * });\n *\n * // Clone a voice\n * const sample = fs.readFileSync('voice-sample.wav');\n * const voice = await tts.cloneVoice({\n * sampleAudio: sample,\n * name: 'My Custom Voice'\n * });\n * ```\n */\nexport class Qwen3TTSProvider implements AIInterface {\n private options: Qwen3TTSOptions;\n private rateLimiter: RateLimiter;\n\n /**\n * Creates a new Qwen3-TTS provider instance\n * @param options - Configuration options for the TTS provider\n */\n constructor(options: Qwen3TTSOptions) {\n this.options = {\n defaultModel: 'qwen3-tts-1.7b',\n endpoint: 'http://localhost:8880',\n ...options,\n };\n\n this.rateLimiter = new RateLimiter(options.rateLimit ?? {});\n }\n\n /**\n * Get the TTS endpoint URL\n */\n private get endpoint(): string {\n return this.options.endpoint || 'http://localhost:8880';\n }\n\n /**\n * Make an HTTP request to the TTS service\n */\n private async request<T>(\n path: string,\n options: {\n method?: 'GET' | 'POST' | 'DELETE';\n body?: unknown;\n } = {},\n ): Promise<T> {\n await this.rateLimiter.acquire();\n\n try {\n const url = `${this.endpoint}${path}`;\n const response = await fetch(url, {\n method: options.method || 'GET',\n headers: {\n 'Content-Type': 'application/json',\n ...(this.options.headers || {}),\n },\n body: options.body ? JSON.stringify(options.body) : undefined,\n signal: this.options.timeout\n ? AbortSignal.timeout(this.options.timeout)\n : undefined,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new AIError(\n `TTS request to ${url} failed: ${response.status} ${errorText}`,\n 'TTS_REQUEST_FAILED',\n 'qwen3-tts',\n );\n }\n\n return await response.json();\n } finally {\n this.rateLimiter.release();\n }\n }\n\n // ============================================================================\n // TTS Methods (Implemented)\n // ============================================================================\n\n /**\n * Synthesize speech from text\n */\n async synthesizeSpeech(\n text: string,\n options: TTSOptions = {},\n ): Promise<TTSResponse> {\n const request: TTSSynthesizeRequest = {\n text,\n voice: options.voice || this.options.defaultVoice,\n language: options.language || this.options.defaultLanguage,\n speed: options.speed,\n pitch: options.pitch,\n output_format: options.outputFormat,\n include_word_timings: options.includeWordTimings,\n };\n\n const response = await this.request<TTSSynthesizeResponse>(\n '/v1/tts/synthesize',\n {\n method: 'POST',\n body: request,\n },\n );\n\n return {\n audio: Buffer.from(response.audio, 'base64'),\n mimeType: response.mime_type,\n duration: response.duration,\n sampleRate: response.sample_rate,\n model: options.model || this.options.defaultModel,\n wordTimings: response.word_timings?.map((wt) => ({\n word: wt.word,\n start: wt.start,\n end: wt.end,\n })),\n };\n }\n\n /**\n * Stream speech synthesis for real-time playback\n */\n async *streamSpeech(\n text: string,\n options: TTSOptions = {},\n ): AsyncIterable<Buffer> {\n await this.rateLimiter.acquire();\n\n try {\n const request: TTSSynthesizeRequest = {\n text,\n voice: options.voice || this.options.defaultVoice,\n language: options.language || this.options.defaultLanguage,\n speed: options.speed,\n pitch: options.pitch,\n output_format: options.outputFormat,\n };\n\n const url = `${this.endpoint}/v1/tts/stream`;\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(this.options.headers || {}),\n },\n body: JSON.stringify(request),\n signal: this.options.timeout\n ? AbortSignal.timeout(this.options.timeout)\n : undefined,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new AIError(\n `TTS stream request failed: ${response.status} ${errorText}`,\n 'TTS_STREAM_FAILED',\n 'qwen3-tts',\n );\n }\n\n if (!response.body) {\n throw new AIError(\n 'No response body for TTS stream',\n 'TTS_STREAM_NO_BODY',\n 'qwen3-tts',\n );\n }\n\n const reader = response.body.getReader();\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n yield Buffer.from(value);\n }\n } finally {\n reader.releaseLock();\n }\n } finally {\n this.rateLimiter.release();\n }\n }\n\n /**\n * Clone a voice from an audio sample\n */\n async cloneVoice(options: VoiceCloneOptions): Promise<Voice> {\n const sampleBase64 =\n typeof options.sampleAudio === 'string'\n ? options.sampleAudio\n : options.sampleAudio.toString('base64');\n\n const request: VoiceCloneRequest = {\n sample_audio: sampleBase64,\n sample_mime_type: options.sampleMimeType,\n name: options.name,\n description: options.description,\n language: options.language,\n };\n\n const response = await this.request<VoiceResponse>('/v1/voices/clone', {\n method: 'POST',\n body: request,\n });\n\n return this.mapVoice(response);\n }\n\n /**\n * Design a voice using natural language description\n */\n async designVoice(options: VoiceDesignOptions): Promise<Voice> {\n const request: VoiceDesignRequest = {\n description: options.description,\n language: options.language,\n gender: options.gender,\n };\n\n const response = await this.request<VoiceResponse>('/v1/voices/design', {\n method: 'POST',\n body: request,\n });\n\n return this.mapVoice(response);\n }\n\n /**\n * List available voices\n */\n async getVoices(options: VoiceListOptions = {}): Promise<Voice[]> {\n const params = new URLSearchParams();\n if (options.language) params.set('language', options.language);\n if (options.gender) params.set('gender', options.gender);\n if (options.includeCloned !== undefined)\n params.set('include_cloned', String(options.includeCloned));\n if (options.includeDesigned !== undefined)\n params.set('include_designed', String(options.includeDesigned));\n\n const queryString = params.toString();\n const path = queryString ? `/v1/voices?${queryString}` : '/v1/voices';\n\n const response = await this.request<{ voices: VoiceResponse[] }>(path);\n return response.voices.map((v) => this.mapVoice(v));\n }\n\n /**\n * Map API voice response to Voice interface\n */\n private mapVoice(response: VoiceResponse): Voice {\n return {\n id: response.id,\n name: response.name,\n language: response.language,\n gender: response.gender,\n description: response.description,\n isCloned: response.is_cloned,\n isDesigned: response.is_designed,\n sampleUrl: response.sample_url,\n voiceData: response.voice_data,\n };\n }\n\n // ============================================================================\n // Text Generation Methods (Not Implemented - Use another provider)\n // ============================================================================\n\n async chat(\n _messages: AIMessage[],\n _options?: ChatOptions,\n ): Promise<AIResponse> {\n throw new AIError(\n 'Chat is not supported by Qwen3-TTS provider. Use OpenAI, Anthropic, or another provider for text generation.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async complete(\n _prompt: string,\n _options?: CompletionOptions,\n ): Promise<AIResponse> {\n throw new AIError(\n 'Complete is not supported by Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async message(_text: string, _options?: MessageOptions): Promise<string> {\n throw new AIError(\n 'Message is not supported by Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async embed(\n _text: string | string[],\n _options?: EmbeddingOptions,\n ): Promise<EmbeddingResponse> {\n throw new AIError(\n 'Embeddings are not supported by Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async embedImage(\n _image: string | Buffer,\n _options?: ImageEmbeddingOptions,\n ): Promise<EmbeddingResponse> {\n throw new AIError(\n 'Image embeddings are not supported by Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async describeImage(\n _image: string | Buffer,\n _prompt?: string,\n _options?: ImageDescriptionOptions,\n ): Promise<string> {\n throw new AIError(\n 'Image description is not supported by Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async generateImage(\n _prompt: string,\n _options?: ImageGenerationOptions,\n ): Promise<ImageGenerationResponse> {\n throw new AIError(\n 'Image generation is not supported by Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async submitVideoGenerationJob(\n _options: VideoGenerationOptions,\n ): Promise<VideoGenerationJob> {\n throw new AIError(\n 'Video generation is not supported by Qwen3-TTS provider. Use gemini, byteplus-modelark, or openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async getVideoGenerationJob(\n _handle: VideoGenerationJob,\n ): Promise<VideoGenerationStatusResult> {\n throw new AIError(\n 'Video generation is not supported by Qwen3-TTS provider. Use gemini, byteplus-modelark, or openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async fetchVideoGenerationResult(\n _handle: VideoGenerationJob,\n ): Promise<VideoGenerationResult> {\n throw new AIError(\n 'Video generation is not supported by Qwen3-TTS provider. Use gemini, byteplus-modelark, or openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async cancelVideoGenerationJob(_handle: VideoGenerationJob): Promise<void> {\n throw new AIError(\n 'Video generation is not supported by Qwen3-TTS provider. Use gemini, byteplus-modelark, or openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async validateVideoGenerationAccess(): Promise<boolean> {\n throw new AIError(\n 'Video generation is not supported by Qwen3-TTS provider. Use gemini, byteplus-modelark, or openai-compat-video provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n stream(\n _messages: AIMessage[],\n _options?: ChatOptions,\n ): AsyncIterable<string> {\n const error = new AIError(\n 'Chat streaming is not supported by Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n return {\n [Symbol.asyncIterator]: () => ({\n next: () => Promise.reject(error),\n }),\n };\n }\n\n async countTokens(_text: string): Promise<number> {\n throw new AIError(\n 'Token counting is not supported by Qwen3-TTS provider.',\n 'NOT_IMPLEMENTED',\n 'qwen3-tts',\n );\n }\n\n async getModels(): Promise<AIModel[]> {\n return [\n {\n id: 'qwen3-tts-1.7b',\n name: 'Qwen3-TTS 1.7B',\n description: 'Higher quality TTS model (4.54GB VRAM)',\n contextLength: 0,\n capabilities: ['tts', 'voice-cloning', 'voice-design'],\n supportsFunctions: false,\n supportsVision: false,\n },\n {\n id: 'qwen3-tts-0.6b',\n name: 'Qwen3-TTS 0.6B',\n description: 'Faster TTS model with lower VRAM (2.52GB)',\n contextLength: 0,\n capabilities: ['tts', 'voice-cloning', 'voice-design'],\n supportsFunctions: false,\n supportsVision: false,\n },\n ];\n }\n\n async getCapabilities(): Promise<AICapabilities> {\n return {\n chat: false,\n completion: false,\n embeddings: false,\n streaming: false,\n functions: false,\n vision: false,\n fineTuning: false,\n imageEmbeddings: false,\n imageGeneration: false,\n videoGeneration: false,\n tts: true,\n voiceCloning: true,\n voiceDesign: true,\n maxContextLength: 0,\n supportedOperations: [\n 'synthesizeSpeech',\n 'streamSpeech',\n 'cloneVoice',\n 'designVoice',\n 'getVoices',\n ],\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { s as RateLimitError } from "./types-
|
|
1
|
+
import { s as RateLimitError } from "./types-sSecpd_N.js";
|
|
2
2
|
//#region src/shared/rate-limit.ts
|
|
3
3
|
var RATE_LIMITED_METHODS = /* @__PURE__ */ new Set([
|
|
4
4
|
"chat",
|
|
@@ -125,7 +125,7 @@ function getRetryDelayMs(error, config) {
|
|
|
125
125
|
if (hintedDelayMs !== void 0) return Math.max(config.cooldownMs, hintedDelayMs);
|
|
126
126
|
return Math.max(config.cooldownMs, config.initialDelayMs);
|
|
127
127
|
}
|
|
128
|
-
async function invokeWithPacing(execute, coordinator, config) {
|
|
128
|
+
async function invokeWithPacing(execute, coordinator, config, allowRetry = true) {
|
|
129
129
|
let attempt = 1;
|
|
130
130
|
while (true) {
|
|
131
131
|
await coordinator.waitUntilReady();
|
|
@@ -136,7 +136,7 @@ async function invokeWithPacing(execute, coordinator, config) {
|
|
|
136
136
|
} catch (error) {
|
|
137
137
|
if (error instanceof RateLimitError) {
|
|
138
138
|
coordinator.delayFor(getRetryDelayMs(error, config));
|
|
139
|
-
if (error.retryable && attempt < config.maxAttempts) {
|
|
139
|
+
if (allowRetry && error.retryable && attempt < config.maxAttempts) {
|
|
140
140
|
attempt += 1;
|
|
141
141
|
continue;
|
|
142
142
|
}
|
|
@@ -180,6 +180,7 @@ function createRateLimitedAI(client, options) {
|
|
|
180
180
|
const config = normalizeRateLimitConfig(options);
|
|
181
181
|
if (!config) return client;
|
|
182
182
|
const wrappedMethods = /* @__PURE__ */ new Map();
|
|
183
|
+
const allowSeevioSubmitRetry = options.type !== "seevio";
|
|
183
184
|
return new Proxy(client, { get(target, property, receiver) {
|
|
184
185
|
const value = Reflect.get(target, property, receiver);
|
|
185
186
|
if (typeof value !== "function") return value;
|
|
@@ -189,7 +190,7 @@ function createRateLimitedAI(client, options) {
|
|
|
189
190
|
}
|
|
190
191
|
if (!wrappedMethods.has(property)) wrappedMethods.set(property, (...args) => {
|
|
191
192
|
const coordinator = getBudgetCoordinator(config.key);
|
|
192
|
-
return coordinator.schedule(() => invokeWithPacing(() => Reflect.apply(value, target, args), coordinator, config));
|
|
193
|
+
return coordinator.schedule(() => invokeWithPacing(() => Reflect.apply(value, target, args), coordinator, config, property !== "submitVideoGenerationJob" || allowSeevioSubmitRetry));
|
|
193
194
|
});
|
|
194
195
|
return wrappedMethods.get(property);
|
|
195
196
|
} });
|
|
@@ -197,4 +198,4 @@ function createRateLimitedAI(client, options) {
|
|
|
197
198
|
//#endregion
|
|
198
199
|
export { extractRetryAfterSeconds as n, createRateLimitedAI as t };
|
|
199
200
|
|
|
200
|
-
//# sourceMappingURL=rate-limit-
|
|
201
|
+
//# sourceMappingURL=rate-limit-C2oPwLGI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit-C2oPwLGI.js","names":[],"sources":["../../src/shared/rate-limit.ts"],"sourcesContent":["import type { AIClientOptions } from './client';\nimport type { AIInterface, AIRateLimitOptions, GetAIOptions } from './types';\nimport { AIError, RateLimitError } from './types';\n\nconst RATE_LIMITED_METHODS = new Set<keyof AIInterface>([\n 'chat',\n 'complete',\n 'message',\n 'embed',\n 'embedImage',\n 'describeImage',\n 'generateImage',\n 'getModels',\n 'synthesizeSpeech',\n 'cloneVoice',\n 'designVoice',\n 'getVoices',\n 'submitVideoGenerationJob',\n]);\n\nconst MAX_BUDGET_COORDINATORS = 128;\nconst BUDGET_COORDINATOR_TTL_MS = 15 * 60 * 1000;\n\ninterface NormalizedRateLimitConfig {\n cooldownMs: number;\n initialDelayMs: number;\n key: string;\n maxAttempts: number;\n}\n\nclass BudgetCoordinator {\n private nextAvailableAt = 0;\n private pendingSchedules = 0;\n private tail: Promise<void> = Promise.resolve();\n private lastUsedAt = Date.now();\n\n touch(): void {\n this.lastUsedAt = Date.now();\n }\n\n schedule<T>(work: () => Promise<T>): Promise<T> {\n this.pendingSchedules += 1;\n this.touch();\n\n const run = this.tail.then(work, work);\n this.tail = run.then(\n () => undefined,\n () => undefined,\n );\n\n return run.finally(() => {\n this.pendingSchedules = Math.max(0, this.pendingSchedules - 1);\n this.touch();\n });\n }\n\n async waitUntilReady(): Promise<void> {\n this.touch();\n const delayMs = this.nextAvailableAt - Date.now();\n if (delayMs > 0) {\n await sleep(delayMs);\n }\n }\n\n delayFor(delayMs: number): void {\n this.touch();\n if (delayMs <= 0) {\n return;\n }\n\n this.nextAvailableAt = Math.max(this.nextAvailableAt, Date.now() + delayMs);\n }\n\n isEvictable(now: number): boolean {\n return this.pendingSchedules === 0 && now >= this.nextAvailableAt;\n }\n\n isExpired(now: number): boolean {\n return (\n this.isEvictable(now) &&\n now - this.lastUsedAt >= BUDGET_COORDINATOR_TTL_MS\n );\n }\n\n getLastUsedAt(): number {\n return this.lastUsedAt;\n }\n}\n\nconst budgetCoordinators = new Map<string, BudgetCoordinator>();\n\nfunction sleep(delayMs: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, delayMs);\n });\n}\n\nfunction pruneBudgetCoordinators(): void {\n const now = Date.now();\n\n for (const [key, coordinator] of budgetCoordinators.entries()) {\n if (coordinator.isExpired(now)) {\n budgetCoordinators.delete(key);\n }\n }\n\n while (budgetCoordinators.size > MAX_BUDGET_COORDINATORS) {\n const evictableEntries = [...budgetCoordinators.entries()]\n .filter(([, coordinator]) => coordinator.isEvictable(now))\n .sort(\n ([, leftCoordinator], [, rightCoordinator]) =>\n leftCoordinator.getLastUsedAt() - rightCoordinator.getLastUsedAt(),\n );\n\n if (evictableEntries.length === 0) {\n break;\n }\n\n budgetCoordinators.delete(evictableEntries[0][0]);\n }\n}\n\nfunction getBudgetCoordinator(key: string): BudgetCoordinator {\n pruneBudgetCoordinators();\n\n let coordinator = budgetCoordinators.get(key);\n if (!coordinator) {\n coordinator = new BudgetCoordinator();\n budgetCoordinators.set(key, coordinator);\n pruneBudgetCoordinators();\n }\n\n coordinator.touch();\n return coordinator;\n}\n\nfunction hasPacingConfig(rateLimit?: AIRateLimitOptions): boolean {\n if (!rateLimit || rateLimit.enabled === false) {\n return false;\n }\n\n return (\n rateLimit.enabled === true ||\n rateLimit.key !== undefined ||\n rateLimit.cooldownMs !== undefined ||\n rateLimit.initialDelayMs !== undefined ||\n rateLimit.maxAttempts !== undefined\n );\n}\n\nfunction normalizeNonNegativeInteger(\n value: number | undefined,\n fallback: number,\n): number {\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n return fallback;\n }\n\n return Math.max(0, Math.trunc(value));\n}\n\nfunction hashKey(value: string): string {\n let hash = 2166136261;\n\n for (let index = 0; index < value.length; index += 1) {\n hash ^= value.charCodeAt(index);\n hash = Math.imul(hash, 16777619);\n }\n\n return (hash >>> 0).toString(36);\n}\n\nfunction deriveBudgetKey(options: GetAIOptions | AIClientOptions): string {\n const type = (options as { type?: string }).type;\n const provider = typeof type === 'string' && type ? type : 'openai';\n\n const credentialLikeValues = [\n 'apiKey' in options ? options.apiKey : undefined,\n 'apiToken' in options ? options.apiToken : undefined,\n 'credentials' in options ? options.credentials?.accessKeyId : undefined,\n 'endpoint' in options ? options.endpoint : undefined,\n 'baseUrl' in options ? options.baseUrl : undefined,\n 'cliPath' in options ? options.cliPath : undefined,\n ];\n\n const seed = credentialLikeValues.find(\n (value): value is string => typeof value === 'string' && value.length > 0,\n );\n\n return seed ? `${provider}:${hashKey(seed)}` : `${provider}:default`;\n}\n\nfunction normalizeRateLimitConfig(\n options: GetAIOptions | AIClientOptions,\n): NormalizedRateLimitConfig | null {\n const rateLimit = 'rateLimit' in options ? options.rateLimit : undefined;\n\n if (!hasPacingConfig(rateLimit)) {\n return null;\n }\n\n return {\n cooldownMs: normalizeNonNegativeInteger(rateLimit?.cooldownMs, 0),\n initialDelayMs: normalizeNonNegativeInteger(\n rateLimit?.initialDelayMs,\n 5000,\n ),\n key: rateLimit?.key?.trim() || deriveBudgetKey(options),\n maxAttempts: Math.max(\n 1,\n normalizeNonNegativeInteger(rateLimit?.maxAttempts, 1),\n ),\n };\n}\n\nfunction getRetryDelayMs(\n error: RateLimitError,\n config: NormalizedRateLimitConfig,\n): number {\n const hintedDelayMs =\n typeof error.retryAfter === 'number' && Number.isFinite(error.retryAfter)\n ? Math.max(0, Math.ceil(error.retryAfter * 1000))\n : undefined;\n\n if (hintedDelayMs !== undefined) {\n return Math.max(config.cooldownMs, hintedDelayMs);\n }\n\n return Math.max(config.cooldownMs, config.initialDelayMs);\n}\n\nasync function invokeWithPacing<T>(\n execute: () => Promise<T>,\n coordinator: BudgetCoordinator,\n config: NormalizedRateLimitConfig,\n allowRetry = true,\n): Promise<T> {\n let attempt = 1;\n\n while (true) {\n await coordinator.waitUntilReady();\n\n try {\n const result = await execute();\n coordinator.delayFor(config.cooldownMs);\n return result;\n } catch (error) {\n if (error instanceof RateLimitError) {\n coordinator.delayFor(getRetryDelayMs(error, config));\n\n if (allowRetry && error.retryable && attempt < config.maxAttempts) {\n attempt += 1;\n continue;\n }\n }\n\n throw error;\n }\n }\n}\n\nexport function parseRetryAfterSeconds(\n retryAfter: number | string | null | undefined,\n): number | undefined {\n if (typeof retryAfter === 'number' && Number.isFinite(retryAfter)) {\n return Math.max(0, retryAfter);\n }\n\n if (typeof retryAfter !== 'string') {\n return undefined;\n }\n\n const trimmed = retryAfter.trim();\n if (!trimmed) {\n return undefined;\n }\n\n const seconds = Number.parseFloat(trimmed);\n if (!Number.isNaN(seconds)) {\n return Math.max(0, seconds);\n }\n\n const retryAt = Date.parse(trimmed);\n if (Number.isNaN(retryAt)) {\n return undefined;\n }\n\n return Math.max(0, Math.ceil((retryAt - Date.now()) / 1000));\n}\n\nfunction getHeaderValue(\n headers: unknown,\n headerName: string,\n): number | string | undefined {\n if (!headers || typeof headers !== 'object') {\n return undefined;\n }\n\n if ('get' in headers && typeof headers.get === 'function') {\n return headers.get(headerName) ?? headers.get(headerName.toLowerCase());\n }\n\n const objectHeaders = headers as Record<string, number | string | undefined>;\n return objectHeaders[headerName] ?? objectHeaders[headerName.toLowerCase()];\n}\n\nexport function extractRetryAfterSeconds(error: unknown): number | undefined {\n if (!error || typeof error !== 'object') {\n return undefined;\n }\n\n if ('retryAfter' in error) {\n const retryAfter = parseRetryAfterSeconds(\n (error as { retryAfter?: number | string | null }).retryAfter,\n );\n if (retryAfter !== undefined) {\n return retryAfter;\n }\n }\n\n if ('headers' in error) {\n const retryAfter = parseRetryAfterSeconds(\n getHeaderValue((error as { headers?: unknown }).headers, 'retry-after'),\n );\n if (retryAfter !== undefined) {\n return retryAfter;\n }\n }\n\n const message =\n 'message' in error && typeof error.message === 'string'\n ? error.message\n : '';\n\n const retryAfterMatch =\n message.match(/retry after\\s+(\\d+(?:\\.\\d+)?)\\s*s?/i) ??\n message.match(/retryDelay[^\\d]*(\\d+(?:\\.\\d+)?)s/i);\n\n return retryAfterMatch\n ? parseRetryAfterSeconds(retryAfterMatch[1])\n : undefined;\n}\n\nexport function createRateLimitedAI<T extends AIInterface>(\n client: T,\n options: GetAIOptions | AIClientOptions,\n): T {\n const config = normalizeRateLimitConfig(options);\n if (!config) {\n return client;\n }\n\n const wrappedMethods = new Map<PropertyKey, unknown>();\n // Seevio does not document idempotency for billed video-task submission.\n // Existing providers retain their established pacing-retry behavior.\n const allowSeevioSubmitRetry = options.type !== 'seevio';\n\n return new Proxy(client, {\n get(target, property, receiver) {\n const value = Reflect.get(target, property, receiver);\n\n if (typeof value !== 'function') {\n return value;\n }\n\n if (!RATE_LIMITED_METHODS.has(property as keyof AIInterface)) {\n if (!wrappedMethods.has(property)) {\n wrappedMethods.set(property, value.bind(target));\n }\n\n return wrappedMethods.get(property);\n }\n\n if (!wrappedMethods.has(property)) {\n wrappedMethods.set(property, (...args: unknown[]) => {\n const coordinator = getBudgetCoordinator(config.key);\n return coordinator.schedule(() =>\n invokeWithPacing(\n () => Reflect.apply(value, target, args) as Promise<unknown>,\n coordinator,\n config,\n property !== 'submitVideoGenerationJob' || allowSeevioSubmitRetry,\n ),\n );\n });\n }\n\n return wrappedMethods.get(property);\n },\n }) as T;\n}\n\nexport function __resetAIRateLimitStateForTests(): void {\n budgetCoordinators.clear();\n}\n\nexport function __getAIRateLimitStateForTests(): {\n count: number;\n maxBudgetCoordinators: number;\n ttlMs: number;\n} {\n return {\n count: budgetCoordinators.size,\n maxBudgetCoordinators: MAX_BUDGET_COORDINATORS,\n ttlMs: BUDGET_COORDINATOR_TTL_MS,\n };\n}\n\nexport function isRetryableAIError(error: unknown): error is AIError {\n return error instanceof AIError && error.retryable;\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as AIError } from "./types-
|
|
1
|
+
import { t as AIError } from "./types-sSecpd_N.js";
|
|
2
2
|
//#region src/shared/safety.ts
|
|
3
3
|
var DEFAULT_AI_TIMEOUT_MS = 12e4;
|
|
4
4
|
var DEFAULT_AI_MAX_RETRIES = 0;
|
|
@@ -275,4 +275,4 @@ function createObservedAI(client, providerOptions) {
|
|
|
275
275
|
//#endregion
|
|
276
276
|
export { normalizeBaseAIOptions as a, prepareRequestControls as c, createObservedAI as i, DEFAULT_AI_MAX_RETRIES as n, normalizeChatOptions as o, DEFAULT_AI_TIMEOUT_MS as r, normalizeImageGenerationOptions as s, DEFAULT_AI_GENERATION_LIMITS as t };
|
|
277
277
|
|
|
278
|
-
//# sourceMappingURL=safety-
|
|
278
|
+
//# sourceMappingURL=safety-ClfpyKEN.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safety-C415clBm.js","names":[],"sources":["../../src/shared/safety.ts"],"sourcesContent":["import type {\n AIGenerationLimits,\n AIInterface,\n AIReasoningOptions,\n AIRequestControls,\n AIRequestEvent,\n AIRequestOperation,\n AIRequestStatus,\n BaseAIOptions,\n ChatOptions,\n ImageGenerationOptions,\n} from './types';\nimport { AIError } from './types';\n\nexport const DEFAULT_AI_TIMEOUT_MS = 120_000;\nexport const DEFAULT_AI_MAX_RETRIES = 0;\n\nexport const DEFAULT_AI_GENERATION_LIMITS: Readonly<AIGenerationLimits> =\n Object.freeze({\n maxOutputTokens: 4096,\n maxReasoningTokens: 1024,\n maxImagesPerRequest: 1,\n onExceeded: 'error',\n });\n\nexport interface NormalizedBaseAIOptions extends BaseAIOptions {\n timeout: number;\n maxRetries: number;\n generationLimits: AIGenerationLimits;\n}\n\nexport interface PreparedRequestControls {\n signal: AbortSignal;\n timeout: number;\n cleanup(): void;\n didTimeout(): boolean;\n}\n\nfunction positiveInteger(\n value: number | undefined,\n fallback: number,\n field: string,\n): number {\n if (value === undefined) return fallback;\n if (!Number.isFinite(value) || value <= 0) {\n throw new AIError(\n `${field} must be a positive finite number`,\n 'AI_LIMIT_INVALID',\n );\n }\n return Math.trunc(value);\n}\n\nexport function normalizeBaseAIOptions<T extends BaseAIOptions>(\n options: T,\n): T & NormalizedBaseAIOptions {\n const configuredLimits = options.generationLimits || {};\n const generationLimits: AIGenerationLimits = {\n maxOutputTokens: positiveInteger(\n configuredLimits.maxOutputTokens,\n DEFAULT_AI_GENERATION_LIMITS.maxOutputTokens,\n 'generationLimits.maxOutputTokens',\n ),\n maxReasoningTokens: positiveInteger(\n configuredLimits.maxReasoningTokens,\n DEFAULT_AI_GENERATION_LIMITS.maxReasoningTokens,\n 'generationLimits.maxReasoningTokens',\n ),\n maxImagesPerRequest: positiveInteger(\n configuredLimits.maxImagesPerRequest,\n DEFAULT_AI_GENERATION_LIMITS.maxImagesPerRequest,\n 'generationLimits.maxImagesPerRequest',\n ),\n onExceeded:\n configuredLimits.onExceeded || DEFAULT_AI_GENERATION_LIMITS.onExceeded,\n };\n\n if (!['error', 'clamp'].includes(generationLimits.onExceeded)) {\n throw new AIError(\n 'generationLimits.onExceeded must be \"error\" or \"clamp\"',\n 'AI_LIMIT_INVALID',\n );\n }\n\n return {\n ...options,\n timeout: positiveInteger(options.timeout, DEFAULT_AI_TIMEOUT_MS, 'timeout'),\n maxRetries: (() => {\n if (options.maxRetries === undefined) return DEFAULT_AI_MAX_RETRIES;\n if (!Number.isFinite(options.maxRetries) || options.maxRetries < 0) {\n throw new AIError(\n 'maxRetries must be a non-negative finite number',\n 'AI_LIMIT_INVALID',\n );\n }\n return Math.trunc(options.maxRetries);\n })(),\n generationLimits,\n };\n}\n\nfunction reasoningTokensForEffort(\n effort: AIReasoningOptions['effort'] | false | undefined,\n): number | undefined {\n switch (effort) {\n case false:\n case 'none':\n return 0;\n case 'minimal':\n return 1024;\n case 'low':\n return 2048;\n case 'medium':\n return 4096;\n case 'high':\n return 8192;\n default:\n return undefined;\n }\n}\n\nfunction enforceLimit(\n requested: number,\n limit: number,\n behavior: AIGenerationLimits['onExceeded'],\n label: string,\n provider?: string,\n model?: string,\n): number {\n if (!Number.isFinite(requested) || requested < 0) {\n throw new AIError(\n `${label} must be a non-negative finite number`,\n 'AI_LIMIT_INVALID',\n provider,\n model,\n );\n }\n const normalized = Math.trunc(requested);\n if (normalized <= limit) return normalized;\n if (behavior === 'clamp') return limit;\n throw new AIError(\n `${label} ${normalized} exceeds configured ceiling ${limit}`,\n 'AI_LIMIT_EXCEEDED',\n provider,\n model,\n );\n}\n\nexport function normalizeChatOptions<\n T extends AIRequestControls & {\n maxTokens?: number;\n thinkingLevel?: ChatOptions['thinkingLevel'];\n includeThoughts?: boolean;\n },\n>(\n providerOptions: BaseAIOptions,\n options: T,\n provider?: string,\n model?: string,\n): T & { maxTokens: number; reasoning: AIReasoningOptions } {\n const normalizedProvider = normalizeBaseAIOptions(providerOptions);\n const limits = normalizedProvider.generationLimits;\n const requestedOutput = options.maxTokens ?? limits.maxOutputTokens;\n const legacyEffort =\n options.thinkingLevel ??\n (\n providerOptions as BaseAIOptions & {\n thinkingLevel?: ChatOptions['thinkingLevel'];\n }\n ).thinkingLevel;\n const reasoningWasRequested =\n options.reasoning !== undefined ||\n legacyEffort !== undefined ||\n options.includeThoughts === true;\n const requestedReasoning = reasoningWasRequested\n ? (options.reasoning?.maxTokens ??\n reasoningTokensForEffort(options.reasoning?.effort ?? legacyEffort) ??\n limits.maxReasoningTokens)\n : 0;\n\n return {\n ...options,\n maxTokens: enforceLimit(\n requestedOutput,\n limits.maxOutputTokens,\n limits.onExceeded,\n 'maxTokens',\n provider,\n model,\n ),\n reasoning: {\n ...options.reasoning,\n maxTokens: enforceLimit(\n requestedReasoning,\n limits.maxReasoningTokens,\n limits.onExceeded,\n 'reasoning.maxTokens',\n provider,\n model,\n ),\n includeThoughts:\n options.reasoning?.includeThoughts ?? options.includeThoughts,\n },\n };\n}\n\nexport function normalizeImageGenerationOptions<\n T extends ImageGenerationOptions,\n>(\n providerOptions: BaseAIOptions,\n options: T,\n provider?: string,\n model?: string,\n): T & { n: number } {\n const normalizedProvider = normalizeBaseAIOptions(providerOptions);\n const limits = normalizedProvider.generationLimits;\n return {\n ...options,\n n: enforceLimit(\n options.n ?? 1,\n limits.maxImagesPerRequest,\n limits.onExceeded,\n 'n',\n provider,\n model,\n ),\n };\n}\n\nexport function prepareRequestControls(\n providerOptions: BaseAIOptions,\n requestOptions: AIRequestControls = {},\n): PreparedRequestControls {\n const normalized = normalizeBaseAIOptions(providerOptions);\n const timeout = positiveInteger(\n requestOptions.timeout,\n normalized.timeout,\n 'timeout',\n );\n const controller = new AbortController();\n let timedOut = false;\n\n const abortFromCaller = () => controller.abort(requestOptions.signal?.reason);\n if (requestOptions.signal?.aborted) {\n abortFromCaller();\n } else {\n requestOptions.signal?.addEventListener('abort', abortFromCaller, {\n once: true,\n });\n }\n\n const timer = setTimeout(() => {\n timedOut = true;\n controller.abort(new Error(`AI request timed out after ${timeout}ms`));\n }, timeout);\n\n return {\n signal: controller.signal,\n timeout,\n didTimeout: () => timedOut,\n cleanup: () => {\n clearTimeout(timer);\n requestOptions.signal?.removeEventListener('abort', abortFromCaller);\n },\n };\n}\n\nfunction mergeTags(\n providerOptions: BaseAIOptions,\n callTags?: Record<string, string>,\n): Record<string, string> | undefined {\n return providerOptions.usageTags || callTags\n ? { ...providerOptions.usageTags, ...callTags }\n : undefined;\n}\n\nexport function emitRequestEvent(\n providerOptions: BaseAIOptions,\n event: Omit<AIRequestEvent, 'tags'> & {\n tags?: Record<string, string>;\n },\n): void {\n if (!providerOptions.onRequest) return;\n try {\n providerOptions.onRequest({\n ...event,\n tags: mergeTags(providerOptions, event.tags),\n });\n } catch {\n // Consumer telemetry must never affect an AI request.\n }\n}\n\nexport function classifyRequestFailure(\n error: unknown,\n didTimeout = false,\n): { status: AIRequestStatus; errorCode?: string } {\n const code = error instanceof AIError ? error.code : undefined;\n if (code === 'AI_LIMIT_EXCEEDED' || code === 'AI_LIMIT_INVALID') {\n return { status: 'rejected', errorCode: code };\n }\n if (didTimeout || code === 'AI_TIMEOUT' || code === 'TIMEOUT') {\n return { status: 'timed_out', errorCode: code || 'AI_TIMEOUT' };\n }\n if (error instanceof Error && error.name === 'AbortError') {\n return { status: 'aborted', errorCode: 'ABORTED' };\n }\n if (code === 'AI_ABORTED') {\n return { status: 'aborted', errorCode: code };\n }\n return {\n status: 'failed',\n errorCode: code || (error instanceof Error ? error.name : undefined),\n };\n}\n\nexport function requestEventBase(options: {\n providerOptions: BaseAIOptions;\n provider: string;\n model: string;\n operation: AIRequestOperation;\n callOptions?: { maxTokens?: number; usageTags?: Record<string, string> };\n effectiveMaxOutputTokens?: number;\n}): Omit<AIRequestEvent, 'status' | 'duration'> & {\n tags?: Record<string, string>;\n} {\n const normalized = normalizeBaseAIOptions(options.providerOptions);\n return {\n provider: options.provider,\n model: options.model,\n operation: options.operation,\n attempts: normalized.maxRetries + 1,\n requestedMaxOutputTokens: options.callOptions?.maxTokens,\n effectiveMaxOutputTokens:\n options.operation === 'generateImage' ||\n VIDEO_GENERATION_OPERATIONS.has(options.operation)\n ? undefined\n : (options.effectiveMaxOutputTokens ??\n normalized.generationLimits.maxOutputTokens),\n tags: options.callOptions?.usageTags,\n };\n}\n\nconst OBSERVED_OPERATIONS = new Set<AIRequestOperation>([\n 'chat',\n 'complete',\n 'message',\n 'describeImage',\n 'generateImage',\n 'stream',\n 'submitVideoGenerationJob',\n 'getVideoGenerationJob',\n 'fetchVideoGenerationResult',\n 'cancelVideoGenerationJob',\n]);\n\n/**\n * `getVideoGenerationJob` / `fetchVideoGenerationResult` /\n * `cancelVideoGenerationJob` take a `VideoGenerationJob` handle as their\n * first (and only) argument rather than a chat-shaped `options` object as\n * the second argument. The handle still carries a `model` field, so\n * `callOptionsFor` reads it from the same position as `submitVideoGenerationJob`'s\n * `options` (index 0) rather than index 1.\n */\nconst VIDEO_HANDLE_OPERATIONS = new Set<AIRequestOperation>([\n 'getVideoGenerationJob',\n 'fetchVideoGenerationResult',\n 'cancelVideoGenerationJob',\n]);\n\nconst VIDEO_GENERATION_OPERATIONS = new Set<AIRequestOperation>([\n 'submitVideoGenerationJob',\n ...VIDEO_HANDLE_OPERATIONS,\n]);\n\nfunction callOptionsFor(\n operation: AIRequestOperation,\n args: unknown[],\n):\n | (AIRequestControls & {\n model?: string;\n maxTokens?: number;\n usageTags?: Record<string, string>;\n })\n | undefined {\n const index =\n operation === 'describeImage'\n ? 2\n : operation === 'submitVideoGenerationJob' ||\n VIDEO_HANDLE_OPERATIONS.has(operation)\n ? 0\n : 1;\n const value = args[index];\n return value && typeof value === 'object'\n ? (value as AIRequestControls & {\n model?: string;\n maxTokens?: number;\n usageTags?: Record<string, string>;\n })\n : undefined;\n}\n\nfunction providerName(options: BaseAIOptions): string {\n const configured = (options as BaseAIOptions & { type?: string }).type;\n return configured || 'openai';\n}\n\nfunction effectiveOutputTokens(\n operation: AIRequestOperation,\n providerOptions: BaseAIOptions,\n callOptions:\n | (AIRequestControls & {\n model?: string;\n maxTokens?: number;\n n?: number;\n })\n | undefined,\n): number | undefined {\n if (\n operation === 'generateImage' ||\n VIDEO_GENERATION_OPERATIONS.has(operation)\n ) {\n // maxTokens/reasoning concepts don't apply to video-generation jobs;\n // avoid attaching a misleading chat-shaped token ceiling to the event.\n if (operation === 'generateImage') {\n normalizeImageGenerationOptions(\n providerOptions,\n callOptions || {},\n providerName(providerOptions),\n callOptions?.model,\n );\n }\n return undefined;\n }\n\n return normalizeChatOptions(\n providerOptions,\n callOptions || {},\n providerName(providerOptions),\n callOptions?.model || providerOptions.defaultModel,\n ).maxTokens;\n}\n\n/**\n * Wrap a public provider instance with prompt-free lifecycle observation.\n * Provider adapters still enforce limits internally so direct construction is\n * protected; this wrapper adds one terminal event to the public getAI() path.\n */\nexport function createObservedAI<T extends AIInterface>(\n client: T,\n providerOptions: BaseAIOptions,\n): T {\n if (!providerOptions.onRequest) return client;\n\n return new Proxy(client, {\n get(target, property, receiver) {\n const value = Reflect.get(target, property, receiver);\n if (\n typeof property !== 'string' ||\n typeof value !== 'function' ||\n !OBSERVED_OPERATIONS.has(property as AIRequestOperation)\n ) {\n return typeof value === 'function' ? value.bind(target) : value;\n }\n\n const operation = property as AIRequestOperation;\n if (operation === 'stream') {\n return async function* (...args: unknown[]) {\n const startedAt = Date.now();\n const callOptions = callOptionsFor(operation, args);\n let base: ReturnType<typeof requestEventBase> | undefined;\n try {\n base = requestEventBase({\n providerOptions,\n provider: providerName(providerOptions),\n model:\n callOptions?.model || providerOptions.defaultModel || 'unknown',\n operation,\n callOptions,\n effectiveMaxOutputTokens: effectiveOutputTokens(\n operation,\n providerOptions,\n callOptions,\n ),\n });\n const iterable = Reflect.apply(\n value,\n target,\n args,\n ) as AsyncIterable<string>;\n for await (const chunk of iterable) yield chunk;\n emitRequestEvent(providerOptions, {\n ...base,\n status: 'succeeded',\n duration: Date.now() - startedAt,\n });\n } catch (error) {\n const failure = classifyRequestFailure(error);\n base ||= requestEventBase({\n providerOptions,\n provider: providerName(providerOptions),\n model:\n callOptions?.model || providerOptions.defaultModel || 'unknown',\n operation,\n callOptions,\n });\n emitRequestEvent(providerOptions, {\n ...base,\n ...failure,\n duration: Date.now() - startedAt,\n });\n throw error;\n }\n };\n }\n\n return async (...args: unknown[]) => {\n const startedAt = Date.now();\n const callOptions = callOptionsFor(operation, args);\n let base: ReturnType<typeof requestEventBase> | undefined;\n try {\n base = requestEventBase({\n providerOptions,\n provider: providerName(providerOptions),\n model:\n callOptions?.model || providerOptions.defaultModel || 'unknown',\n operation,\n callOptions,\n effectiveMaxOutputTokens: effectiveOutputTokens(\n operation,\n providerOptions,\n callOptions,\n ),\n });\n const result = await Reflect.apply(value, target, args);\n emitRequestEvent(providerOptions, {\n ...base,\n status: 'succeeded',\n duration: Date.now() - startedAt,\n });\n return result;\n } catch (error) {\n const failure = classifyRequestFailure(error);\n base ||= requestEventBase({\n providerOptions,\n provider: providerName(providerOptions),\n model:\n callOptions?.model || providerOptions.defaultModel || 'unknown',\n operation,\n callOptions,\n });\n emitRequestEvent(providerOptions, {\n ...base,\n ...failure,\n duration: Date.now() - startedAt,\n });\n throw error;\n }\n };\n },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"safety-ClfpyKEN.js","names":[],"sources":["../../src/shared/safety.ts"],"sourcesContent":["import type {\n AIGenerationLimits,\n AIInterface,\n AIReasoningOptions,\n AIRequestControls,\n AIRequestEvent,\n AIRequestOperation,\n AIRequestStatus,\n BaseAIOptions,\n ChatOptions,\n ImageGenerationOptions,\n} from './types';\nimport { AIError } from './types';\n\nexport const DEFAULT_AI_TIMEOUT_MS = 120_000;\nexport const DEFAULT_AI_MAX_RETRIES = 0;\n\nexport const DEFAULT_AI_GENERATION_LIMITS: Readonly<AIGenerationLimits> =\n Object.freeze({\n maxOutputTokens: 4096,\n maxReasoningTokens: 1024,\n maxImagesPerRequest: 1,\n onExceeded: 'error',\n });\n\nexport interface NormalizedBaseAIOptions extends BaseAIOptions {\n timeout: number;\n maxRetries: number;\n generationLimits: AIGenerationLimits;\n}\n\nexport interface PreparedRequestControls {\n signal: AbortSignal;\n timeout: number;\n cleanup(): void;\n didTimeout(): boolean;\n}\n\nfunction positiveInteger(\n value: number | undefined,\n fallback: number,\n field: string,\n): number {\n if (value === undefined) return fallback;\n if (!Number.isFinite(value) || value <= 0) {\n throw new AIError(\n `${field} must be a positive finite number`,\n 'AI_LIMIT_INVALID',\n );\n }\n return Math.trunc(value);\n}\n\nexport function normalizeBaseAIOptions<T extends BaseAIOptions>(\n options: T,\n): T & NormalizedBaseAIOptions {\n const configuredLimits = options.generationLimits || {};\n const generationLimits: AIGenerationLimits = {\n maxOutputTokens: positiveInteger(\n configuredLimits.maxOutputTokens,\n DEFAULT_AI_GENERATION_LIMITS.maxOutputTokens,\n 'generationLimits.maxOutputTokens',\n ),\n maxReasoningTokens: positiveInteger(\n configuredLimits.maxReasoningTokens,\n DEFAULT_AI_GENERATION_LIMITS.maxReasoningTokens,\n 'generationLimits.maxReasoningTokens',\n ),\n maxImagesPerRequest: positiveInteger(\n configuredLimits.maxImagesPerRequest,\n DEFAULT_AI_GENERATION_LIMITS.maxImagesPerRequest,\n 'generationLimits.maxImagesPerRequest',\n ),\n onExceeded:\n configuredLimits.onExceeded || DEFAULT_AI_GENERATION_LIMITS.onExceeded,\n };\n\n if (!['error', 'clamp'].includes(generationLimits.onExceeded)) {\n throw new AIError(\n 'generationLimits.onExceeded must be \"error\" or \"clamp\"',\n 'AI_LIMIT_INVALID',\n );\n }\n\n return {\n ...options,\n timeout: positiveInteger(options.timeout, DEFAULT_AI_TIMEOUT_MS, 'timeout'),\n maxRetries: (() => {\n if (options.maxRetries === undefined) return DEFAULT_AI_MAX_RETRIES;\n if (!Number.isFinite(options.maxRetries) || options.maxRetries < 0) {\n throw new AIError(\n 'maxRetries must be a non-negative finite number',\n 'AI_LIMIT_INVALID',\n );\n }\n return Math.trunc(options.maxRetries);\n })(),\n generationLimits,\n };\n}\n\nfunction reasoningTokensForEffort(\n effort: AIReasoningOptions['effort'] | false | undefined,\n): number | undefined {\n switch (effort) {\n case false:\n case 'none':\n return 0;\n case 'minimal':\n return 1024;\n case 'low':\n return 2048;\n case 'medium':\n return 4096;\n case 'high':\n return 8192;\n default:\n return undefined;\n }\n}\n\nfunction enforceLimit(\n requested: number,\n limit: number,\n behavior: AIGenerationLimits['onExceeded'],\n label: string,\n provider?: string,\n model?: string,\n): number {\n if (!Number.isFinite(requested) || requested < 0) {\n throw new AIError(\n `${label} must be a non-negative finite number`,\n 'AI_LIMIT_INVALID',\n provider,\n model,\n );\n }\n const normalized = Math.trunc(requested);\n if (normalized <= limit) return normalized;\n if (behavior === 'clamp') return limit;\n throw new AIError(\n `${label} ${normalized} exceeds configured ceiling ${limit}`,\n 'AI_LIMIT_EXCEEDED',\n provider,\n model,\n );\n}\n\nexport function normalizeChatOptions<\n T extends AIRequestControls & {\n maxTokens?: number;\n thinkingLevel?: ChatOptions['thinkingLevel'];\n includeThoughts?: boolean;\n },\n>(\n providerOptions: BaseAIOptions,\n options: T,\n provider?: string,\n model?: string,\n): T & { maxTokens: number; reasoning: AIReasoningOptions } {\n const normalizedProvider = normalizeBaseAIOptions(providerOptions);\n const limits = normalizedProvider.generationLimits;\n const requestedOutput = options.maxTokens ?? limits.maxOutputTokens;\n const legacyEffort =\n options.thinkingLevel ??\n (\n providerOptions as BaseAIOptions & {\n thinkingLevel?: ChatOptions['thinkingLevel'];\n }\n ).thinkingLevel;\n const reasoningWasRequested =\n options.reasoning !== undefined ||\n legacyEffort !== undefined ||\n options.includeThoughts === true;\n const requestedReasoning = reasoningWasRequested\n ? (options.reasoning?.maxTokens ??\n reasoningTokensForEffort(options.reasoning?.effort ?? legacyEffort) ??\n limits.maxReasoningTokens)\n : 0;\n\n return {\n ...options,\n maxTokens: enforceLimit(\n requestedOutput,\n limits.maxOutputTokens,\n limits.onExceeded,\n 'maxTokens',\n provider,\n model,\n ),\n reasoning: {\n ...options.reasoning,\n maxTokens: enforceLimit(\n requestedReasoning,\n limits.maxReasoningTokens,\n limits.onExceeded,\n 'reasoning.maxTokens',\n provider,\n model,\n ),\n includeThoughts:\n options.reasoning?.includeThoughts ?? options.includeThoughts,\n },\n };\n}\n\nexport function normalizeImageGenerationOptions<\n T extends ImageGenerationOptions,\n>(\n providerOptions: BaseAIOptions,\n options: T,\n provider?: string,\n model?: string,\n): T & { n: number } {\n const normalizedProvider = normalizeBaseAIOptions(providerOptions);\n const limits = normalizedProvider.generationLimits;\n return {\n ...options,\n n: enforceLimit(\n options.n ?? 1,\n limits.maxImagesPerRequest,\n limits.onExceeded,\n 'n',\n provider,\n model,\n ),\n };\n}\n\nexport function prepareRequestControls(\n providerOptions: BaseAIOptions,\n requestOptions: AIRequestControls = {},\n): PreparedRequestControls {\n const normalized = normalizeBaseAIOptions(providerOptions);\n const timeout = positiveInteger(\n requestOptions.timeout,\n normalized.timeout,\n 'timeout',\n );\n const controller = new AbortController();\n let timedOut = false;\n\n const abortFromCaller = () => controller.abort(requestOptions.signal?.reason);\n if (requestOptions.signal?.aborted) {\n abortFromCaller();\n } else {\n requestOptions.signal?.addEventListener('abort', abortFromCaller, {\n once: true,\n });\n }\n\n const timer = setTimeout(() => {\n timedOut = true;\n controller.abort(new Error(`AI request timed out after ${timeout}ms`));\n }, timeout);\n\n return {\n signal: controller.signal,\n timeout,\n didTimeout: () => timedOut,\n cleanup: () => {\n clearTimeout(timer);\n requestOptions.signal?.removeEventListener('abort', abortFromCaller);\n },\n };\n}\n\nfunction mergeTags(\n providerOptions: BaseAIOptions,\n callTags?: Record<string, string>,\n): Record<string, string> | undefined {\n return providerOptions.usageTags || callTags\n ? { ...providerOptions.usageTags, ...callTags }\n : undefined;\n}\n\nexport function emitRequestEvent(\n providerOptions: BaseAIOptions,\n event: Omit<AIRequestEvent, 'tags'> & {\n tags?: Record<string, string>;\n },\n): void {\n if (!providerOptions.onRequest) return;\n try {\n providerOptions.onRequest({\n ...event,\n tags: mergeTags(providerOptions, event.tags),\n });\n } catch {\n // Consumer telemetry must never affect an AI request.\n }\n}\n\nexport function classifyRequestFailure(\n error: unknown,\n didTimeout = false,\n): { status: AIRequestStatus; errorCode?: string } {\n const code = error instanceof AIError ? error.code : undefined;\n if (code === 'AI_LIMIT_EXCEEDED' || code === 'AI_LIMIT_INVALID') {\n return { status: 'rejected', errorCode: code };\n }\n if (didTimeout || code === 'AI_TIMEOUT' || code === 'TIMEOUT') {\n return { status: 'timed_out', errorCode: code || 'AI_TIMEOUT' };\n }\n if (error instanceof Error && error.name === 'AbortError') {\n return { status: 'aborted', errorCode: 'ABORTED' };\n }\n if (code === 'AI_ABORTED') {\n return { status: 'aborted', errorCode: code };\n }\n return {\n status: 'failed',\n errorCode: code || (error instanceof Error ? error.name : undefined),\n };\n}\n\nexport function requestEventBase(options: {\n providerOptions: BaseAIOptions;\n provider: string;\n model: string;\n operation: AIRequestOperation;\n callOptions?: { maxTokens?: number; usageTags?: Record<string, string> };\n effectiveMaxOutputTokens?: number;\n}): Omit<AIRequestEvent, 'status' | 'duration'> & {\n tags?: Record<string, string>;\n} {\n const normalized = normalizeBaseAIOptions(options.providerOptions);\n return {\n provider: options.provider,\n model: options.model,\n operation: options.operation,\n attempts: normalized.maxRetries + 1,\n requestedMaxOutputTokens: options.callOptions?.maxTokens,\n effectiveMaxOutputTokens:\n options.operation === 'generateImage' ||\n VIDEO_GENERATION_OPERATIONS.has(options.operation)\n ? undefined\n : (options.effectiveMaxOutputTokens ??\n normalized.generationLimits.maxOutputTokens),\n tags: options.callOptions?.usageTags,\n };\n}\n\nconst OBSERVED_OPERATIONS = new Set<AIRequestOperation>([\n 'chat',\n 'complete',\n 'message',\n 'describeImage',\n 'generateImage',\n 'stream',\n 'submitVideoGenerationJob',\n 'getVideoGenerationJob',\n 'fetchVideoGenerationResult',\n 'cancelVideoGenerationJob',\n]);\n\n/**\n * `getVideoGenerationJob` / `fetchVideoGenerationResult` /\n * `cancelVideoGenerationJob` take a `VideoGenerationJob` handle as their\n * first (and only) argument rather than a chat-shaped `options` object as\n * the second argument. The handle still carries a `model` field, so\n * `callOptionsFor` reads it from the same position as `submitVideoGenerationJob`'s\n * `options` (index 0) rather than index 1.\n */\nconst VIDEO_HANDLE_OPERATIONS = new Set<AIRequestOperation>([\n 'getVideoGenerationJob',\n 'fetchVideoGenerationResult',\n 'cancelVideoGenerationJob',\n]);\n\nconst VIDEO_GENERATION_OPERATIONS = new Set<AIRequestOperation>([\n 'submitVideoGenerationJob',\n ...VIDEO_HANDLE_OPERATIONS,\n]);\n\nfunction callOptionsFor(\n operation: AIRequestOperation,\n args: unknown[],\n):\n | (AIRequestControls & {\n model?: string;\n maxTokens?: number;\n usageTags?: Record<string, string>;\n })\n | undefined {\n const index =\n operation === 'describeImage'\n ? 2\n : operation === 'submitVideoGenerationJob' ||\n VIDEO_HANDLE_OPERATIONS.has(operation)\n ? 0\n : 1;\n const value = args[index];\n return value && typeof value === 'object'\n ? (value as AIRequestControls & {\n model?: string;\n maxTokens?: number;\n usageTags?: Record<string, string>;\n })\n : undefined;\n}\n\nfunction providerName(options: BaseAIOptions): string {\n const configured = (options as BaseAIOptions & { type?: string }).type;\n return configured || 'openai';\n}\n\nfunction effectiveOutputTokens(\n operation: AIRequestOperation,\n providerOptions: BaseAIOptions,\n callOptions:\n | (AIRequestControls & {\n model?: string;\n maxTokens?: number;\n n?: number;\n })\n | undefined,\n): number | undefined {\n if (\n operation === 'generateImage' ||\n VIDEO_GENERATION_OPERATIONS.has(operation)\n ) {\n // maxTokens/reasoning concepts don't apply to video-generation jobs;\n // avoid attaching a misleading chat-shaped token ceiling to the event.\n if (operation === 'generateImage') {\n normalizeImageGenerationOptions(\n providerOptions,\n callOptions || {},\n providerName(providerOptions),\n callOptions?.model,\n );\n }\n return undefined;\n }\n\n return normalizeChatOptions(\n providerOptions,\n callOptions || {},\n providerName(providerOptions),\n callOptions?.model || providerOptions.defaultModel,\n ).maxTokens;\n}\n\n/**\n * Wrap a public provider instance with prompt-free lifecycle observation.\n * Provider adapters still enforce limits internally so direct construction is\n * protected; this wrapper adds one terminal event to the public getAI() path.\n */\nexport function createObservedAI<T extends AIInterface>(\n client: T,\n providerOptions: BaseAIOptions,\n): T {\n if (!providerOptions.onRequest) return client;\n\n return new Proxy(client, {\n get(target, property, receiver) {\n const value = Reflect.get(target, property, receiver);\n if (\n typeof property !== 'string' ||\n typeof value !== 'function' ||\n !OBSERVED_OPERATIONS.has(property as AIRequestOperation)\n ) {\n return typeof value === 'function' ? value.bind(target) : value;\n }\n\n const operation = property as AIRequestOperation;\n if (operation === 'stream') {\n return async function* (...args: unknown[]) {\n const startedAt = Date.now();\n const callOptions = callOptionsFor(operation, args);\n let base: ReturnType<typeof requestEventBase> | undefined;\n try {\n base = requestEventBase({\n providerOptions,\n provider: providerName(providerOptions),\n model:\n callOptions?.model || providerOptions.defaultModel || 'unknown',\n operation,\n callOptions,\n effectiveMaxOutputTokens: effectiveOutputTokens(\n operation,\n providerOptions,\n callOptions,\n ),\n });\n const iterable = Reflect.apply(\n value,\n target,\n args,\n ) as AsyncIterable<string>;\n for await (const chunk of iterable) yield chunk;\n emitRequestEvent(providerOptions, {\n ...base,\n status: 'succeeded',\n duration: Date.now() - startedAt,\n });\n } catch (error) {\n const failure = classifyRequestFailure(error);\n base ||= requestEventBase({\n providerOptions,\n provider: providerName(providerOptions),\n model:\n callOptions?.model || providerOptions.defaultModel || 'unknown',\n operation,\n callOptions,\n });\n emitRequestEvent(providerOptions, {\n ...base,\n ...failure,\n duration: Date.now() - startedAt,\n });\n throw error;\n }\n };\n }\n\n return async (...args: unknown[]) => {\n const startedAt = Date.now();\n const callOptions = callOptionsFor(operation, args);\n let base: ReturnType<typeof requestEventBase> | undefined;\n try {\n base = requestEventBase({\n providerOptions,\n provider: providerName(providerOptions),\n model:\n callOptions?.model || providerOptions.defaultModel || 'unknown',\n operation,\n callOptions,\n effectiveMaxOutputTokens: effectiveOutputTokens(\n operation,\n providerOptions,\n callOptions,\n ),\n });\n const result = await Reflect.apply(value, target, args);\n emitRequestEvent(providerOptions, {\n ...base,\n status: 'succeeded',\n duration: Date.now() - startedAt,\n });\n return result;\n } catch (error) {\n const failure = classifyRequestFailure(error);\n base ||= requestEventBase({\n providerOptions,\n provider: providerName(providerOptions),\n model:\n callOptions?.model || providerOptions.defaultModel || 'unknown',\n operation,\n callOptions,\n });\n emitRequestEvent(providerOptions, {\n ...base,\n ...failure,\n duration: Date.now() - startedAt,\n });\n throw error;\n }\n };\n },\n });\n}\n"],"mappings":"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"}
|