@drax/ai-back 3.54.1 → 3.57.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.
Files changed (35) hide show
  1. package/dist/config/AiConfig.js +6 -0
  2. package/dist/factory/ai/AiProviderFactory.js +6 -1
  3. package/dist/index.js +2 -1
  4. package/dist/providers/ai/AbstractAiProvider.js +53 -0
  5. package/dist/providers/ai/DeepSeekAiProvider.js +209 -3
  6. package/dist/providers/ai/GoogleAiProvider.js +4 -20
  7. package/dist/providers/ai/OllamaAiProvider.js +4 -43
  8. package/dist/providers/ai/OpenAiProvider.js +4 -40
  9. package/package.json +3 -3
  10. package/src/config/AiConfig.ts +10 -0
  11. package/src/factory/ai/AiProviderFactory.ts +6 -1
  12. package/src/index.ts +2 -0
  13. package/src/providers/ai/AbstractAiProvider.ts +69 -0
  14. package/src/providers/ai/DeepSeekAiProvider.ts +263 -5
  15. package/src/providers/ai/GoogleAiProvider.ts +4 -26
  16. package/src/providers/ai/OllamaAiProvider.ts +4 -53
  17. package/src/providers/ai/OpenAiProvider.ts +4 -50
  18. package/test/DeepSeekProvider.test.ts +94 -0
  19. package/test/GoogleAiProvider.test.ts +17 -1
  20. package/tsconfig.tsbuildinfo +1 -1
  21. package/types/config/AiConfig.d.ts +6 -0
  22. package/types/config/AiConfig.d.ts.map +1 -0
  23. package/types/controllers/AICrudController.d.ts +1 -1
  24. package/types/factory/ai/AiProviderFactory.d.ts.map +1 -1
  25. package/types/index.d.ts +2 -1
  26. package/types/index.d.ts.map +1 -1
  27. package/types/providers/ai/AbstractAiProvider.d.ts +11 -1
  28. package/types/providers/ai/AbstractAiProvider.d.ts.map +1 -1
  29. package/types/providers/ai/DeepSeekAiProvider.d.ts +65 -2
  30. package/types/providers/ai/DeepSeekAiProvider.d.ts.map +1 -1
  31. package/types/providers/ai/GoogleAiProvider.d.ts.map +1 -1
  32. package/types/providers/ai/OllamaAiProvider.d.ts +0 -2
  33. package/types/providers/ai/OllamaAiProvider.d.ts.map +1 -1
  34. package/types/providers/ai/OpenAiProvider.d.ts +0 -2
  35. package/types/providers/ai/OpenAiProvider.d.ts.map +1 -1
@@ -3,6 +3,7 @@ import type {
3
3
  IAIProvider,
4
4
  IPromptParams,
5
5
  IPromptResponse,
6
+ IPromptTool,
6
7
  } from "../../interfaces/IAIProvider.js";
7
8
  import type {AILogService} from "../../services/AILogService.js";
8
9
 
@@ -30,6 +31,74 @@ abstract class AbstractAiProvider implements IAIProvider {
30
31
 
31
32
  abstract prompt(input: IPromptParams): Promise<IPromptResponse>
32
33
 
34
+ protected buildSystemPrompt(input: IPromptParams){
35
+ if(!input.systemPrompt){
36
+ throw new Error("systemPrompt required")
37
+ }
38
+
39
+ let systemPrompt = input.systemPrompt
40
+
41
+ if(input.memory && input.memory.length > 0){
42
+ systemPrompt += `\n\n ${input.memoryHeader ?? '[MEMORIA]'}\n ${input.memory.map(m => `${m.key}: ${m.value}`).join('\n')}`
43
+ }
44
+
45
+ if(input.knowledgeBase && input.knowledgeBase.length > 0){
46
+ systemPrompt += `\n\n${input.knowledgeBaseHeader ?? '[BASE DE CONOCIMIENTO]'}\n ${input.knowledgeBase.join('\n')}`
47
+ }
48
+
49
+ return systemPrompt
50
+ }
51
+
52
+ protected resolvePromptModel(input: IPromptParams, model: string, visionModel?: string){
53
+ return input.model ?? (this.hasImageInput(input) ? visionModel ?? model : model)
54
+ }
55
+
56
+ protected getDefaultToolParameters(){
57
+ return {
58
+ type: "object",
59
+ properties: {},
60
+ additionalProperties: false,
61
+ }
62
+ }
63
+
64
+ protected findToolOrThrow(toolName: string | undefined, tools: IPromptTool[] = []){
65
+ const tool = tools.find(t => t.name === toolName)
66
+
67
+ if(!tool){
68
+ throw new Error(`Tool not found: ${toolName}`)
69
+ }
70
+
71
+ return tool
72
+ }
73
+
74
+ protected parseToolArguments(args: string | object | undefined){
75
+ if(!args){
76
+ return {}
77
+ }
78
+
79
+ if(typeof args === "object"){
80
+ return args
81
+ }
82
+
83
+ try{
84
+ return JSON.parse(args)
85
+ }catch(e){
86
+ throw new Error(`Invalid tool arguments: ${args}`)
87
+ }
88
+ }
89
+
90
+ protected serializeToolOutput(output: unknown){
91
+ if(typeof output === "string"){
92
+ return output
93
+ }
94
+
95
+ if(output === undefined){
96
+ return ""
97
+ }
98
+
99
+ return JSON.stringify(output)
100
+ }
101
+
33
102
  protected hasImageInput(input: IPromptParams){
34
103
  if(input.userImages && input.userImages.length > 0){
35
104
  return true
@@ -1,9 +1,22 @@
1
1
  import OpenAI from "openai";
2
+ import {toJSONSchema} from "zod";
3
+ import type {
4
+ IPromptContentPart,
5
+ IPromptMessage,
6
+ IPromptParams,
7
+ IPromptResponse,
8
+ IPromptTool
9
+ } from "../../interfaces/IAIProvider.js";
2
10
  import type {AILogService} from "../../services/AILogService.js";
3
- import OpenAiProvider from "./OpenAiProvider.js";
11
+ import PromptAudioService from "../../services/PromptAudioService.js";
12
+ import AbstractAiProvider from "./AbstractAiProvider.js";
4
13
 
5
- class DeepSeekAiProvider extends OpenAiProvider{
14
+ class DeepSeekAiProvider extends AbstractAiProvider{
15
+ protected _apiKey: string
16
+ protected _model: any
6
17
  protected _baseUrl: string
18
+ protected _visionModel?: string
19
+ protected _client: any
7
20
 
8
21
  constructor(apiKey: string, model: string, baseUrl: string = "https://api.deepseek.com", visionModel?: string, aiLogService?: AILogService) {
9
22
  if (!apiKey) {
@@ -12,14 +25,23 @@ class DeepSeekAiProvider extends OpenAiProvider{
12
25
  if (!model) {
13
26
  throw new Error("DeepSeek model required")
14
27
  }
15
-
16
- super(apiKey, model, visionModel, aiLogService, "deepseek")
17
-
18
28
  if (!baseUrl) {
19
29
  throw new Error("DeepSeek baseUrl required")
20
30
  }
21
31
 
32
+ super("deepseek", aiLogService)
33
+
34
+ this._apiKey = apiKey
35
+ this._model = model
22
36
  this._baseUrl = baseUrl
37
+ this._visionModel = visionModel
38
+ }
39
+
40
+ get model(){
41
+ if(!this._model){
42
+ throw new Error("DeepSeek model not found")
43
+ }
44
+ return this._model;
23
45
  }
24
46
 
25
47
  get client(){
@@ -33,6 +55,242 @@ class DeepSeekAiProvider extends OpenAiProvider{
33
55
  return this._client
34
56
  }
35
57
 
58
+ protected get visionModel(){
59
+ return this._visionModel
60
+ }
61
+
62
+ protected buildUserContent(input: IPromptParams): string | Array<{type: 'text', text: string} | {type: 'image_url', image_url: {url: string, detail?: 'auto' | 'low' | 'high'}}> {
63
+ if(input.userContent && input.userContent.length > 0){
64
+ return this.mapContentParts(input.userContent)
65
+ }
66
+
67
+ if(input.userImages && input.userImages.length > 0){
68
+ const content: Array<{type: 'text', text: string} | {type: 'image_url', image_url: {url: string, detail?: 'auto' | 'low' | 'high'}}> = []
69
+
70
+ if(input.userInput){
71
+ content.push({type: 'text', text: input.userInput})
72
+ }
73
+
74
+ content.push(...input.userImages.map(image => ({
75
+ type: 'image_url' as const,
76
+ image_url: {
77
+ url: image.url,
78
+ ...(image.detail ? {detail: image.detail} : {}),
79
+ }
80
+ })))
81
+
82
+ return content
83
+ }
84
+
85
+ return input.userInput ?? ""
86
+ }
87
+
88
+ protected mapContentParts(content: IPromptContentPart[]){
89
+ return content.map(part => {
90
+ if(part.type === 'text'){
91
+ return {
92
+ type: 'text' as const,
93
+ text: part.text
94
+ }
95
+ }
96
+
97
+ return {
98
+ type: 'image_url' as const,
99
+ image_url: {
100
+ url: part.imageUrl,
101
+ ...(part.detail ? {detail: part.detail} : {}),
102
+ }
103
+ }
104
+ })
105
+ }
106
+
107
+ protected mapHistory(history: IPromptMessage[] = []){
108
+ return history.map(message => ({
109
+ role: message.role,
110
+ content: typeof message.content === 'string'
111
+ ? message.content
112
+ : this.mapContentParts(message.content)
113
+ }))
114
+ }
115
+
116
+ async generateEmbedding({text, model="text-embedding-ada-002"}: {text:string,model:string }): Promise<number[]> {
117
+ const response = await this.client.embeddings.create({
118
+ model: model,
119
+ input: text,
120
+ });
121
+ return response.data[0].embedding;
122
+ }
123
+
124
+ protected mapTools(tools: IPromptTool[] = []){
125
+ return tools.map(tool => ({
126
+ type: "function" as const,
127
+ function: {
128
+ name: tool.name,
129
+ description: tool.description,
130
+ parameters: tool.parameters ?? this.getDefaultToolParameters(),
131
+ },
132
+ }))
133
+ }
134
+
135
+ protected normalizeJsonSchema(input: IPromptParams){
136
+ if(input.zodSchema){
137
+ return toJSONSchema(input.zodSchema, {
138
+ target: "draft-7",
139
+ })
140
+ }
141
+
142
+ if(!input.jsonSchema){
143
+ return undefined
144
+ }
145
+
146
+ const jsonSchema: any = input.jsonSchema
147
+
148
+ if(jsonSchema.type === "json_schema" && jsonSchema.json_schema?.schema){
149
+ return jsonSchema.json_schema.schema
150
+ }
151
+
152
+ if(jsonSchema.type === "json_object"){
153
+ return undefined
154
+ }
155
+
156
+ return jsonSchema
157
+ }
158
+
159
+ protected normalizeResponseFormat(input: IPromptParams){
160
+ if(input.zodSchema || input.jsonSchema){
161
+ return {type: "json_object" as const}
162
+ }
163
+
164
+ return undefined
165
+ }
166
+
167
+ protected buildDeepSeekSystemPrompt(input: IPromptParams){
168
+ const systemPrompt = this.buildSystemPrompt(input)
169
+ const jsonSchema = this.normalizeJsonSchema(input)
170
+
171
+ if(!jsonSchema){
172
+ return systemPrompt
173
+ }
174
+
175
+ return `${systemPrompt}
176
+
177
+ Respond only with valid JSON matching this JSON Schema. Do not wrap the response in markdown or include any extra text.
178
+
179
+ JSON Schema:
180
+ ${JSON.stringify(jsonSchema, null, 2)}`
181
+ }
182
+
183
+ protected async buildToolMessages(toolCalls: any[] = [], tools: IPromptTool[] = []){
184
+ const toolMessages: any[] = []
185
+
186
+ for(const toolCall of toolCalls){
187
+ const toolName = toolCall.function?.name
188
+ const tool = this.findToolOrThrow(toolName, tools)
189
+
190
+ const args = this.parseToolArguments(toolCall.function?.arguments)
191
+ const output = await tool.execute(args)
192
+
193
+ toolMessages.push({
194
+ role: "tool",
195
+ tool_call_id: toolCall.id,
196
+ content: this.serializeToolOutput(output),
197
+ })
198
+ }
199
+
200
+ return toolMessages
201
+ }
202
+
203
+ async prompt(input: IPromptParams): Promise<IPromptResponse> {
204
+
205
+ const systemPrompt = this.buildDeepSeekSystemPrompt(input)
206
+ const userInput = this.buildUserContent(input)
207
+ const model = this.resolvePromptModel(input, this.model, this.visionModel)
208
+ const startedAt = new Date()
209
+ const startTime = performance.now()
210
+ let tokens = 0
211
+ let inputTokens = 0
212
+ let outputTokens = 0
213
+
214
+ try {
215
+ const messages: any[] = [
216
+ {role: 'system', content: systemPrompt},
217
+ ...this.mapHistory(input.history),
218
+ {role: 'user', content: userInput},
219
+ ]
220
+ const tools = input.tools ?? []
221
+ const maxIterations = input.toolMaxIterations ?? 5
222
+ const responseFormat = this.normalizeResponseFormat(input)
223
+ let output: any
224
+
225
+ for(let iteration = 0; iteration < maxIterations; iteration++){
226
+ const chatCompletion = await this.client.chat.completions.create({
227
+ messages,
228
+
229
+ ...(responseFormat ? {response_format: responseFormat} : {}),
230
+ ...(tools.length > 0 ? {tools: this.mapTools(tools)} : {}),
231
+ model: model,
232
+ });
233
+
234
+ tokens += chatCompletion.usage?.total_tokens ?? 0
235
+ inputTokens += chatCompletion.usage?.prompt_tokens ?? 0
236
+ outputTokens += chatCompletion.usage?.completion_tokens ?? 0
237
+
238
+ const message = chatCompletion.choices[0].message
239
+ const toolCalls = message.tool_calls ?? []
240
+
241
+ if(toolCalls.length === 0){
242
+ output = message.content
243
+ break
244
+ }
245
+
246
+ messages.push(message)
247
+ messages.push(...await this.buildToolMessages(toolCalls, tools))
248
+ }
249
+
250
+ if(output === undefined){
251
+ throw new Error(`Tool max iterations reached: ${maxIterations}`)
252
+ }
253
+
254
+ const endTime = performance.now()
255
+ const time = endTime - startTime
256
+ const endedAt = new Date()
257
+ const audio = await PromptAudioService.build(input, output)
258
+
259
+ await this.registerPromptLog(input, {
260
+ model,
261
+ systemPrompt,
262
+ startedAt,
263
+ endedAt,
264
+ inputTokens,
265
+ outputTokens,
266
+ tokens,
267
+ output,
268
+ success: true,
269
+ })
270
+
271
+ return {
272
+ output,
273
+ tokens,
274
+ inputTokens,
275
+ outputTokens,
276
+ time,
277
+ ...(audio ? {audio} : {}),
278
+ }
279
+ } catch (e: any) {
280
+ const endedAt = new Date()
281
+
282
+ await this.registerPromptLog(input, {
283
+ model,
284
+ systemPrompt,
285
+ startedAt,
286
+ endedAt,
287
+ success: false,
288
+ errorMessage: e?.message,
289
+ })
290
+
291
+ throw e
292
+ }
293
+ }
36
294
  }
37
295
 
38
296
  export default DeepSeekAiProvider
@@ -185,11 +185,7 @@ class GoogleAiProvider extends AbstractAiProvider{
185
185
  functionDeclarations: tools.map(tool => ({
186
186
  name: tool.name,
187
187
  description: tool.description,
188
- parametersJsonSchema: tool.parameters ?? {
189
- type: "object",
190
- properties: {},
191
- additionalProperties: false,
192
- },
188
+ parametersJsonSchema: tool.parameters ?? this.getDefaultToolParameters(),
193
189
  }))
194
190
  }]
195
191
  }
@@ -238,11 +234,7 @@ class GoogleAiProvider extends AbstractAiProvider{
238
234
 
239
235
  for(const functionCall of functionCalls){
240
236
  const toolName = functionCall.name
241
- const tool = tools.find(t => t.name === toolName)
242
-
243
- if(!tool){
244
- throw new Error(`Tool not found: ${toolName}`)
245
- }
237
+ const tool = this.findToolOrThrow(toolName, tools)
246
238
 
247
239
  const output = await tool.execute(functionCall.args ?? {})
248
240
 
@@ -271,23 +263,9 @@ class GoogleAiProvider extends AbstractAiProvider{
271
263
 
272
264
  async prompt(input: IPromptParams): Promise<IPromptResponse> {
273
265
 
274
- if(!input.systemPrompt){
275
- throw new Error("systemPrompt required")
276
- }
277
-
278
- let systemPrompt = input.systemPrompt
279
-
280
- if(input.memory && input.memory.length > 0){
281
- systemPrompt += `\n\n ${input.memoryHeader ?? '[MEMORIA]'}\n ${input.memory.map(m => `${m.key}: ${m.value}`).join('\n')}`
282
- }
283
-
284
- if(input.knowledgeBase && input.knowledgeBase.length > 0){
285
- systemPrompt += `\n\n${input.knowledgeBaseHeader ?? '[BASE DE CONOCIMIENTO]'}\n ${input.knowledgeBase.join('\n')}`
286
- }
287
-
288
-
266
+ const systemPrompt = this.buildSystemPrompt(input)
289
267
  const userInput = this.buildUserContent(input)
290
- const model = input.model ?? (this.hasImageInput(input) ? this.visionModel ?? this.model : this.model)
268
+ const model = this.resolvePromptModel(input, this.model, this.visionModel)
291
269
  const startedAt = new Date()
292
270
  const startTime = performance.now()
293
271
  let tokens = 0
@@ -168,11 +168,7 @@ class OllamaAiProvider extends AbstractAiProvider{
168
168
  function: {
169
169
  name: tool.name,
170
170
  description: tool.description,
171
- parameters: tool.parameters ?? {
172
- type: "object",
173
- properties: {},
174
- additionalProperties: false,
175
- },
171
+ parameters: tool.parameters ?? this.getDefaultToolParameters(),
176
172
  },
177
173
  }))
178
174
  }
@@ -197,44 +193,12 @@ class OllamaAiProvider extends AbstractAiProvider{
197
193
  return jsonSchema
198
194
  }
199
195
 
200
- protected parseToolArguments(args: string | object | undefined){
201
- if(!args){
202
- return {}
203
- }
204
-
205
- if(typeof args === "object"){
206
- return args
207
- }
208
-
209
- try{
210
- return JSON.parse(args)
211
- }catch(e){
212
- throw new Error(`Invalid tool arguments: ${args}`)
213
- }
214
- }
215
-
216
- protected serializeToolOutput(output: unknown){
217
- if(typeof output === "string"){
218
- return output
219
- }
220
-
221
- if(output === undefined){
222
- return ""
223
- }
224
-
225
- return JSON.stringify(output)
226
- }
227
-
228
196
  protected async buildToolMessages(toolCalls: OllamaToolCall[] = [], tools: IPromptTool[] = []){
229
197
  const toolMessages: OllamaMessage[] = []
230
198
 
231
199
  for(const toolCall of toolCalls){
232
200
  const toolName = toolCall.function?.name
233
- const tool = tools.find(t => t.name === toolName)
234
-
235
- if(!tool){
236
- throw new Error(`Tool not found: ${toolName}`)
237
- }
201
+ const tool = this.findToolOrThrow(toolName, tools)
238
202
 
239
203
  const args = this.parseToolArguments(toolCall.function?.arguments)
240
204
  const output = await tool.execute(args)
@@ -251,21 +215,8 @@ class OllamaAiProvider extends AbstractAiProvider{
251
215
 
252
216
  async prompt(input: IPromptParams): Promise<IPromptResponse> {
253
217
 
254
- if(!input.systemPrompt){
255
- throw new Error("systemPrompt required")
256
- }
257
-
258
- let systemPrompt = input.systemPrompt
259
-
260
- if(input.memory && input.memory.length > 0){
261
- systemPrompt += `\n\n ${input.memoryHeader ?? '[MEMORIA]'}\n ${input.memory.map(m => `${m.key}: ${m.value}`).join('\n')}`
262
- }
263
-
264
- if(input.knowledgeBase && input.knowledgeBase.length > 0){
265
- systemPrompt += `\n\n${input.knowledgeBaseHeader ?? '[BASE DE CONOCIMIENTO]'}\n ${input.knowledgeBase.join('\n')}`
266
- }
267
-
268
- const model = input.model ?? (this.hasImageInput(input) ? this.visionModel ?? this.model : this.model)
218
+ const systemPrompt = this.buildSystemPrompt(input)
219
+ const model = this.resolvePromptModel(input, this.model, this.visionModel)
269
220
  const startedAt = new Date()
270
221
  const startTime = performance.now()
271
222
  let tokens = 0
@@ -122,49 +122,17 @@ class OpenAiProvider extends AbstractAiProvider{
122
122
  function: {
123
123
  name: tool.name,
124
124
  description: tool.description,
125
- parameters: tool.parameters ?? {
126
- type: "object",
127
- properties: {},
128
- additionalProperties: false,
129
- },
125
+ parameters: tool.parameters ?? this.getDefaultToolParameters(),
130
126
  },
131
127
  }))
132
128
  }
133
129
 
134
- protected parseToolArguments(args: string | undefined){
135
- if(!args){
136
- return {}
137
- }
138
-
139
- try{
140
- return JSON.parse(args)
141
- }catch(e){
142
- throw new Error(`Invalid tool arguments: ${args}`)
143
- }
144
- }
145
-
146
- protected serializeToolOutput(output: unknown){
147
- if(typeof output === "string"){
148
- return output
149
- }
150
-
151
- if(output === undefined){
152
- return ""
153
- }
154
-
155
- return JSON.stringify(output)
156
- }
157
-
158
130
  protected async buildToolMessages(toolCalls: any[] = [], tools: IPromptTool[] = []){
159
131
  const toolMessages: any[] = []
160
132
 
161
133
  for(const toolCall of toolCalls){
162
134
  const toolName = toolCall.function?.name
163
- const tool = tools.find(t => t.name === toolName)
164
-
165
- if(!tool){
166
- throw new Error(`Tool not found: ${toolName}`)
167
- }
135
+ const tool = this.findToolOrThrow(toolName, tools)
168
136
 
169
137
  const args = this.parseToolArguments(toolCall.function?.arguments)
170
138
  const output = await tool.execute(args)
@@ -181,23 +149,9 @@ class OpenAiProvider extends AbstractAiProvider{
181
149
 
182
150
  async prompt(input: IPromptParams): Promise<IPromptResponse> {
183
151
 
184
- if(!input.systemPrompt){
185
- throw new Error("systemPrompt required")
186
- }
187
-
188
- let systemPrompt = input.systemPrompt
189
-
190
- if(input.memory && input.memory.length > 0){
191
- systemPrompt += `\n\n ${input.memoryHeader ?? '[MEMORIA]'}\n ${input.memory.map(m => `${m.key}: ${m.value}`).join('\n')}`
192
- }
193
-
194
- if(input.knowledgeBase && input.knowledgeBase.length > 0){
195
- systemPrompt += `\n\n${input.knowledgeBaseHeader ?? '[BASE DE CONOCIMIENTO]'}\n ${input.knowledgeBase.join('\n')}`
196
- }
197
-
198
-
152
+ const systemPrompt = this.buildSystemPrompt(input)
199
153
  const userInput = this.buildUserContent(input)
200
- const model = input.model ?? (this.hasImageInput(input) ? this.visionModel ?? this.model : this.model)
154
+ const model = this.resolvePromptModel(input, this.model, this.visionModel)
201
155
  const startedAt = new Date()
202
156
  const startTime = performance.now()
203
157
  let tokens = 0
@@ -1,4 +1,5 @@
1
1
  import {describe, expect, test} from "vitest";
2
+ import {z} from "zod";
2
3
  import {AiProviderFactory, DeepSeekAiProvider} from "../src";
3
4
  import {IPromptTool} from "../src/interfaces/IAIProvider";
4
5
 
@@ -45,6 +46,99 @@ describe("DeepSeekProvider Test", () => {
45
46
  })
46
47
  })
47
48
 
49
+ test("DeepSeek prompt falls back from OpenAI json_schema to json_object with schema instructions", async () => {
50
+ let request: any
51
+
52
+ class MockedDeepSeekProvider extends DeepSeekAiProvider {
53
+ constructor() {
54
+ super("test-key", "deepseek-chat")
55
+ this._client = {
56
+ chat: {
57
+ completions: {
58
+ create: async (payload: any) => {
59
+ request = payload
60
+ return {
61
+ choices: [{message: {content: "{\"name\":\"Pikachu\"}"}}],
62
+ usage: {
63
+ total_tokens: 8,
64
+ prompt_tokens: 6,
65
+ completion_tokens: 2
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ const jsonSchema = {
76
+ type: "json_schema",
77
+ json_schema: {
78
+ name: "element_description",
79
+ schema: {
80
+ type: "object",
81
+ properties: {
82
+ name: {type: "string"}
83
+ },
84
+ required: ["name"]
85
+ }
86
+ }
87
+ }
88
+
89
+ const deepSeek = new MockedDeepSeekProvider()
90
+ await deepSeek.prompt({
91
+ systemPrompt: "You are an AI assistant.",
92
+ userInput: "What is the most famous pokemon?",
93
+ jsonSchema,
94
+ })
95
+
96
+ expect(request.response_format).toEqual({type: "json_object"})
97
+ expect(request.messages[0].content).toContain("Respond only with valid JSON")
98
+ expect(request.messages[0].content).toContain("\"required\": [")
99
+ expect(request.messages[0].content).toContain("\"name\"")
100
+ })
101
+
102
+ test("DeepSeek prompt falls back from zod schema to json_object with generated schema instructions", async () => {
103
+ let request: any
104
+
105
+ class MockedDeepSeekProvider extends DeepSeekAiProvider {
106
+ constructor() {
107
+ super("test-key", "deepseek-chat")
108
+ this._client = {
109
+ chat: {
110
+ completions: {
111
+ create: async (payload: any) => {
112
+ request = payload
113
+ return {
114
+ choices: [{message: {content: "{\"name\":\"Pikachu\"}"}}],
115
+ usage: {
116
+ total_tokens: 8,
117
+ prompt_tokens: 6,
118
+ completion_tokens: 2
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ const deepSeek = new MockedDeepSeekProvider()
129
+ await deepSeek.prompt({
130
+ systemPrompt: "You are an AI assistant.",
131
+ userInput: "What is the most famous pokemon?",
132
+ zodSchema: z.object({
133
+ name: z.string(),
134
+ }),
135
+ })
136
+
137
+ expect(request.response_format).toEqual({type: "json_object"})
138
+ expect(request.messages[0].content).toContain("JSON Schema")
139
+ expect(request.messages[0].content).toContain("\"name\"")
140
+ })
141
+
48
142
  test("DeepSeek prompt executes tools through OpenAI-compatible tool calls", async () => {
49
143
  const requests: any[] = []
50
144
  const weatherTool: IPromptTool = {