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

271 lines (270 loc) 13 kB
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; import { stepCountIs, streamText } from "ai"; import { DeepSeekModels } 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 { createProxyFetch, maskProxyUrl } from "../proxy/proxyFetch.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 { createDeepSeekConfig, 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"; const makeLoggingFetch = (provider) => { const base = createProxyFetch(); return (async (input, init) => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; const reqSize = init?.body && typeof init.body === "string" ? init.body.length : 0; const response = await base(input, init); if (!response.ok) { // Don't fall back to the raw URL — that would defeat the redaction. const safeUrl = maskProxyUrl(url) ?? "<redacted>"; if (process.env.NEUROLINK_DEBUG_HTTP === "1") { const clone = response.clone(); const body = await clone.text().catch(() => "<unreadable>"); logger.warn(`[${provider}] upstream ${response.status}`, { url: safeUrl, body: body.slice(0, 800), reqSize, }); } else { logger.warn(`[${provider}] upstream ${response.status} url=${safeUrl} reqSize=${reqSize}`); } } return response; }); }; const DEEPSEEK_DEFAULT_BASE_URL = "https://api.deepseek.com"; const getDeepSeekApiKey = () => { return validateApiKey(createDeepSeekConfig()); }; const getDefaultDeepSeekModel = () => { return getProviderModel("DEEPSEEK_MODEL", DeepSeekModels.DEEPSEEK_CHAT); }; /** * DeepSeek Provider * OpenAI-compatible chat completions; supports deepseek-chat (V3) and * deepseek-reasoner (R1, exposes reasoning_content). */ export class DeepSeekProvider extends BaseProvider { model; apiKey; baseURL; constructor(modelName, sdk, _region, credentials) { const validatedNeurolink = isNeuroLink(sdk) ? sdk : undefined; super(modelName, "deepseek", validatedNeurolink); // Trim the override before applying precedence. A blank/whitespace // `credentials.apiKey` should NOT bypass `getDeepSeekApiKey()` — that // would build a client with an unusable bearer token and fail at request // time with a confusing 401 instead of at construction time. const overrideApiKey = credentials?.apiKey?.trim(); this.apiKey = overrideApiKey && overrideApiKey.length > 0 ? overrideApiKey : getDeepSeekApiKey(); this.baseURL = credentials?.baseURL ?? process.env.DEEPSEEK_BASE_URL ?? DEEPSEEK_DEFAULT_BASE_URL; // We deliberately use `@ai-sdk/openai-compatible` rather than // `@ai-sdk/openai`. Two upstream behaviors of `@ai-sdk/openai` break us: // 1. It always sends `response_format: { type: "json_schema" }` when a // schema is provided. DeepSeek's API rejects that with the literal // message "This response_format type is unavailable now". // 2. It does not parse the `reasoning_content` field that // `deepseek-reasoner` emits, so chain-of-thought is silently dropped. // `@ai-sdk/openai-compatible` honors `supportsStructuredOutputs: false` // (falls back to `{ type: "json_object" }` and injects the schema into // the prompt) and parses both `choice.message.reasoning_content` and // `delta.reasoning_content` into the SDK-standard `reasoning` part. const deepseek = createOpenAICompatible({ name: "deepseek", apiKey: this.apiKey, baseURL: this.baseURL, fetch: makeLoggingFetch("deepseek"), supportsStructuredOutputs: false, includeUsage: true, // DeepSeek's `response_format: { type: "json_object" }` requires the // prompt to literally contain the word "json" — otherwise the API // rejects with: "Prompt must contain the word 'json' in some form to // use 'response_format' of type 'json_object'." The OpenAI-compatible // SDK fallback path (used because supportsStructuredOutputs is false) // does not inject this guidance itself, so we prepend a system // message when it's missing. No-op for non-JSON requests. transformRequestBody: (body) => { const rf = body .response_format; if (rf?.type !== "json_object") { return body; } const messages = body .messages; if (!Array.isArray(messages)) { return body; } const containsJsonWord = messages.some((m) => { const c = m?.content; if (typeof c === "string") { return /\bjson\b/i.test(c); } if (Array.isArray(c)) { return c.some((part) => typeof part?.text === "string" && /\bjson\b/i.test(part.text)); } return false; }); if (containsJsonWord) { return body; } return { ...body, messages: [ { role: "system", content: "Respond with valid JSON that satisfies the requested schema. Output JSON only — no prose, no markdown fencing.", }, ...messages, ], }; }, }); this.model = deepseek.chatModel(this.modelName); logger.debug("DeepSeek 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]: "deepseek", [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 isReasoner = this.modelName === DeepSeekModels.DEEPSEEK_REASONER; 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), // DeepSeek's `thinking` mode is opt-in for chat models — only enable // when the caller explicitly asks for it via `thinkingConfig.enabled`. // Forcing it on every chat call would trigger extended reasoning for // simple prompts (and ignore reasoner models which control it natively). providerOptions: !isReasoner && options.thinkingConfig?.enabled ? { openai: { thinking: { type: "enabled" }, }, } : undefined, 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("[DeepSeekProvider] 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: `deepseek-stream-${Date.now()}`, streamingMode: true, }); return { stream: transformedStream, provider: this.providerName, model: this.modelName, analytics: analyticsPromise, metadata: { startTime, streamId: `deepseek-${Date.now()}` }, }; } catch (error) { timeoutController?.cleanup(); throw this.handleProviderError(error); } } getProviderName() { return this.providerName; } getDefaultModel() { return getDefaultDeepSeekModel(); } getAISDKModel() { return this.model; } formatProviderError(error) { if (error instanceof TimeoutError) { return new NetworkError(`Request timed out: ${error.message}`, "deepseek"); } 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 DeepSeek API key. Please check your DEEPSEEK_API_KEY environment variable.", "deepseek"); } if (message.includes("rate limit") || message.includes("429")) { return new RateLimitError("DeepSeek rate limit exceeded", "deepseek"); } if (message.includes("Insufficient Balance") || message.includes("insufficient_balance") || message.includes("402")) { return new ProviderError("DeepSeek account has insufficient balance. Top up at https://platform.deepseek.com/usage", "deepseek"); } if (message.includes("model_not_found") || message.includes("404")) { return new InvalidModelError(`DeepSeek model '${this.modelName}' not found. Use 'deepseek-chat' or 'deepseek-reasoner'.`, "deepseek"); } return new ProviderError(`DeepSeek error: ${message}`, "deepseek"); } async validateConfiguration() { return typeof this.apiKey === "string" && this.apiKey.trim().length > 0; } getConfiguration() { return { provider: this.providerName, model: this.modelName, defaultModel: getDefaultDeepSeekModel(), baseURL: this.baseURL, }; } } export default DeepSeekProvider;