{"repo": "berryhill/roo-code", "files": {"src/api/providers/deepseek.ts": {"05460a7f7868f7d6fdde4863b6b38edc427a2d8f": {"t": 1785746038.6457443, "n": 5233, "h": "5ac08f423b5f39c2", "c": "import { Anthropic } from \"@anthropic-ai/sdk\"\nimport OpenAI from \"openai\"\n\nimport {\n\tdeepSeekModels,\n\tdeepSeekDefaultModelId,\n\tDEEP_SEEK_DEFAULT_TEMPERATURE,\n\tOPENAI_AZURE_AI_INFERENCE_PATH,\n} from \"@roo-code/types\"\n\nimport type { ApiHandlerOptions } from \"../../shared/api\"\n\nimport { ApiStream, ApiStreamUsageChunk } from \"../transform/stream\"\nimport { getModelParams } from \"../transform/model-params\"\nimport { convertToR1Format } from \"../transform/r1-format\"\n\nimport { OpenAiHandler } from \"./openai\"\nimport type { ApiHandlerCreateMessageMetadata } from \"../index\"\n\n// Custom interface for DeepSeek params to support thinking mode\ntype DeepSeekChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParamsStreaming & {\n\tthinking?: { type: \"enabled\" | \"disabled\" }\n}\n\nexport class DeepSeekHandler extends OpenAiHandler {\n\tconstructor(options: ApiHandlerOptions) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\topenAiApiKey: options.deepSeekApiKey ?? \"not-provided\",\n\t\t\topenAiModelId: options.apiModelId ?? deepSeekDefaultModelId,\n\t\t\topenAiBaseUrl: options.deepSeekBaseUrl ?? \"https://api.deepseek.com\",\n\t\t\topenAiStreamingEnabled: true,\n\t\t\tincludeMaxTokens: true,\n\t\t})\n\t}\n\n\toverride getModel() {\n\t\tconst id = this.options.apiModelId ?? deepSeekDefaultModelId\n\t\tconst info = deepSeekModels[id as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId]\n\t\tconst params = getModelParams({ format: \"openai\", modelId: id, model: info, settings: this.options })\n\t\treturn { id, info, ...params }\n\t}\n\n\toverride async *createMessage(\n\t\tsystemPrompt: string,\n\t\tmessages: Anthropic.Messages.MessageParam[],\n\t\tmetadata?: ApiHandlerCreateMessageMetadata,\n\t): ApiStream {\n\t\tconst modelId = this.options.apiModelId ?? deepSeekDefaultModelId\n\t\tconst { info: modelInfo } = this.getModel()\n\n\t\t// Check if this is a thinking-enabled model (deepseek-reasoner)\n\t\tconst isThinkingModel = modelId.includes(\"deepseek-reasoner\")\n\n\t\t// Convert messages to R1 format (merges consecutive same-role messages)\n\t\t// This is required for DeepSeek which does not support successive messages with the same role\n\t\t// For thinking models (deepseek-reasoner), enable mergeToolResultText to preserve reasoning_content\n\t\t// during tool call sequences. Without this, environment_details text after tool_results would\n\t\t// create user messages that cause DeepSeek to drop all previous reasoning_content.\n\t\t// See: https://api-docs.deepseek.com/guides/thinking_mode\n\t\tconst convertedMessages = convertToR1Format([{ role: \"user\", content: systemPrompt }, ...messages], {\n\t\t\tmergeToolResultText: isThinkingModel,\n\t\t})\n\n\t\tconst requestOptions: DeepSeekChatCompletionParams = {\n\t\t\tmodel: modelId,\n\t\t\ttemperature: this.options.modelTemperature ?? DEEP_SEEK_DEFAULT_TEMPERATURE,\n\t\t\tmessages: convertedMessages,\n\t\t\tstream: true as const,\n\t\t\tstream_options: { include_usage: true },\n\t\t\t// Enable thinking mode for deepseek-reasoner or when tools are used with thinking model\n\t\t\t...(isThinkingModel && { thinking: { type: \"enabled\" } }),\n\t\t\t...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }),\n\t\t\t...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }),\n\t\t\t...(metadata?.toolProtocol === \"native\" && {\n\t\t\t\tparallel_tool_calls: metadata.parallelToolCalls ?? false,\n\t\t\t}),\n\t\t}\n\n\t\t// Add max_tokens if needed\n\t\tthis.addMaxTokensIfNeeded(requestOptions, modelInfo)\n\n\t\t// Check if base URL is Azure AI Inference (for DeepSeek via Azure)\n\t\tconst isAzureAiInference = this._isAzureAiInference(this.options.deepSeekBaseUrl)\n\n\t\tlet stream\n\t\ttry {\n\t\t\tstream = await this.client.chat.completions.create(\n\t\t\t\trequestOptions,\n\t\t\t\tisAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {},\n\t\t\t)\n\t\t} catch (error) {\n\t\t\tconst { handleOpenAIError } = await import(\"./utils/openai-error-handler\")\n\t\t\tthrow handleOpenAIError(error, \"DeepSeek\")\n\t\t}\n\n\t\tlet lastUsage\n\n\t\tfor await (const chunk of stream) {\n\t\t\tconst delta = chunk.choices?.[0]?.delta ?? {}\n\n\t\t\t// Handle regular text content\n\t\t\tif (delta.content) {\n\t\t\t\tyield {\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: delta.content,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle reasoning_content from DeepSeek's interleaved thinking\n\t\t\t// This is the proper way DeepSeek sends thinking content in streaming\n\t\t\tif (\"reasoning_content\" in delta && delta.reasoning_content) {\n\t\t\t\tyield {\n\t\t\t\t\ttype: \"reasoning\",\n\t\t\t\t\ttext: (delta.reasoning_content as string) || \"\",\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle tool calls\n\t\t\tif (delta.tool_calls) {\n\t\t\t\tfor (const toolCall of delta.tool_calls) {\n\t\t\t\t\tyield {\n\t\t\t\t\t\ttype: \"tool_call_partial\",\n\t\t\t\t\t\tindex: toolCall.index,\n\t\t\t\t\t\tid: toolCall.id,\n\t\t\t\t\t\tname: toolCall.function?.name,\n\t\t\t\t\t\targuments: toolCall.function?.arguments,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (chunk.usage) {\n\t\t\t\tlastUsage = chunk.usage\n\t\t\t}\n\t\t}\n\n\t\tif (lastUsage) {\n\t\t\tyield this.processUsageMetrics(lastUsage, modelInfo)\n\t\t}\n\t}\n\n\t// Override to handle DeepSeek's usage metrics, including caching.\n\tprotected override processUsageMetrics(usage: any, _modelInfo?: any): ApiStreamUsageChunk {\n\t\treturn {\n\t\t\ttype: \"usage\",\n\t\t\tinputTokens: usage?.prompt_tokens || 0,\n\t\t\toutputTokens: usage?.completion_tokens || 0,\n\t\t\tcacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,\n\t\t\tcacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,\n\t\t}\n\t}\n}\n"}, "970deead5bab060f8c63382c5d22c10c695e2dda": {"t": 1785759101.6229706, "n": 5233, "h": "5ac08f423b5f39c2", "c": "import { Anthropic } from \"@anthropic-ai/sdk\"\nimport OpenAI from \"openai\"\n\nimport {\n\tdeepSeekModels,\n\tdeepSeekDefaultModelId,\n\tDEEP_SEEK_DEFAULT_TEMPERATURE,\n\tOPENAI_AZURE_AI_INFERENCE_PATH,\n} from \"@roo-code/types\"\n\nimport type { ApiHandlerOptions } from \"../../shared/api\"\n\nimport { ApiStream, ApiStreamUsageChunk } from \"../transform/stream\"\nimport { getModelParams } from \"../transform/model-params\"\nimport { convertToR1Format } from \"../transform/r1-format\"\n\nimport { OpenAiHandler } from \"./openai\"\nimport type { ApiHandlerCreateMessageMetadata } from \"../index\"\n\n// Custom interface for DeepSeek params to support thinking mode\ntype DeepSeekChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParamsStreaming & {\n\tthinking?: { type: \"enabled\" | \"disabled\" }\n}\n\nexport class DeepSeekHandler extends OpenAiHandler {\n\tconstructor(options: ApiHandlerOptions) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\topenAiApiKey: options.deepSeekApiKey ?? \"not-provided\",\n\t\t\topenAiModelId: options.apiModelId ?? deepSeekDefaultModelId,\n\t\t\topenAiBaseUrl: options.deepSeekBaseUrl ?? \"https://api.deepseek.com\",\n\t\t\topenAiStreamingEnabled: true,\n\t\t\tincludeMaxTokens: true,\n\t\t})\n\t}\n\n\toverride getModel() {\n\t\tconst id = this.options.apiModelId ?? deepSeekDefaultModelId\n\t\tconst info = deepSeekModels[id as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId]\n\t\tconst params = getModelParams({ format: \"openai\", modelId: id, model: info, settings: this.options })\n\t\treturn { id, info, ...params }\n\t}\n\n\toverride async *createMessage(\n\t\tsystemPrompt: string,\n\t\tmessages: Anthropic.Messages.MessageParam[],\n\t\tmetadata?: ApiHandlerCreateMessageMetadata,\n\t): ApiStream {\n\t\tconst modelId = this.options.apiModelId ?? deepSeekDefaultModelId\n\t\tconst { info: modelInfo } = this.getModel()\n\n\t\t// Check if this is a thinking-enabled model (deepseek-reasoner)\n\t\tconst isThinkingModel = modelId.includes(\"deepseek-reasoner\")\n\n\t\t// Convert messages to R1 format (merges consecutive same-role messages)\n\t\t// This is required for DeepSeek which does not support successive messages with the same role\n\t\t// For thinking models (deepseek-reasoner), enable mergeToolResultText to preserve reasoning_content\n\t\t// during tool call sequences. Without this, environment_details text after tool_results would\n\t\t// create user messages that cause DeepSeek to drop all previous reasoning_content.\n\t\t// See: https://api-docs.deepseek.com/guides/thinking_mode\n\t\tconst convertedMessages = convertToR1Format([{ role: \"user\", content: systemPrompt }, ...messages], {\n\t\t\tmergeToolResultText: isThinkingModel,\n\t\t})\n\n\t\tconst requestOptions: DeepSeekChatCompletionParams = {\n\t\t\tmodel: modelId,\n\t\t\ttemperature: this.options.modelTemperature ?? DEEP_SEEK_DEFAULT_TEMPERATURE,\n\t\t\tmessages: convertedMessages,\n\t\t\tstream: true as const,\n\t\t\tstream_options: { include_usage: true },\n\t\t\t// Enable thinking mode for deepseek-reasoner or when tools are used with thinking model\n\t\t\t...(isThinkingModel && { thinking: { type: \"enabled\" } }),\n\t\t\t...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }),\n\t\t\t...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }),\n\t\t\t...(metadata?.toolProtocol === \"native\" && {\n\t\t\t\tparallel_tool_calls: metadata.parallelToolCalls ?? false,\n\t\t\t}),\n\t\t}\n\n\t\t// Add max_tokens if needed\n\t\tthis.addMaxTokensIfNeeded(requestOptions, modelInfo)\n\n\t\t// Check if base URL is Azure AI Inference (for DeepSeek via Azure)\n\t\tconst isAzureAiInference = this._isAzureAiInference(this.options.deepSeekBaseUrl)\n\n\t\tlet stream\n\t\ttry {\n\t\t\tstream = await this.client.chat.completions.create(\n\t\t\t\trequestOptions,\n\t\t\t\tisAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {},\n\t\t\t)\n\t\t} catch (error) {\n\t\t\tconst { handleOpenAIError } = await import(\"./utils/openai-error-handler\")\n\t\t\tthrow handleOpenAIError(error, \"DeepSeek\")\n\t\t}\n\n\t\tlet lastUsage\n\n\t\tfor await (const chunk of stream) {\n\t\t\tconst delta = chunk.choices?.[0]?.delta ?? {}\n\n\t\t\t// Handle regular text content\n\t\t\tif (delta.content) {\n\t\t\t\tyield {\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: delta.content,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle reasoning_content from DeepSeek's interleaved thinking\n\t\t\t// This is the proper way DeepSeek sends thinking content in streaming\n\t\t\tif (\"reasoning_content\" in delta && delta.reasoning_content) {\n\t\t\t\tyield {\n\t\t\t\t\ttype: \"reasoning\",\n\t\t\t\t\ttext: (delta.reasoning_content as string) || \"\",\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle tool calls\n\t\t\tif (delta.tool_calls) {\n\t\t\t\tfor (const toolCall of delta.tool_calls) {\n\t\t\t\t\tyield {\n\t\t\t\t\t\ttype: \"tool_call_partial\",\n\t\t\t\t\t\tindex: toolCall.index,\n\t\t\t\t\t\tid: toolCall.id,\n\t\t\t\t\t\tname: toolCall.function?.name,\n\t\t\t\t\t\targuments: toolCall.function?.arguments,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (chunk.usage) {\n\t\t\t\tlastUsage = chunk.usage\n\t\t\t}\n\t\t}\n\n\t\tif (lastUsage) {\n\t\t\tyield this.processUsageMetrics(lastUsage, modelInfo)\n\t\t}\n\t}\n\n\t// Override to handle DeepSeek's usage metrics, including caching.\n\tprotected override processUsageMetrics(usage: any, _modelInfo?: any): ApiStreamUsageChunk {\n\t\treturn {\n\t\t\ttype: \"usage\",\n\t\t\tinputTokens: usage?.prompt_tokens || 0,\n\t\t\toutputTokens: usage?.completion_tokens || 0,\n\t\t\tcacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,\n\t\t\tcacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,\n\t\t}\n\t}\n}\n"}, "d274812332bafafacc52d56665a71ece359e03bd": {"t": 1785759102.8095365, "n": 4838, "h": "2148b16f9c2421a4", "c": "import { Anthropic } from \"@anthropic-ai/sdk\"\nimport OpenAI from \"openai\"\n\nimport {\n\tdeepSeekModels,\n\tdeepSeekDefaultModelId,\n\tDEEP_SEEK_DEFAULT_TEMPERATURE,\n\tOPENAI_AZURE_AI_INFERENCE_PATH,\n} from \"@roo-code/types\"\n\nimport type { ApiHandlerOptions } from \"../../shared/api\"\n\nimport { ApiStream, ApiStreamUsageChunk } from \"../transform/stream\"\nimport { getModelParams } from \"../transform/model-params\"\nimport { convertToR1Format } from \"../transform/r1-format\"\n\nimport { OpenAiHandler } from \"./openai\"\nimport type { ApiHandlerCreateMessageMetadata } from \"../index\"\n\n// Custom interface for DeepSeek params to support thinking mode\ntype DeepSeekChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParamsStreaming & {\n\tthinking?: { type: \"enabled\" | \"disabled\" }\n}\n\nexport class DeepSeekHandler extends OpenAiHandler {\n\tconstructor(options: ApiHandlerOptions) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\topenAiApiKey: options.deepSeekApiKey ?? \"not-provided\",\n\t\t\topenAiModelId: options.apiModelId ?? deepSeekDefaultModelId,\n\t\t\topenAiBaseUrl: options.deepSeekBaseUrl ?? \"https://api.deepseek.com\",\n\t\t\topenAiStreamingEnabled: true,\n\t\t\tincludeMaxTokens: true,\n\t\t})\n\t}\n\n\toverride getModel() {\n\t\tconst id = this.options.apiModelId ?? deepSeekDefaultModelId\n\t\tconst info = deepSeekModels[id as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId]\n\t\tconst params = getModelParams({ format: \"openai\", modelId: id, model: info, settings: this.options })\n\t\treturn { id, info, ...params }\n\t}\n\n\toverride async *createMessage(\n\t\tsystemPrompt: string,\n\t\tmessages: Anthropic.Messages.MessageParam[],\n\t\tmetadata?: ApiHandlerCreateMessageMetadata,\n\t): ApiStream {\n\t\tconst modelId = this.options.apiModelId ?? deepSeekDefaultModelId\n\t\tconst { info: modelInfo } = this.getModel()\n\n\t\t// Check if this is a thinking-enabled model (deepseek-reasoner)\n\t\tconst isThinkingModel = modelId.includes(\"deepseek-reasoner\")\n\n\t\t// Convert messages to R1 format (merges consecutive same-role messages)\n\t\t// This is required for DeepSeek which does not support successive messages with the same role\n\t\tconst convertedMessages = convertToR1Format([{ role: \"user\", content: systemPrompt }, ...messages])\n\n\t\tconst requestOptions: DeepSeekChatCompletionParams = {\n\t\t\tmodel: modelId,\n\t\t\ttemperature: this.options.modelTemperature ?? DEEP_SEEK_DEFAULT_TEMPERATURE,\n\t\t\tmessages: convertedMessages,\n\t\t\tstream: true as const,\n\t\t\tstream_options: { include_usage: true },\n\t\t\t// Enable thinking mode for deepseek-reasoner or when tools are used with thinking model\n\t\t\t...(isThinkingModel && { thinking: { type: \"enabled\" } }),\n\t\t\t...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }),\n\t\t\t...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }),\n\t\t\t...(metadata?.toolProtocol === \"native\" && {\n\t\t\t\tparallel_tool_calls: metadata.parallelToolCalls ?? false,\n\t\t\t}),\n\t\t}\n\n\t\t// Add max_tokens if needed\n\t\tthis.addMaxTokensIfNeeded(requestOptions, modelInfo)\n\n\t\t// Check if base URL is Azure AI Inference (for DeepSeek via Azure)\n\t\tconst isAzureAiInference = this._isAzureAiInference(this.options.deepSeekBaseUrl)\n\n\t\tlet stream\n\t\ttry {\n\t\t\tstream = await this.client.chat.completions.create(\n\t\t\t\trequestOptions,\n\t\t\t\tisAzureAiInference ? { path: OPENAI_AZURE_AI_INFERENCE_PATH } : {},\n\t\t\t)\n\t\t} catch (error) {\n\t\t\tconst { handleOpenAIError } = await import(\"./utils/openai-error-handler\")\n\t\t\tthrow handleOpenAIError(error, \"DeepSeek\")\n\t\t}\n\n\t\tlet lastUsage\n\n\t\tfor await (const chunk of stream) {\n\t\t\tconst delta = chunk.choices?.[0]?.delta ?? {}\n\n\t\t\t// Handle regular text content\n\t\t\tif (delta.content) {\n\t\t\t\tyield {\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: delta.content,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle reasoning_content from DeepSeek's interleaved thinking\n\t\t\t// This is the proper way DeepSeek sends thinking content in streaming\n\t\t\tif (\"reasoning_content\" in delta && delta.reasoning_content) {\n\t\t\t\tyield {\n\t\t\t\t\ttype: \"reasoning\",\n\t\t\t\t\ttext: (delta.reasoning_content as string) || \"\",\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Handle tool calls\n\t\t\tif (delta.tool_calls) {\n\t\t\t\tfor (const toolCall of delta.tool_calls) {\n\t\t\t\t\tyield {\n\t\t\t\t\t\ttype: \"tool_call_partial\",\n\t\t\t\t\t\tindex: toolCall.index,\n\t\t\t\t\t\tid: toolCall.id,\n\t\t\t\t\t\tname: toolCall.function?.name,\n\t\t\t\t\t\targuments: toolCall.function?.arguments,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (chunk.usage) {\n\t\t\t\tlastUsage = chunk.usage\n\t\t\t}\n\t\t}\n\n\t\tif (lastUsage) {\n\t\t\tyield this.processUsageMetrics(lastUsage, modelInfo)\n\t\t}\n\t}\n\n\t// Override to handle DeepSeek's usage metrics, including caching.\n\tprotected override processUsageMetrics(usage: any, _modelInfo?: any): ApiStreamUsageChunk {\n\t\treturn {\n\t\t\ttype: \"usage\",\n\t\t\tinputTokens: usage?.prompt_tokens || 0,\n\t\t\toutputTokens: usage?.completion_tokens || 0,\n\t\t\tcacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,\n\t\t\tcacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,\n\t\t}\n\t}\n}\n"}, "9e3b0fd87077521ce4ab674d3f376372e169adb3": {"t": 1785759104.1536338, "n": 1425, "h": "9ea9a29661332e66", "c": "import { deepSeekModels, deepSeekDefaultModelId } from \"@roo-code/types\"\n\nimport type { ApiHandlerOptions } from \"../../shared/api\"\n\nimport type { ApiStreamUsageChunk } from \"../transform/stream\"\nimport { getModelParams } from \"../transform/model-params\"\n\nimport { OpenAiHandler } from \"./openai\"\n\nexport class DeepSeekHandler extends OpenAiHandler {\n\tconstructor(options: ApiHandlerOptions) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\topenAiApiKey: options.deepSeekApiKey ?? \"not-provided\",\n\t\t\topenAiModelId: options.apiModelId ?? deepSeekDefaultModelId,\n\t\t\topenAiBaseUrl: options.deepSeekBaseUrl ?? \"https://api.deepseek.com\",\n\t\t\topenAiStreamingEnabled: true,\n\t\t\tincludeMaxTokens: true,\n\t\t})\n\t}\n\n\toverride getModel() {\n\t\tconst id = this.options.apiModelId ?? deepSeekDefaultModelId\n\t\tconst info = deepSeekModels[id as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId]\n\t\tconst params = getModelParams({ format: \"openai\", modelId: id, model: info, settings: this.options })\n\t\treturn { id, info, ...params }\n\t}\n\n\t// Override to handle DeepSeek's usage metrics, including caching.\n\tprotected override processUsageMetrics(usage: any): ApiStreamUsageChunk {\n\t\treturn {\n\t\t\ttype: \"usage\",\n\t\t\tinputTokens: usage?.prompt_tokens || 0,\n\t\t\toutputTokens: usage?.completion_tokens || 0,\n\t\t\tcacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,\n\t\t\tcacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,\n\t\t}\n\t}\n}\n"}, "fa1e7b415cc565647b436807682ebfbc751cf788": {"t": 1785759105.5385332, "n": 1425, "h": "2d12db920e22f7ad", "c": "import { deepSeekModels, deepSeekDefaultModelId } from \"../../shared/api\"\nimport type { ApiHandlerOptions } from \"../../shared/api\"\n\nimport type { ApiStreamUsageChunk } from \"../transform/stream\"\nimport { getModelParams } from \"../transform/model-params\"\n\nimport { OpenAiHandler } from \"./openai\"\n\nexport class DeepSeekHandler extends OpenAiHandler {\n\tconstructor(options: ApiHandlerOptions) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\topenAiApiKey: options.deepSeekApiKey ?? \"not-provided\",\n\t\t\topenAiModelId: options.apiModelId ?? deepSeekDefaultModelId,\n\t\t\topenAiBaseUrl: options.deepSeekBaseUrl ?? \"https://api.deepseek.com\",\n\t\t\topenAiStreamingEnabled: true,\n\t\t\tincludeMaxTokens: true,\n\t\t})\n\t}\n\n\toverride getModel() {\n\t\tconst id = this.options.apiModelId ?? deepSeekDefaultModelId\n\t\tconst info = deepSeekModels[id as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId]\n\t\tconst params = getModelParams({ format: \"openai\", modelId: id, model: info, settings: this.options })\n\t\treturn { id, info, ...params }\n\t}\n\n\t// Override to handle DeepSeek's usage metrics, including caching.\n\tprotected override processUsageMetrics(usage: any): ApiStreamUsageChunk {\n\t\treturn {\n\t\t\ttype: \"usage\",\n\t\t\tinputTokens: usage?.prompt_tokens || 0,\n\t\t\toutputTokens: usage?.completion_tokens || 0,\n\t\t\tcacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,\n\t\t\tcacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,\n\t\t}\n\t}\n}\n"}, "21101e25bfee2256b06d049812fde6c89a4fd875": {"t": 1785759107.3959613, "n": 1407, "h": "54688870a0086d64", "c": "import { OpenAiHandler, OpenAiHandlerOptions } from \"./openai\"\nimport { deepSeekModels, deepSeekDefaultModelId, ModelInfo } from \"../../shared/api\"\nimport { ApiStreamUsageChunk } from \"../transform/stream\" // Import for type\nimport { getModelParams } from \"../index\"\n\nexport class DeepSeekHandler extends OpenAiHandler {\n\tconstructor(options: OpenAiHandlerOptions) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\topenAiApiKey: options.deepSeekApiKey ?? \"not-provided\",\n\t\t\topenAiModelId: options.apiModelId ?? deepSeekDefaultModelId,\n\t\t\topenAiBaseUrl: options.deepSeekBaseUrl ?? \"https://api.deepseek.com\",\n\t\t\topenAiStreamingEnabled: true,\n\t\t\tincludeMaxTokens: true,\n\t\t})\n\t}\n\n\toverride getModel(): { id: string; info: ModelInfo } {\n\t\tconst modelId = this.options.apiModelId ?? deepSeekDefaultModelId\n\t\tconst info = deepSeekModels[modelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId]\n\n\t\treturn {\n\t\t\tid: modelId,\n\t\t\tinfo,\n\t\t\t...getModelParams({ options: this.options, model: info }),\n\t\t}\n\t}\n\n\t// Override to handle DeepSeek's usage metrics, including caching.\n\tprotected override processUsageMetrics(usage: any): ApiStreamUsageChunk {\n\t\treturn {\n\t\t\ttype: \"usage\",\n\t\t\tinputTokens: usage?.prompt_tokens || 0,\n\t\t\toutputTokens: usage?.completion_tokens || 0,\n\t\t\tcacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,\n\t\t\tcacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,\n\t\t}\n\t}\n}\n"}, "e75fdc1ef6e47e03ebd12baad80363aa82173e63": {"t": 1785759108.9939818, "n": 1435, "h": "e094097dcd0e5922", "c": "import { OpenAiHandler, OpenAiHandlerOptions } from \"./openai\"\nimport { deepSeekModels, deepSeekDefaultModelId, ModelInfo } from \"../../shared/api\"\nimport { ApiStreamUsageChunk } from \"../transform/stream\" // Import for type\nimport { getModelParams } from \"../index\"\nimport OpenAI from \"openai\"\n\nexport class DeepSeekHandler extends OpenAiHandler {\n\tconstructor(options: OpenAiHandlerOptions) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\topenAiApiKey: options.deepSeekApiKey ?? \"not-provided\",\n\t\t\topenAiModelId: options.apiModelId ?? deepSeekDefaultModelId,\n\t\t\topenAiBaseUrl: options.deepSeekBaseUrl ?? \"https://api.deepseek.com\",\n\t\t\topenAiStreamingEnabled: true,\n\t\t\tincludeMaxTokens: true,\n\t\t})\n\t}\n\n\toverride getModel(): { id: string; info: ModelInfo } {\n\t\tconst modelId = this.options.apiModelId ?? deepSeekDefaultModelId\n\t\tconst info = deepSeekModels[modelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId]\n\n\t\treturn {\n\t\t\tid: modelId,\n\t\t\tinfo,\n\t\t\t...getModelParams({ options: this.options, model: info }),\n\t\t}\n\t}\n\n\t// Override to handle DeepSeek's usage metrics, including caching.\n\tprotected override processUsageMetrics(usage: any): ApiStreamUsageChunk {\n\t\treturn {\n\t\t\ttype: \"usage\",\n\t\t\tinputTokens: usage?.prompt_tokens || 0,\n\t\t\toutputTokens: usage?.completion_tokens || 0,\n\t\t\tcacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,\n\t\t\tcacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,\n\t\t}\n\t}\n}\n"}, "89cf2c4c58dc973f44ab53c3f2997d4f5a31b421": {"t": 1785759110.2489195, "n": 1610, "h": "67e28451c2f64578", "c": "import { OpenAiHandler, OpenAiHandlerOptions } from \"./openai\"\nimport { deepSeekModels, deepSeekDefaultModelId, ModelInfo } from \"../../shared/api\"\nimport { ApiStreamUsageChunk } from \"../transform/stream\" // Import for type\nimport { getModelParams } from \"../index\"\nimport OpenAI from \"openai\"\n\nexport class DeepSeekHandler extends OpenAiHandler {\n\tconstructor(options: OpenAiHandlerOptions) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\topenAiApiKey: options.deepSeekApiKey ?? \"not-provided\",\n\t\t\topenAiModelId: options.apiModelId ?? deepSeekDefaultModelId,\n\t\t\topenAiBaseUrl: options.deepSeekBaseUrl ?? \"https://api.deepseek.com\",\n\t\t\topenAiStreamingEnabled: true,\n\t\t\tincludeMaxTokens: true,\n\t\t})\n\t}\n\n\toverride getModel(): { id: string; info: ModelInfo } {\n\t\tconst modelId = this.options.apiModelId ?? deepSeekDefaultModelId\n\t\tconst originalInfo =\n\t\t\tdeepSeekModels[modelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId]\n\n\t\t// Apply defaults with proper typing\n\t\tconst info: ModelInfo = {\n\t\t\t...originalInfo,\n\t\t\tsupportsPromptCache: true, // DeepSeek *does* support prompt caching\n\t\t}\n\n\t\treturn {\n\t\t\tid: modelId,\n\t\t\tinfo,\n\t\t\t...getModelParams({ options: this.options, model: info }),\n\t\t}\n\t}\n\n\t// Override to handle DeepSeek's usage metrics, including caching.\n\tprotected override processUsageMetrics(usage: any): ApiStreamUsageChunk {\n\t\treturn {\n\t\t\ttype: \"usage\",\n\t\t\tinputTokens: usage?.prompt_tokens || 0,\n\t\t\toutputTokens: usage?.completion_tokens || 0,\n\t\t\tcacheWriteTokens: usage?.prompt_tokens_details?.cache_miss_tokens,\n\t\t\tcacheReadTokens: usage?.prompt_tokens_details?.cached_tokens,\n\t\t}\n\t}\n}\n"}, "2e78662e9e4743aabeab2ba7d0a50c8781ec258e": {"t": 1785759111.7802355, "n": 839, "h": "91602b2fcb8c254d", "c": "import { OpenAiHandler, OpenAiHandlerOptions } from \"./openai\"\nimport { ModelInfo } from \"../../shared/api\"\nimport { deepSeekModels, deepSeekDefaultModelId } from \"../../shared/api\"\n\nexport class DeepSeekHandler extends OpenAiHandler {\n\tconstructor(options: OpenAiHandlerOptions) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\topenAiApiKey: options.deepSeekApiKey ?? \"not-provided\",\n\t\t\topenAiModelId: options.apiModelId ?? deepSeekDefaultModelId,\n\t\t\topenAiBaseUrl: options.deepSeekBaseUrl ?? \"https://api.deepseek.com\",\n\t\t\topenAiStreamingEnabled: true,\n\t\t\tincludeMaxTokens: true,\n\t\t})\n\t}\n\n\toverride getModel(): { id: string; info: ModelInfo } {\n\t\tconst modelId = this.options.apiModelId ?? deepSeekDefaultModelId\n\t\treturn {\n\t\t\tid: modelId,\n\t\t\tinfo: deepSeekModels[modelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId],\n\t\t}\n\t}\n}\n"}, "41fcf85c484fbee6fb4f75c13c4a7d0118151030": {"t": 1785759113.001351, "n": 842, "h": "4303c6fc0a9d02b9", "c": "import { OpenAiHandler, OpenAiHandlerOptions } from \"./openai\"\nimport { ModelInfo } from \"../../shared/api\"\nimport { deepSeekModels, deepSeekDefaultModelId } from \"../../shared/api\"\n\nexport class DeepSeekHandler extends OpenAiHandler {\n\tconstructor(options: OpenAiHandlerOptions) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\topenAiApiKey: options.deepSeekApiKey ?? \"not-provided\",\n\t\t\topenAiModelId: options.apiModelId ?? deepSeekDefaultModelId,\n\t\t\topenAiBaseUrl: options.deepSeekBaseUrl ?? \"https://api.deepseek.com/v1\",\n\t\t\topenAiStreamingEnabled: true,\n\t\t\tincludeMaxTokens: true,\n\t\t})\n\t}\n\n\toverride getModel(): { id: string; info: ModelInfo } {\n\t\tconst modelId = this.options.apiModelId ?? deepSeekDefaultModelId\n\t\treturn {\n\t\t\tid: modelId,\n\t\t\tinfo: deepSeekModels[modelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId],\n\t\t}\n\t}\n}\n"}, "4b3ea070339c8561ed6479cd5c8f55712a27e9d1": {"t": 1785759114.3597772, "n": 836, "h": "69844302b36344b9", "c": "import { OpenAiHandler } from \"./openai\"\nimport { ApiHandlerOptions, ModelInfo } from \"../../shared/api\"\nimport { deepSeekModels, deepSeekDefaultModelId } from \"../../shared/api\"\n\nexport class DeepSeekHandler extends OpenAiHandler {\n\tconstructor(options: ApiHandlerOptions) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\topenAiApiKey: options.deepSeekApiKey ?? \"not-provided\",\n\t\t\topenAiModelId: options.apiModelId ?? deepSeekDefaultModelId,\n\t\t\topenAiBaseUrl: options.deepSeekBaseUrl ?? \"https://api.deepseek.com/v1\",\n\t\t\topenAiStreamingEnabled: true,\n\t\t\tincludeMaxTokens: true,\n\t\t})\n\t}\n\n\toverride getModel(): { id: string; info: ModelInfo } {\n\t\tconst modelId = this.options.apiModelId ?? deepSeekDefaultModelId\n\t\treturn {\n\t\t\tid: modelId,\n\t\t\tinfo: deepSeekModels[modelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId],\n\t\t}\n\t}\n}\n"}, "a13da25dd10f8d552c862aab71dc89c2910269d7": {"t": 1785759116.3558528, "n": 942, "h": "c0b61df28d9fd193", "c": "import { OpenAiHandler } from \"./openai\"\nimport { ApiHandlerOptions, ModelInfo } from \"../../shared/api\"\nimport { deepSeekModels, deepSeekDefaultModelId } from \"../../shared/api\"\n\nexport class DeepSeekHandler extends OpenAiHandler {\n\tconstructor(options: ApiHandlerOptions) {\n\t\tif (!options.deepSeekApiKey) {\n\t\t\tthrow new Error(\"DeepSeek API key is required. Please provide it in the settings.\")\n\t\t}\n\t\tsuper({\n\t\t\t...options,\n\t\t\topenAiApiKey: options.deepSeekApiKey,\n\t\t\topenAiModelId: options.apiModelId ?? deepSeekDefaultModelId,\n\t\t\topenAiBaseUrl: options.deepSeekBaseUrl ?? \"https://api.deepseek.com/v1\",\n\t\t\topenAiStreamingEnabled: true,\n\t\t\tincludeMaxTokens: true,\n\t\t})\n\t}\n\n\toverride getModel(): { id: string; info: ModelInfo } {\n\t\tconst modelId = this.options.apiModelId ?? deepSeekDefaultModelId\n\t\treturn {\n\t\t\tid: modelId,\n\t\t\tinfo: deepSeekModels[modelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId],\n\t\t}\n\t}\n}\n"}, "60a0a824b96a0b326af4d8871b6903f4ddcfe114": {"t": 1785759117.9223864, "n": 919, "h": "c49aed6965926fc6", "c": "import { OpenAiHandler } from \"./openai\"\nimport { ApiHandlerOptions, ModelInfo } from \"../../shared/api\"\nimport { deepSeekModels, deepSeekDefaultModelId } from \"../../shared/api\"\n\nexport class DeepSeekHandler extends OpenAiHandler {\n\tconstructor(options: ApiHandlerOptions) {\n\t\tif (!options.deepSeekApiKey) {\n\t\t\tthrow new Error(\"DeepSeek API key is required. Please provide it in the settings.\")\n\t\t}\n\t\tsuper({\n\t\t\t...options,\n\t\t\topenAiApiKey: options.deepSeekApiKey,\n\t\t\topenAiModelId: options.deepSeekModelId ?? deepSeekDefaultModelId,\n\t\t\topenAiBaseUrl: options.deepSeekBaseUrl ?? \"https://api.deepseek.com/v1\",\n\t\t\tincludeMaxTokens: true,\n\t\t})\n\t}\n\n\toverride getModel(): { id: string; info: ModelInfo } {\n\t\tconst modelId = this.options.deepSeekModelId ?? deepSeekDefaultModelId\n\t\treturn {\n\t\t\tid: modelId,\n\t\t\tinfo: deepSeekModels[modelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId],\n\t\t}\n\t}\n}\n"}, "6290f90fa529d964ef1bc1df939f35e75bb67b16": {"t": 1785759119.0502672, "n": 1043, "h": "b5e4c84e9ccdf901", "c": "import { OpenAiHandler } from \"./openai\"\nimport { ApiHandlerOptions, ModelInfo } from \"../../shared/api\"\nimport { deepSeekModels, deepSeekDefaultModelId } from \"../../shared/api\"\n\nexport class DeepSeekHandler extends OpenAiHandler {\n constructor(options: ApiHandlerOptions) {\n if (!options.deepSeekApiKey) {\n throw new Error(\"DeepSeek API key is required. Please provide it in the settings.\")\n }\n super({\n ...options,\n openAiApiKey: options.deepSeekApiKey,\n openAiModelId: options.deepSeekModelId ?? deepSeekDefaultModelId,\n openAiBaseUrl: options.deepSeekBaseUrl ?? \"https://api.deepseek.com/v1\",\n includeMaxTokens: true\n })\n }\n\n override getModel(): { id: string; info: ModelInfo } {\n const modelId = this.options.deepSeekModelId ?? deepSeekDefaultModelId\n return {\n id: modelId,\n info: deepSeekModels[modelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId]\n }\n }\n}\n"}, "25987dd40b4726c579cd4a896b4ed6165d78bde4": {"t": 1785759120.4340076, "n": 2850, "h": "3fff0178515fc41c", "c": "import { Anthropic } from \"@anthropic-ai/sdk\"\nimport OpenAI from \"openai\"\nimport { ApiHandlerOptions, ModelInfo, deepSeekModels, deepSeekDefaultModelId } from \"../../shared/api\"\nimport { ApiHandler } from \"../index\"\nimport { ApiStream } from \"../transform/stream\"\n\nexport class DeepSeekHandler implements ApiHandler {\n\tprivate options: ApiHandlerOptions\n\tprivate client: OpenAI\n\n\tconstructor(options: ApiHandlerOptions) {\n\t\tthis.options = options\n\t\tif (!options.deepSeekApiKey) {\n\t\t\tthrow new Error(\"DeepSeek API key is required. Please provide it in the settings.\")\n\t\t}\n\t\tthis.client = new OpenAI({\n\t\t\tbaseURL: this.options.deepSeekBaseUrl ?? \"https://api.deepseek.com/v1\",\n\t\t\tapiKey: this.options.deepSeekApiKey,\n\t\t})\n\t}\n\n\tasync *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {\n\t\tconst modelInfo = deepSeekModels[this.options.deepSeekModelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId]\n\n\t\t// Format all messages\n\t\tconst messagesToInclude: OpenAI.Chat.ChatCompletionMessageParam[] = [\n\t\t\t{ role: 'system' as const, content: systemPrompt }\n\t\t]\n\n\t\t// Add the rest of the messages\n\t\tfor (const msg of messages) {\n\t\t\tlet messageContent = \"\"\n\t\t\tif (typeof msg.content === \"string\") {\n\t\t\t\tmessageContent = msg.content\n\t\t\t} else if (Array.isArray(msg.content)) {\n\t\t\t\tmessageContent = msg.content.reduce((acc, part) => {\n\t\t\t\t\tif (part.type === \"text\") {\n\t\t\t\t\t\treturn acc + part.text\n\t\t\t\t\t}\n\t\t\t\t\treturn acc\n\t\t\t\t}, \"\")\n\t\t\t}\n\t\t\t\n\t\t\tmessagesToInclude.push({\n\t\t\t\trole: msg.role === 'user' ? 'user' as const : 'assistant' as const,\n\t\t\t\tcontent: messageContent\n\t\t\t})\n\t\t}\n\n\t\tconst requestOptions: OpenAI.Chat.ChatCompletionCreateParamsStreaming = {\n\t\t\tmodel: this.options.deepSeekModelId ?? \"deepseek-chat\",\n\t\t\tmessages: messagesToInclude,\n\t\t\ttemperature: 0,\n\t\t\tstream: true,\n\t\t\tmax_tokens: modelInfo.maxTokens,\n\t\t}\n\n\t\tif (this.options.includeStreamOptions ?? true) {\n\t\t\trequestOptions.stream_options = { include_usage: true }\n\t\t}\n\n\t\tlet totalInputTokens = 0;\n\t\tlet totalOutputTokens = 0;\n\n\t\ttry {\n\t\t\tconst stream = await this.client.chat.completions.create(requestOptions)\n\t\t\tfor await (const chunk of stream) {\n\t\t\t\tconst delta = chunk.choices[0]?.delta\n\t\t\t\tif (delta?.content) {\n\t\t\t\t\tyield {\n\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\ttext: delta.content,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (chunk.usage) {\n\t\t\t\t\tyield {\n\t\t\t\t\t\ttype: \"usage\",\n\t\t\t\t\t\tinputTokens: chunk.usage.prompt_tokens || 0,\n\t\t\t\t\t\toutputTokens: chunk.usage.completion_tokens || 0,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error(\"DeepSeek API Error:\", error)\n\t\t\tthrow error\n\t\t}\n\t}\n\n\tgetModel(): { id: string; info: ModelInfo } {\n\t\tconst modelId = this.options.deepSeekModelId ?? deepSeekDefaultModelId\n\t\treturn {\n\t\t\tid: modelId,\n\t\t\tinfo: deepSeekModels[modelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId],\n\t\t}\n\t}\n}\n"}, "eb8c4cc50f5eb48ed761bfe50e4e60c66365ffd9": {"t": 1785759122.197304, "n": 3577, "h": "3ce8b1d9c0c9f3ae", "c": "import { Anthropic } from \"@anthropic-ai/sdk\"\nimport OpenAI from \"openai\"\nimport { ApiHandlerOptions, ModelInfo, deepSeekModels, deepSeekDefaultModelId } from \"../../shared/api\"\nimport { ApiHandler } from \"../index\"\nimport { ApiStream } from \"../transform/stream\"\n\nexport class DeepSeekHandler implements ApiHandler {\n\tprivate options: ApiHandlerOptions\n\tprivate client: OpenAI\n\n\tconstructor(options: ApiHandlerOptions) {\n\t\tthis.options = options\n\t\tif (!options.deepSeekApiKey) {\n\t\t\tthrow new Error(\"DeepSeek API key is required. Please provide it in the settings.\")\n\t\t}\n\t\tthis.client = new OpenAI({\n\t\t\tbaseURL: this.options.deepSeekBaseUrl ?? \"https://api.deepseek.com/v1\",\n\t\t\tapiKey: this.options.deepSeekApiKey,\n\t\t})\n\t}\n\n\tasync *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {\n\t\t// Convert messages to simple format that DeepSeek expects\n\t\tconst formattedMessages = messages.map(msg => {\n\t\t\tif (typeof msg.content === \"string\") {\n\t\t\t\treturn { role: msg.role, content: msg.content }\n\t\t\t}\n\t\t\t// For array content, concatenate text parts\n\t\t\treturn {\n\t\t\t\trole: msg.role,\n\t\t\t\tcontent: msg.content.reduce((acc, part) => {\n\t\t\t\t\tif (part.type === \"text\") {\n\t\t\t\t\t\treturn acc + part.text\n\t\t\t\t\t}\n\t\t\t\t\treturn acc\n\t\t\t\t}, \"\")\n\t\t\t}\n\t\t})\n\n\t\tconst openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [\n\t\t\t{ role: \"system\", content: systemPrompt },\n\t\t\t...formattedMessages,\n\t\t]\n\t\tconst modelInfo = deepSeekModels[this.options.deepSeekModelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId]\n\t\t\n\t\tconst contextWindow = modelInfo.contextWindow || 64_000\n\t\tconst getTokenCount = (content: string) => Math.ceil(content.length * 0.3)\n\n\t\t// Always keep system prompt\n\t\tconst systemMsg = openAiMessages[0]\n\t\tlet availableTokens = contextWindow - getTokenCount(typeof systemMsg.content === 'string' ? systemMsg.content : '')\n\t\t\n\t\t// Start with most recent messages and work backwards\n\t\tconst userMessages = openAiMessages.slice(1).reverse()\n\t\tconst includedMessages = []\n\t\tlet truncated = false\n\n\t\tfor (const msg of userMessages) {\n\t\t\tconst content = typeof msg.content === 'string' ? msg.content : ''\n\t\t\tconst tokens = getTokenCount(content)\n\t\t\t\n\t\t\tif (tokens <= availableTokens) {\n\t\t\t\tincludedMessages.unshift(msg)\n\t\t\t\tavailableTokens -= tokens\n\t\t\t} else {\n\t\t\t\ttruncated = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif (truncated) {\n\t\t\tyield {\n\t\t\t\ttype: 'text',\n\t\t\t\ttext: '(Note: Some earlier messages were truncated to fit within the model\\'s context window)\\n\\n'\n\t\t\t}\n\t\t}\n\n\t\tconst requestOptions: OpenAI.Chat.ChatCompletionCreateParamsStreaming = {\n\t\t\tmodel: this.options.deepSeekModelId ?? \"deepseek-chat\",\n\t\t\tmessages: [systemMsg, ...includedMessages],\n\t\t\ttemperature: 0,\n\t\t\tstream: true,\n\t\t\tmax_tokens: modelInfo.maxTokens,\n\t\t}\n\n\t\tif (this.options.includeStreamOptions ?? true) {\n\t\t\trequestOptions.stream_options = { include_usage: true }\n\t\t}\n\n\t\tconst stream = await this.client.chat.completions.create(requestOptions)\n\t\tfor await (const chunk of stream) {\n\t\t\tconst delta = chunk.choices[0]?.delta\n\t\t\tif (delta?.content) {\n\t\t\t\tyield {\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: delta.content,\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (chunk.usage) {\n\t\t\t\tyield {\n\t\t\t\t\ttype: \"usage\",\n\t\t\t\t\tinputTokens: chunk.usage.prompt_tokens || 0,\n\t\t\t\t\toutputTokens: chunk.usage.completion_tokens || 0,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tgetModel(): { id: string; info: ModelInfo } {\n\t\tconst modelId = this.options.deepSeekModelId ?? deepSeekDefaultModelId\n\t\treturn {\n\t\t\tid: modelId,\n\t\t\tinfo: deepSeekModels[modelId as keyof typeof deepSeekModels] || deepSeekModels[deepSeekDefaultModelId],\n\t\t}\n\t}\n}"}}, "src/core/condense/__tests__/condense.spec.ts": {"05460a7f7868f7d6fdde4863b6b38edc427a2d8f": {"t": 1786068448.9122999, "n": 10913, "h": "edae586bcbbecc19", "c": "// npx vitest src/core/condense/__tests__/condense.spec.ts\n\nimport { Anthropic } from \"@anthropic-ai/sdk\"\nimport type { ModelInfo } from \"@roo-code/types\"\nimport { TelemetryService } from \"@roo-code/telemetry\"\n\nimport { BaseProvider } from \"../../../api/providers/base-provider\"\nimport { ApiMessage } from \"../../task-persistence/apiMessages\"\nimport {\n\tsummarizeConversation,\n\tgetMessagesSinceLastSummary,\n\tgetEffectiveApiHistory,\n\tN_MESSAGES_TO_KEEP,\n} from \"../index\"\n\n// Create a mock ApiHandler for testing\nclass MockApiHandler extends BaseProvider {\n\tcreateMessage(): any {\n\t\t// Mock implementation for testing - returns an async iterable stream\n\t\tconst mockStream = {\n\t\t\tasync *[Symbol.asyncIterator]() {\n\t\t\t\tyield { type: \"text\", text: \"Mock summary of the conversation\" }\n\t\t\t\tyield { type: \"usage\", inputTokens: 100, outputTokens: 50, totalCost: 0.01 }\n\t\t\t},\n\t\t}\n\t\treturn mockStream\n\t}\n\n\tgetModel(): { id: string; info: ModelInfo } {\n\t\treturn {\n\t\t\tid: \"test-model\",\n\t\t\tinfo: {\n\t\t\t\tcontextWindow: 100000,\n\t\t\t\tmaxTokens: 50000,\n\t\t\t\tsupportsPromptCache: true,\n\t\t\t\tsupportsImages: false,\n\t\t\t\tinputPrice: 0,\n\t\t\t\toutputPrice: 0,\n\t\t\t\tdescription: \"Test model\",\n\t\t\t},\n\t\t}\n\t}\n\n\toverride async countTokens(content: Array): Promise {\n\t\t// Simple token counting for testing\n\t\tlet tokens = 0\n\t\tfor (const block of content) {\n\t\t\tif (block.type === \"text\") {\n\t\t\t\ttokens += Math.ceil(block.text.length / 4) // Rough approximation\n\t\t\t}\n\t\t}\n\t\treturn tokens\n\t}\n}\n\nconst mockApiHandler = new MockApiHandler()\nconst taskId = \"test-task-id\"\n\ndescribe(\"Condense\", () => {\n\tbeforeEach(() => {\n\t\tif (!TelemetryService.hasInstance()) {\n\t\t\tTelemetryService.createInstance([])\n\t\t}\n\t})\n\n\tdescribe(\"summarizeConversation\", () => {\n\t\tit(\"should preserve the first message when summarizing\", async () => {\n\t\t\tconst messages: ApiMessage[] = [\n\t\t\t\t{ role: \"user\", content: \"First message with /prr command content\" },\n\t\t\t\t{ role: \"assistant\", content: \"Second message\" },\n\t\t\t\t{ role: \"user\", content: \"Third message\" },\n\t\t\t\t{ role: \"assistant\", content: \"Fourth message\" },\n\t\t\t\t{ role: \"user\", content: \"Fifth message\" },\n\t\t\t\t{ role: \"assistant\", content: \"Sixth message\" },\n\t\t\t\t{ role: \"user\", content: \"Seventh message\" },\n\t\t\t\t{ role: \"assistant\", content: \"Eighth message\" },\n\t\t\t\t{ role: \"user\", content: \"Ninth message\" },\n\t\t\t]\n\n\t\t\tconst result = await summarizeConversation(messages, mockApiHandler, \"System prompt\", taskId, 5000, false)\n\n\t\t\t// Verify the first message is preserved\n\t\t\texpect(result.messages[0]).toEqual(messages[0])\n\t\t\texpect(result.messages[0].content).toBe(\"First message with /prr command content\")\n\n\t\t\t// Verify we have a summary message\n\t\t\tconst summaryMessage = result.messages.find((msg) => msg.isSummary)\n\t\t\texpect(summaryMessage).toBeTruthy()\n\t\t\t// Summary content is now always an array with a synthetic reasoning block + text block\n\t\t\t// for DeepSeek-reasoner compatibility\n\t\t\texpect(Array.isArray(summaryMessage?.content)).toBe(true)\n\t\t\tconst contentArray = summaryMessage?.content as Anthropic.Messages.ContentBlockParam[]\n\t\t\texpect(contentArray).toHaveLength(2)\n\t\t\texpect(contentArray[0]).toEqual({\n\t\t\t\ttype: \"reasoning\",\n\t\t\t\ttext: \"Condensing conversation context. The summary below captures the key information from the prior conversation.\",\n\t\t\t})\n\t\t\texpect(contentArray[1]).toEqual({\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: \"Mock summary of the conversation\",\n\t\t\t})\n\n\t\t\t// With non-destructive condensing, all messages are retained (tagged but not deleted)\n\t\t\t// Use getEffectiveApiHistory to verify the effective view matches the old behavior\n\t\t\texpect(result.messages.length).toBe(messages.length + 1) // All original messages + summary\n\t\t\tconst effectiveHistory = getEffectiveApiHistory(result.messages)\n\t\t\texpect(effectiveHistory.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // first + summary + last N\n\n\t\t\t// Verify the last N messages are preserved (same messages by reference)\n\t\t\tconst lastMessages = result.messages.slice(-N_MESSAGES_TO_KEEP)\n\t\t\texpect(lastMessages).toEqual(messages.slice(-N_MESSAGES_TO_KEEP))\n\t\t})\n\n\t\tit(\"should preserve slash command content in the first message\", async () => {\n\t\t\tconst slashCommandContent = \"/prr #123 - Fix authentication bug\"\n\t\t\tconst messages: ApiMessage[] = [\n\t\t\t\t{ role: \"user\", content: slashCommandContent },\n\t\t\t\t{ role: \"assistant\", content: \"I'll help you fix that authentication bug\" },\n\t\t\t\t{ role: \"user\", content: \"The issue is with JWT tokens\" },\n\t\t\t\t{ role: \"assistant\", content: \"Let me examine the JWT implementation\" },\n\t\t\t\t{ role: \"user\", content: \"It's failing on refresh\" },\n\t\t\t\t{ role: \"assistant\", content: \"I found the issue\" },\n\t\t\t\t{ role: \"user\", content: \"Great, can you fix it?\" },\n\t\t\t\t{ role: \"assistant\", content: \"Here's the fix\" },\n\t\t\t\t{ role: \"user\", content: \"Thanks!\" },\n\t\t\t]\n\n\t\t\tconst result = await summarizeConversation(messages, mockApiHandler, \"System prompt\", taskId, 5000, false)\n\n\t\t\t// The first message with slash command should be intact\n\t\t\texpect(result.messages[0].content).toBe(slashCommandContent)\n\t\t\texpect(result.messages[0]).toEqual(messages[0])\n\t\t})\n\n\t\tit(\"should handle complex first message content\", async () => {\n\t\t\tconst complexContent: Anthropic.Messages.ContentBlockParam[] = [\n\t\t\t\t{ type: \"text\", text: \"/mode code\" },\n\t\t\t\t{ type: \"text\", text: \"Additional context from the user\" },\n\t\t\t]\n\n\t\t\tconst messages: ApiMessage[] = [\n\t\t\t\t{ role: \"user\", content: complexContent },\n\t\t\t\t{ role: \"assistant\", content: \"Switching to code mode\" },\n\t\t\t\t{ role: \"user\", content: \"Write a function\" },\n\t\t\t\t{ role: \"assistant\", content: \"Here's the function\" },\n\t\t\t\t{ role: \"user\", content: \"Add error handling\" },\n\t\t\t\t{ role: \"assistant\", content: \"Added error handling\" },\n\t\t\t\t{ role: \"user\", content: \"Add tests\" },\n\t\t\t\t{ role: \"assistant\", content: \"Tests added\" },\n\t\t\t\t{ role: \"user\", content: \"Perfect!\" },\n\t\t\t]\n\n\t\t\tconst result = await summarizeConversation(messages, mockApiHandler, \"System prompt\", taskId, 5000, false)\n\n\t\t\t// The first message with complex content should be preserved\n\t\t\texpect(result.messages[0].content).toEqual(complexContent)\n\t\t\texpect(result.messages[0]).toEqual(messages[0])\n\t\t})\n\n\t\tit(\"should return error when not enough messages to summarize\", async () => {\n\t\t\tconst messages: ApiMessage[] = [\n\t\t\t\t{ role: \"user\", content: \"First message with /command\" },\n\t\t\t\t{ role: \"assistant\", content: \"Second message\" },\n\t\t\t\t{ role: \"user\", content: \"Third message\" },\n\t\t\t\t{ role: \"assistant\", content: \"Fourth message\" },\n\t\t\t]\n\n\t\t\tconst result = await summarizeConversation(messages, mockApiHandler, \"System prompt\", taskId, 5000, false)\n\n\t\t\t// Should return an error since we have only 4 messages (first + 3 to keep)\n\t\t\texpect(result.error).toBeDefined()\n\t\t\texpect(result.messages).toEqual(messages) // Original messages unchanged\n\t\t\texpect(result.summary).toBe(\"\")\n\t\t})\n\n\t\tit(\"should not summarize messages that already contain a recent summary\", async () => {\n\t\t\tconst messages: ApiMessage[] = [\n\t\t\t\t{ role: \"user\", content: \"First message with /command\" },\n\t\t\t\t{ role: \"assistant\", content: \"Old message\" },\n\t\t\t\t{ role: \"user\", content: \"Message before summary\" },\n\t\t\t\t{ role: \"assistant\", content: \"Response\" },\n\t\t\t\t{ role: \"user\", content: \"Another message\" },\n\t\t\t\t{ role: \"assistant\", content: \"Previous summary\", isSummary: true }, // Summary in last N messages\n\t\t\t\t{ role: \"user\", content: \"Final message\" },\n\t\t\t]\n\n\t\t\tconst result = await summarizeConversation(messages, mockApiHandler, \"System prompt\", taskId, 5000, false)\n\n\t\t\t// Should return an error due to recent summary in last N messages\n\t\t\texpect(result.error).toBeDefined()\n\t\t\texpect(result.messages).toEqual(messages)\n\t\t\texpect(result.summary).toBe(\"\")\n\t\t})\n\n\t\tit(\"should handle empty summary from API gracefully\", async () => {\n\t\t\t// Mock handler that returns empty summary\n\t\t\tclass EmptyMockApiHandler extends MockApiHandler {\n\t\t\t\toverride createMessage(): any {\n\t\t\t\t\tconst mockStream = {\n\t\t\t\t\t\tasync *[Symbol.asyncIterator]() {\n\t\t\t\t\t\t\tyield { type: \"text\", text: \"\" }\n\t\t\t\t\t\t\tyield { type: \"usage\", inputTokens: 100, outputTokens: 0, totalCost: 0.01 }\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\treturn mockStream\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst emptyHandler = new EmptyMockApiHandler()\n\t\t\tconst messages: ApiMessage[] = [\n\t\t\t\t{ role: \"user\", content: \"First message\" },\n\t\t\t\t{ role: \"assistant\", content: \"Second\" },\n\t\t\t\t{ role: \"user\", content: \"Third\" },\n\t\t\t\t{ role: \"assistant\", content: \"Fourth\" },\n\t\t\t\t{ role: \"user\", content: \"Fifth\" },\n\t\t\t\t{ role: \"assistant\", content: \"Sixth\" },\n\t\t\t\t{ role: \"user\", content: \"Seventh\" },\n\t\t\t]\n\n\t\t\tconst result = await summarizeConversation(messages, emptyHandler, \"System prompt\", taskId, 5000, false)\n\n\t\t\texpect(result.error).toBeDefined()\n\t\t\texpect(result.messages).toEqual(messages)\n\t\t\texpect(result.cost).toBeGreaterThan(0)\n\t\t})\n\t})\n\n\tdescribe(\"getMessagesSinceLastSummary\", () => {\n\t\tit(\"should return all messages when no summary exists\", () => {\n\t\t\tconst messages: ApiMessage[] = [\n\t\t\t\t{ role: \"user\", content: \"First message\" },\n\t\t\t\t{ role: \"assistant\", content: \"Second message\" },\n\t\t\t\t{ role: \"user\", content: \"Third message\" },\n\t\t\t]\n\n\t\t\tconst result = getMessagesSinceLastSummary(messages)\n\t\t\texpect(result).toEqual(messages)\n\t\t})\n\n\t\tit(\"should return messages since last summary including the summary\", () => {\n\t\t\tconst messages: ApiMessage[] = [\n\t\t\t\t{ role: \"user\", content: \"First message\" },\n\t\t\t\t{ role: \"assistant\", content: \"Second message\" },\n\t\t\t\t{ role: \"assistant\", content: \"Summary content\", isSummary: true },\n\t\t\t\t{ role: \"user\", content: \"Message after summary\" },\n\t\t\t\t{ role: \"assistant\", content: \"Final message\" },\n\t\t\t]\n\n\t\t\tconst result = getMessagesSinceLastSummary(messages)\n\n\t\t\t// Should include the original first user message for context preservation, the summary, and messages after\n\t\t\texpect(result[0].role).toBe(\"user\")\n\t\t\texpect(result[0].content).toBe(\"First message\") // Preserves original first message\n\t\t\texpect(result[1]).toEqual(messages[2]) // The summary\n\t\t\texpect(result[2]).toEqual(messages[3])\n\t\t\texpect(result[3]).toEqual(messages[4])\n\t\t})\n\n\t\tit(\"should handle multiple summaries and return from the last one\", () => {\n\t\t\tconst messages: ApiMessage[] = [\n\t\t\t\t{ role: \"user\", content: \"First message\" },\n\t\t\t\t{ role: \"assistant\", content: \"First summary\", isSummary: true },\n\t\t\t\t{ role: \"user\", content: \"Middle message\" },\n\t\t\t\t{ role: \"assistant\", content: \"Second summary\", isSummary: true },\n\t\t\t\t{ role: \"user\", content: \"Recent message\" },\n\t\t\t\t{ role: \"assistant\", content: \"Final message\" },\n\t\t\t]\n\n\t\t\tconst result = getMessagesSinceLastSummary(messages)\n\n\t\t\t// Should only include from the last summary with original first message preserved\n\t\t\texpect(result[0].role).toBe(\"user\")\n\t\t\texpect(result[0].content).toBe(\"First message\") // Preserves original first message\n\t\t\texpect(result[1]).toEqual(messages[3]) // Second summary\n\t\t\texpect(result[2]).toEqual(messages[4])\n\t\t\texpect(result[3]).toEqual(messages[5])\n\t\t})\n\t})\n})\n"}}}}