import { APICallError, InvalidResponseDataError, LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3Content, LanguageModelV3FinishReason, LanguageModelV3GenerateResult, LanguageModelV3StreamPart, LanguageModelV3StreamResult, } from '@ai-sdk/provider'; import { combineHeaders, createEventSourceResponseHandler, createJsonErrorResponseHandler, createJsonResponseHandler, FetchFunction, generateId, InferSchema, isParsableJson, parseProviderOptions, ParseResult, postJsonToApi, ResponseHandler, } from '@ai-sdk/provider-utils'; import { convertToDeepSeekChatMessages } from './convert-to-deepseek-chat-messages'; import { convertDeepSeekUsage } from './convert-to-deepseek-usage'; import { deepseekChatChunkSchema, deepseekChatResponseSchema, DeepSeekChatTokenUsage, deepSeekErrorSchema, } from './deepseek-chat-api-types'; import { DeepSeekChatModelId, deepseekChatOptions, } from './deepseek-chat-options'; import { prepareTools } from './deepseek-prepare-tools'; import { getResponseMetadata } from './get-response-metadata'; import { mapDeepSeekFinishReason } from './map-deepseek-finish-reason'; export type DeepSeekChatConfig = { provider: string; headers: () => Record; url: (options: { modelId: string; path: string }) => string; fetch?: FetchFunction; }; export class DeepSeekChatLanguageModel implements LanguageModelV3 { readonly specificationVersion = 'v3'; readonly modelId: DeepSeekChatModelId; readonly supportedUrls = {}; private readonly config: DeepSeekChatConfig; private readonly failedResponseHandler: ResponseHandler; constructor(modelId: DeepSeekChatModelId, config: DeepSeekChatConfig) { this.modelId = modelId; this.config = config; this.failedResponseHandler = createJsonErrorResponseHandler({ errorSchema: deepSeekErrorSchema, errorToMessage: (error: InferSchema) => error.error.message, }); } get provider(): string { return this.config.provider; } private get providerOptionsName(): string { return this.config.provider.split('.')[0].trim(); } private async getArgs({ prompt, maxOutputTokens, temperature, topP, topK, frequencyPenalty, presencePenalty, providerOptions, stopSequences, responseFormat, seed, toolChoice, tools, }: LanguageModelV3CallOptions) { const deepseekOptions = (await parseProviderOptions({ provider: this.providerOptionsName, providerOptions, schema: deepseekChatOptions, })) ?? {}; const { messages, warnings } = convertToDeepSeekChatMessages({ prompt, responseFormat, }); if (topK != null) { warnings.push({ type: 'unsupported', feature: 'topK' }); } if (seed != null) { warnings.push({ type: 'unsupported', feature: 'seed' }); } const { tools: deepseekTools, toolChoice: deepseekToolChoices, toolWarnings, } = prepareTools({ tools, toolChoice, }); return { args: { model: this.modelId, max_tokens: maxOutputTokens, temperature, top_p: topP, frequency_penalty: frequencyPenalty, presence_penalty: presencePenalty, response_format: responseFormat?.type === 'json' ? { type: 'json_object' } : undefined, stop: stopSequences, messages, tools: deepseekTools, tool_choice: deepseekToolChoices, thinking: deepseekOptions.thinking?.type != null ? { type: deepseekOptions.thinking.type } : undefined, }, warnings: [...warnings, ...toolWarnings], }; } async doGenerate( options: LanguageModelV3CallOptions, ): Promise { const { args, warnings } = await this.getArgs({ ...options }); const { responseHeaders, value: responseBody, rawValue: rawResponse, } = await postJsonToApi({ url: this.config.url({ path: '/chat/completions', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body: args, failedResponseHandler: this.failedResponseHandler, successfulResponseHandler: createJsonResponseHandler( deepseekChatResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const choice = responseBody.choices[0]; const content: Array = []; // reasoning content (before text): const reasoning = choice.message.reasoning_content; if (reasoning != null && reasoning.length > 0) { content.push({ type: 'reasoning', text: reasoning, }); } // tool calls: if (choice.message.tool_calls != null) { for (const toolCall of choice.message.tool_calls) { content.push({ type: 'tool-call', toolCallId: toolCall.id ?? generateId(), toolName: toolCall.function.name, input: toolCall.function.arguments!, }); } } // text content: const text = choice.message.content; if (text != null && text.length > 0) { content.push({ type: 'text', text }); } return { content, finishReason: { unified: mapDeepSeekFinishReason(choice.finish_reason), raw: choice.finish_reason ?? undefined, }, usage: convertDeepSeekUsage(responseBody.usage), providerMetadata: { [this.providerOptionsName]: { promptCacheHitTokens: responseBody.usage?.prompt_cache_hit_tokens, promptCacheMissTokens: responseBody.usage?.prompt_cache_miss_tokens, }, }, request: { body: args }, response: { ...getResponseMetadata(responseBody), headers: responseHeaders, body: rawResponse, }, warnings, }; } async doStream( options: LanguageModelV3CallOptions, ): Promise { const { args, warnings } = await this.getArgs({ ...options }); const body = { ...args, stream: true, stream_options: { include_usage: true }, }; const { responseHeaders, value: response } = await postJsonToApi({ url: this.config.url({ path: '/chat/completions', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body, failedResponseHandler: this.failedResponseHandler, successfulResponseHandler: createEventSourceResponseHandler( deepseekChatChunkSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const toolCalls: Array<{ id: string; type: 'function'; function: { name: string; arguments: string; }; hasFinished: boolean; }> = []; let finishReason: LanguageModelV3FinishReason = { unified: 'other', raw: undefined, }; let usage: DeepSeekChatTokenUsage | undefined = undefined; let isFirstChunk = true; const providerOptionsName = this.providerOptionsName; let isActiveReasoning = false; let isActiveText = false; return { stream: response.pipeThrough( new TransformStream< ParseResult>, LanguageModelV3StreamPart >({ start(controller) { controller.enqueue({ type: 'stream-start', warnings }); }, transform(chunk, controller) { // Emit raw chunk if requested (before anything else) if (options.includeRawChunks) { controller.enqueue({ type: 'raw', rawValue: chunk.rawValue }); } // handle failed chunk parsing / validation: if (!chunk.success) { finishReason = { unified: 'error', raw: undefined }; controller.enqueue({ type: 'error', error: chunk.error }); return; } const value = chunk.value; // handle error chunks: if ('error' in value) { finishReason = { unified: 'error', raw: undefined }; controller.enqueue({ type: 'error', error: value.error.message }); return; } if (isFirstChunk) { isFirstChunk = false; controller.enqueue({ type: 'response-metadata', ...getResponseMetadata(value), }); } if (value.usage != null) { usage = value.usage; } const choice = value.choices[0]; if (choice?.finish_reason != null) { finishReason = { unified: mapDeepSeekFinishReason(choice.finish_reason), raw: choice.finish_reason, }; } if (choice?.delta == null) { return; } const delta = choice.delta; // enqueue reasoning before text deltas: const reasoningContent = delta.reasoning_content; if (reasoningContent) { if (!isActiveReasoning) { controller.enqueue({ type: 'reasoning-start', id: 'reasoning-0', }); isActiveReasoning = true; } controller.enqueue({ type: 'reasoning-delta', id: 'reasoning-0', delta: reasoningContent, }); } if (delta.content) { if (!isActiveText) { controller.enqueue({ type: 'text-start', id: 'txt-0' }); isActiveText = true; } // end reasoning when text starts: if (isActiveReasoning) { controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0', }); isActiveReasoning = false; } controller.enqueue({ type: 'text-delta', id: 'txt-0', delta: delta.content, }); } if (delta.tool_calls != null) { // end reasoning when tool calls start: if (isActiveReasoning) { controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0', }); isActiveReasoning = false; } for (const toolCallDelta of delta.tool_calls) { const index = toolCallDelta.index; if (toolCalls[index] == null) { if (toolCallDelta.id == null) { throw new InvalidResponseDataError({ data: toolCallDelta, message: `Expected 'id' to be a string.`, }); } if (toolCallDelta.function?.name == null) { throw new InvalidResponseDataError({ data: toolCallDelta, message: `Expected 'function.name' to be a string.`, }); } controller.enqueue({ type: 'tool-input-start', id: toolCallDelta.id, toolName: toolCallDelta.function.name, }); toolCalls[index] = { id: toolCallDelta.id, type: 'function', function: { name: toolCallDelta.function.name, arguments: toolCallDelta.function.arguments ?? '', }, hasFinished: false, }; const toolCall = toolCalls[index]; if ( toolCall.function?.name != null && toolCall.function?.arguments != null ) { // send delta if the argument text has already started: if (toolCall.function.arguments.length > 0) { controller.enqueue({ type: 'tool-input-delta', id: toolCall.id, delta: toolCall.function.arguments, }); } // check if tool call is complete // (some providers send the full tool call in one chunk): if (isParsableJson(toolCall.function.arguments)) { controller.enqueue({ type: 'tool-input-end', id: toolCall.id, }); controller.enqueue({ type: 'tool-call', toolCallId: toolCall.id ?? generateId(), toolName: toolCall.function.name, input: toolCall.function.arguments, }); toolCall.hasFinished = true; } } continue; } // existing tool call, merge if not finished const toolCall = toolCalls[index]; if (toolCall.hasFinished) { continue; } if (toolCallDelta.function?.arguments != null) { toolCall.function!.arguments += toolCallDelta.function?.arguments ?? ''; } // send delta controller.enqueue({ type: 'tool-input-delta', id: toolCall.id, delta: toolCallDelta.function.arguments ?? '', }); // check if tool call is complete if ( toolCall.function?.name != null && toolCall.function?.arguments != null && isParsableJson(toolCall.function.arguments) ) { controller.enqueue({ type: 'tool-input-end', id: toolCall.id, }); controller.enqueue({ type: 'tool-call', toolCallId: toolCall.id ?? generateId(), toolName: toolCall.function.name, input: toolCall.function.arguments, }); toolCall.hasFinished = true; } } } }, flush(controller) { if (isActiveReasoning) { controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' }); } if (isActiveText) { controller.enqueue({ type: 'text-end', id: 'txt-0' }); } // go through all tool calls and send the ones that are not finished for (const toolCall of toolCalls.filter( toolCall => !toolCall.hasFinished, )) { controller.enqueue({ type: 'tool-input-end', id: toolCall.id, }); controller.enqueue({ type: 'tool-call', toolCallId: toolCall.id ?? generateId(), toolName: toolCall.function.name, input: toolCall.function.arguments, }); } controller.enqueue({ type: 'finish', finishReason, usage: convertDeepSeekUsage(usage), providerMetadata: { [providerOptionsName]: { promptCacheHitTokens: usage?.prompt_cache_hit_tokens ?? undefined, promptCacheMissTokens: usage?.prompt_cache_miss_tokens ?? undefined, }, }, }); }, }), ), request: { body }, response: { headers: responseHeaders }, }; } }