UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

174 lines (173 loc) 8.45 kB
import { createOpenAI } from "@ai-sdk/openai"; import { stepCountIs, streamText } from "ai"; import { CloudflareModels } from "../constants/enums.js"; import { BaseProvider } from "../core/baseProvider.js"; import { DEFAULT_MAX_STEPS } from "../core/constants.js"; import { streamAnalyticsCollector } from "../core/streamAnalytics.js"; import { isNeuroLink } from "../neurolink.js"; import { createLoggingFetch } from "../utils/loggingFetch.js"; import { tracers, ATTR, withClientStreamSpan } from "../telemetry/index.js"; import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, RateLimitError, } from "../types/index.js"; import { logger } from "../utils/logger.js"; import { createCloudflareConfig, getProviderModel, validateApiKey, } from "../utils/providerConfig.js"; import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js"; import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js"; import { resolveToolChoice } from "../utils/toolChoice.js"; import { toAnalyticsStreamResult } from "./providerTypeUtils.js"; /** * Cloudflare Workers AI exposes an OpenAI-compatible endpoint scoped per * account: `https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/ai/v1`. * The account id is required — without it the URL would 404. */ const buildCloudflareBaseURL = (accountId) => `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`; const getCloudflareApiKey = () => validateApiKey(createCloudflareConfig()); const getDefaultCloudflareModel = () => getProviderModel("CLOUDFLARE_MODEL", CloudflareModels.LLAMA_3_3_70B_FAST); /** * Cloudflare Workers AI Provider * * Edge-served open models (Llama / Mistral / Qwen / Gemma) at * `https://api.cloudflare.com/client/v4/accounts/{accountId}/ai/v1` * (OpenAI-compatible). Cheapest tier for high-volume usage. * * Required env: `CLOUDFLARE_API_KEY` + `CLOUDFLARE_ACCOUNT_ID`. * * @see https://developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility/ */ export class CloudflareProvider extends BaseProvider { model; apiKey; baseURL; constructor(modelName, sdk, _region, credentials) { const validatedNeurolink = isNeuroLink(sdk) ? sdk : undefined; super(modelName, "cloudflare", validatedNeurolink); const overrideApiKey = credentials?.apiKey?.trim(); this.apiKey = overrideApiKey && overrideApiKey.length > 0 ? overrideApiKey : getCloudflareApiKey(); const accountId = (credentials?.accountId ?? process.env.CLOUDFLARE_ACCOUNT_ID ?? "").trim(); if (!accountId) { throw new Error("CLOUDFLARE_ACCOUNT_ID is required (or pass credentials.cloudflare.accountId). Get the account id from https://dash.cloudflare.com/"); } this.baseURL = credentials?.baseURL ?? buildCloudflareBaseURL(accountId); const cloudflare = createOpenAI({ apiKey: this.apiKey, baseURL: this.baseURL, fetch: createLoggingFetch("cloudflare"), }); this.model = cloudflare.chat(this.modelName); logger.debug("Cloudflare Workers AI Provider initialized", { modelName: this.modelName, providerName: this.providerName, baseURL: this.baseURL, }); } async executeStream(options, _analysisSchema) { return withClientStreamSpan({ name: "neurolink.provider.stream", tracer: tracers.provider, attributes: { [ATTR.GEN_AI_SYSTEM]: "cloudflare", [ATTR.GEN_AI_MODEL]: this.modelName, [ATTR.GEN_AI_OPERATION]: "stream", [ATTR.NL_STREAM_MODE]: true, }, }, async () => this.executeStreamInner(options), (r) => r.stream, (r, wrapped) => ({ ...r, stream: wrapped })); } async executeStreamInner(options) { this.validateStreamOptions(options); const startTime = Date.now(); const timeout = this.getTimeout(options); const timeoutController = createTimeoutController(timeout, this.providerName, "stream"); try { const shouldUseTools = !options.disableTools && this.supportsTools(); const tools = shouldUseTools ? options.tools || (await this.getAllTools()) : {}; const messages = await this.buildMessagesForStream(options); const model = await this.getAISDKModelWithMiddleware(options); const result = await streamText({ model, messages, temperature: options.temperature, maxOutputTokens: options.maxTokens, tools, stopWhen: stepCountIs(options.maxSteps || DEFAULT_MAX_STEPS), toolChoice: resolveToolChoice(options, tools, shouldUseTools), abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal), experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options), experimental_repairToolCall: this.getToolCallRepairFn(options), onStepFinish: ({ toolCalls, toolResults }) => { emitToolEndFromStepFinish(this.neurolink?.getEventEmitter(), toolResults); this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => { logger.warn("[CloudflareProvider] Failed to store tool executions", { provider: this.providerName, error: error instanceof Error ? error.message : String(error), }); }); }, }); timeoutController?.cleanup(); const transformedStream = this.createTextStream(result); const analyticsPromise = streamAnalyticsCollector.createAnalytics(this.providerName, this.modelName, toAnalyticsStreamResult(result), Date.now() - startTime, { requestId: `cloudflare-stream-${Date.now()}`, streamingMode: true, }); return { stream: transformedStream, provider: this.providerName, model: this.modelName, analytics: analyticsPromise, metadata: { startTime, streamId: `cloudflare-${Date.now()}` }, }; } catch (error) { timeoutController?.cleanup(); throw this.handleProviderError(error); } } getProviderName() { return this.providerName; } getDefaultModel() { return getDefaultCloudflareModel(); } getAISDKModel() { return this.model; } formatProviderError(error) { if (error instanceof TimeoutError) { return new NetworkError(`Request timed out: ${error.message}`, "cloudflare"); } const errorRecord = error; const message = typeof errorRecord?.message === "string" ? errorRecord.message : "Unknown error"; if (message.includes("Invalid API key") || message.includes("Authentication") || message.includes("401")) { return new AuthenticationError("Invalid Cloudflare API key. Use a token with Workers AI Read+Write scope. Get one at https://dash.cloudflare.com/profile/api-tokens", "cloudflare"); } if (message.includes("rate limit") || message.includes("429")) { return new RateLimitError("Cloudflare Workers AI rate limit exceeded. Free-tier neurons reset daily.", "cloudflare"); } if (message.includes("model_not_found") || message.includes("404")) { return new InvalidModelError(`Cloudflare model '${this.modelName}' not found. Browse https://developers.cloudflare.com/workers-ai/models/`, "cloudflare"); } return new ProviderError(`Cloudflare Workers AI error: ${message}`, "cloudflare"); } async validateConfiguration() { return typeof this.apiKey === "string" && this.apiKey.trim().length > 0; } getConfiguration() { return { provider: this.providerName, model: this.modelName, defaultModel: getDefaultCloudflareModel(), baseURL: this.baseURL, }; } } export default CloudflareProvider;