@hed-hog/core 0.0.400 → 0.0.402

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 (274) hide show
  1. package/dist/access-log/access-log.interceptor.d.ts +10 -0
  2. package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
  3. package/dist/access-log/access-log.interceptor.js +15 -1
  4. package/dist/access-log/access-log.interceptor.js.map +1 -1
  5. package/dist/ai/ai.controller.d.ts +2 -0
  6. package/dist/ai/ai.controller.d.ts.map +1 -1
  7. package/dist/ai/ai.service.d.ts +86 -2
  8. package/dist/ai/ai.service.d.ts.map +1 -1
  9. package/dist/ai/ai.service.js +304 -23
  10. package/dist/ai/ai.service.js.map +1 -1
  11. package/dist/ai-instruction/ai-prompt-resolver.service.d.ts +12 -0
  12. package/dist/ai-instruction/ai-prompt-resolver.service.d.ts.map +1 -1
  13. package/dist/ai-instruction/ai-prompt-resolver.service.js +26 -0
  14. package/dist/ai-instruction/ai-prompt-resolver.service.js.map +1 -1
  15. package/dist/core.module.d.ts.map +1 -1
  16. package/dist/core.module.js +3 -0
  17. package/dist/core.module.js.map +1 -1
  18. package/dist/document-kit/document-kit.module.d.ts +3 -0
  19. package/dist/document-kit/document-kit.module.d.ts.map +1 -0
  20. package/dist/document-kit/document-kit.module.js +24 -0
  21. package/dist/document-kit/document-kit.module.js.map +1 -0
  22. package/dist/document-kit/document-kit.registry.d.ts +55 -0
  23. package/dist/document-kit/document-kit.registry.d.ts.map +1 -0
  24. package/dist/document-kit/document-kit.registry.js +91 -0
  25. package/dist/document-kit/document-kit.registry.js.map +1 -0
  26. package/dist/document-kit/document-kit.service.d.ts +49 -0
  27. package/dist/document-kit/document-kit.service.d.ts.map +1 -0
  28. package/dist/document-kit/document-kit.service.js +135 -0
  29. package/dist/document-kit/document-kit.service.js.map +1 -0
  30. package/dist/file/file-upload-ticket.service.d.ts +1 -1
  31. package/dist/file/file.controller.d.ts +3 -3
  32. package/dist/file/file.service.d.ts +136 -13
  33. package/dist/file/file.service.d.ts.map +1 -1
  34. package/dist/file/file.service.js +297 -27
  35. package/dist/file/file.service.js.map +1 -1
  36. package/dist/file/mcp-file-source.schema.d.ts.map +1 -1
  37. package/dist/file/mcp-file-source.schema.js +8 -5
  38. package/dist/file/mcp-file-source.schema.js.map +1 -1
  39. package/dist/file/provider/abstract.provider.d.ts +33 -0
  40. package/dist/file/provider/abstract.provider.d.ts.map +1 -1
  41. package/dist/file/provider/abstract.provider.js +9 -0
  42. package/dist/file/provider/abstract.provider.js.map +1 -1
  43. package/dist/file/provider/s3-multipart.util.d.ts +17 -0
  44. package/dist/file/provider/s3-multipart.util.d.ts.map +1 -0
  45. package/dist/file/provider/s3-multipart.util.js +88 -0
  46. package/dist/file/provider/s3-multipart.util.js.map +1 -0
  47. package/dist/file/provider/s3.provider.d.ts +9 -1
  48. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  49. package/dist/file/provider/s3.provider.js +16 -0
  50. package/dist/file/provider/s3.provider.js.map +1 -1
  51. package/dist/file/provider/spaces.provider.d.ts +10 -1
  52. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  53. package/dist/file/provider/spaces.provider.js +17 -0
  54. package/dist/file/provider/spaces.provider.js.map +1 -1
  55. package/dist/index.d.ts +5 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +5 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/integration-profile/integration-profile.controller.d.ts +1 -0
  60. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
  61. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  62. package/dist/integration-profile/integration-profile.secrets.js +26 -0
  63. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  64. package/dist/integration-profile/integration-profile.service.d.ts +1 -0
  65. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  66. package/dist/integration-profile/integration-profile.service.js +3 -3
  67. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  68. package/dist/integration-profile/testers/ai.testers.d.ts.map +1 -1
  69. package/dist/integration-profile/testers/ai.testers.js +46 -8
  70. package/dist/integration-profile/testers/ai.testers.js.map +1 -1
  71. package/dist/integration-profile/testers/http-probe.d.ts.map +1 -1
  72. package/dist/integration-profile/testers/http-probe.js +5 -3
  73. package/dist/integration-profile/testers/http-probe.js.map +1 -1
  74. package/dist/integration-profile/testers/integration-tester.types.d.ts +5 -0
  75. package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -1
  76. package/dist/mail/mail.service.d.ts.map +1 -1
  77. package/dist/mail/mail.service.js +16 -1
  78. package/dist/mail/mail.service.js.map +1 -1
  79. package/dist/mcp/mcp-server.service.d.ts.map +1 -1
  80. package/dist/mcp/mcp-server.service.js +10 -1
  81. package/dist/mcp/mcp-server.service.js.map +1 -1
  82. package/dist/mcp/mcp.module.d.ts.map +1 -1
  83. package/dist/mcp/mcp.module.js +2 -0
  84. package/dist/mcp/mcp.module.js.map +1 -1
  85. package/dist/mcp/tools/core-access-log.mcp-tools.d.ts +17 -0
  86. package/dist/mcp/tools/core-access-log.mcp-tools.d.ts.map +1 -0
  87. package/dist/mcp/tools/core-access-log.mcp-tools.js +74 -0
  88. package/dist/mcp/tools/core-access-log.mcp-tools.js.map +1 -0
  89. package/dist/mcp/tools/core-files.mcp-tools.js +4 -4
  90. package/dist/mcp/tools/core-files.mcp-tools.js.map +1 -1
  91. package/dist/meeting/meeting-provider.registry.d.ts +72 -0
  92. package/dist/meeting/meeting-provider.registry.d.ts.map +1 -1
  93. package/dist/meeting/meeting-provider.registry.js.map +1 -1
  94. package/dist/oauth/oauth-token-broker.service.d.ts +50 -1
  95. package/dist/oauth/oauth-token-broker.service.d.ts.map +1 -1
  96. package/dist/oauth/oauth-token-broker.service.js +105 -3
  97. package/dist/oauth/oauth-token-broker.service.js.map +1 -1
  98. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +6 -0
  99. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
  100. package/dist/oauth/providers/microsoft-entra-id.provider.js +30 -4
  101. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  102. package/dist/pdf/document/charts/chart-bar.d.ts +24 -0
  103. package/dist/pdf/document/charts/chart-bar.d.ts.map +1 -0
  104. package/dist/pdf/document/charts/chart-bar.js +117 -0
  105. package/dist/pdf/document/charts/chart-bar.js.map +1 -0
  106. package/dist/pdf/document/charts/chart-donut.d.ts +16 -0
  107. package/dist/pdf/document/charts/chart-donut.d.ts.map +1 -0
  108. package/dist/pdf/document/charts/chart-donut.js +89 -0
  109. package/dist/pdf/document/charts/chart-donut.js.map +1 -0
  110. package/dist/pdf/document/charts/chart-gauge.d.ts +15 -0
  111. package/dist/pdf/document/charts/chart-gauge.d.ts.map +1 -0
  112. package/dist/pdf/document/charts/chart-gauge.js +60 -0
  113. package/dist/pdf/document/charts/chart-gauge.js.map +1 -0
  114. package/dist/pdf/document/charts/chart-line.d.ts +29 -0
  115. package/dist/pdf/document/charts/chart-line.d.ts.map +1 -0
  116. package/dist/pdf/document/charts/chart-line.js +89 -0
  117. package/dist/pdf/document/charts/chart-line.js.map +1 -0
  118. package/dist/pdf/document/charts/chart-primitives.d.ts +64 -0
  119. package/dist/pdf/document/charts/chart-primitives.d.ts.map +1 -0
  120. package/dist/pdf/document/charts/chart-primitives.js +110 -0
  121. package/dist/pdf/document/charts/chart-primitives.js.map +1 -0
  122. package/dist/pdf/document/charts/chart-radar.d.ts +13 -0
  123. package/dist/pdf/document/charts/chart-radar.d.ts.map +1 -0
  124. package/dist/pdf/document/charts/chart-radar.js +83 -0
  125. package/dist/pdf/document/charts/chart-radar.js.map +1 -0
  126. package/dist/pdf/document/charts/chart-scale.d.ts +39 -0
  127. package/dist/pdf/document/charts/chart-scale.d.ts.map +1 -0
  128. package/dist/pdf/document/charts/chart-scale.js +118 -0
  129. package/dist/pdf/document/charts/chart-scale.js.map +1 -0
  130. package/dist/pdf/document/charts/index.d.ts +8 -0
  131. package/dist/pdf/document/charts/index.d.ts.map +1 -0
  132. package/dist/pdf/document/charts/index.js +24 -0
  133. package/dist/pdf/document/charts/index.js.map +1 -0
  134. package/dist/pdf/document/index.d.ts +8 -0
  135. package/dist/pdf/document/index.d.ts.map +1 -0
  136. package/dist/pdf/document/index.js +24 -0
  137. package/dist/pdf/document/index.js.map +1 -0
  138. package/dist/pdf/document/pdf-document.chrome.d.ts +20 -0
  139. package/dist/pdf/document/pdf-document.chrome.d.ts.map +1 -0
  140. package/dist/pdf/document/pdf-document.chrome.js +36 -0
  141. package/dist/pdf/document/pdf-document.chrome.js.map +1 -0
  142. package/dist/pdf/document/pdf-document.css.d.ts +20 -0
  143. package/dist/pdf/document/pdf-document.css.d.ts.map +1 -0
  144. package/dist/pdf/document/pdf-document.css.js +327 -0
  145. package/dist/pdf/document/pdf-document.css.js.map +1 -0
  146. package/dist/pdf/document/pdf-document.renderer.d.ts +10 -0
  147. package/dist/pdf/document/pdf-document.renderer.d.ts.map +1 -0
  148. package/dist/pdf/document/pdf-document.renderer.js +226 -0
  149. package/dist/pdf/document/pdf-document.renderer.js.map +1 -0
  150. package/dist/pdf/document/pdf-document.service.d.ts +27 -0
  151. package/dist/pdf/document/pdf-document.service.d.ts.map +1 -0
  152. package/dist/pdf/document/pdf-document.service.js +81 -0
  153. package/dist/pdf/document/pdf-document.service.js.map +1 -0
  154. package/dist/pdf/document/pdf-document.theme.d.ts +41 -0
  155. package/dist/pdf/document/pdf-document.theme.d.ts.map +1 -0
  156. package/dist/pdf/document/pdf-document.theme.js +105 -0
  157. package/dist/pdf/document/pdf-document.theme.js.map +1 -0
  158. package/dist/pdf/document/pdf-document.types.d.ts +164 -0
  159. package/dist/pdf/document/pdf-document.types.d.ts.map +1 -0
  160. package/dist/pdf/document/pdf-document.types.js +18 -0
  161. package/dist/pdf/document/pdf-document.types.js.map +1 -0
  162. package/dist/pdf/pdf.module.d.ts.map +1 -1
  163. package/dist/pdf/pdf.module.js +3 -2
  164. package/dist/pdf/pdf.module.js.map +1 -1
  165. package/dist/pdf/pdf.service.d.ts +75 -1
  166. package/dist/pdf/pdf.service.d.ts.map +1 -1
  167. package/dist/pdf/pdf.service.js +167 -63
  168. package/dist/pdf/pdf.service.js.map +1 -1
  169. package/dist/pdf/templates/commercial-proposal.template.d.ts +96 -0
  170. package/dist/pdf/templates/commercial-proposal.template.d.ts.map +1 -0
  171. package/dist/pdf/templates/commercial-proposal.template.js +276 -0
  172. package/dist/pdf/templates/commercial-proposal.template.js.map +1 -0
  173. package/dist/setting/favicon.controller.d.ts +26 -0
  174. package/dist/setting/favicon.controller.d.ts.map +1 -0
  175. package/dist/setting/favicon.controller.js +130 -0
  176. package/dist/setting/favicon.controller.js.map +1 -0
  177. package/dist/setting/setting.module.d.ts.map +1 -1
  178. package/dist/setting/setting.module.js +2 -1
  179. package/dist/setting/setting.module.js.map +1 -1
  180. package/dist/setting/setting.service.d.ts.map +1 -1
  181. package/dist/setting/setting.service.js +3 -0
  182. package/dist/setting/setting.service.js.map +1 -1
  183. package/dist/task/task.service.d.ts +6 -0
  184. package/dist/task/task.service.d.ts.map +1 -1
  185. package/dist/task/task.service.js +44 -13
  186. package/dist/task/task.service.js.map +1 -1
  187. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +35 -2
  188. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
  189. package/dist/whatsapp-sender/whatsapp-sender.service.js +107 -22
  190. package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
  191. package/hedhog/data/integration_type.yaml +6 -0
  192. package/hedhog/data/route.yaml +8 -0
  193. package/hedhog/data/setting_group.yaml +52 -0
  194. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +85 -2
  195. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +576 -491
  196. package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +29 -50
  197. package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +146 -50
  198. package/hedhog/frontend/messages/en.json +81 -2
  199. package/hedhog/frontend/messages/pt.json +81 -2
  200. package/hedhog/table/file.yaml +3 -1
  201. package/package.json +5 -5
  202. package/src/access-log/access-log.interceptor.spec.ts +50 -0
  203. package/src/access-log/access-log.interceptor.ts +18 -0
  204. package/src/ai/ai-attachment.fragments.spec.ts +56 -3
  205. package/src/ai/ai.service.responses.spec.ts +429 -0
  206. package/src/ai/ai.service.ts +409 -27
  207. package/src/ai-instruction/ai-prompt-resolver.service.spec.ts +44 -0
  208. package/src/ai-instruction/ai-prompt-resolver.service.ts +29 -0
  209. package/src/core.module.ts +3 -0
  210. package/src/document-kit/document-kit.module.ts +12 -0
  211. package/src/document-kit/document-kit.registry.ts +125 -0
  212. package/src/document-kit/document-kit.service.spec.ts +156 -0
  213. package/src/document-kit/document-kit.service.ts +173 -0
  214. package/src/file/file-direct-upload.spec.ts +344 -0
  215. package/src/file/file.service.spec.ts +168 -1
  216. package/src/file/file.service.ts +435 -36
  217. package/src/file/mcp-file-source.schema.ts +8 -5
  218. package/src/file/provider/abstract.provider.ts +57 -0
  219. package/src/file/provider/s3-multipart.spec.ts +123 -0
  220. package/src/file/provider/s3-multipart.util.ts +126 -0
  221. package/src/file/provider/s3.provider.ts +65 -1
  222. package/src/file/provider/spaces.provider.ts +67 -1
  223. package/src/index.ts +5 -0
  224. package/src/integration-profile/integration-profile.secrets.ts +26 -0
  225. package/src/integration-profile/integration-profile.service.ts +1 -0
  226. package/src/integration-profile/testers/ai.testers.ts +70 -5
  227. package/src/integration-profile/testers/http-probe.ts +5 -3
  228. package/src/integration-profile/testers/integration-tester.types.ts +5 -0
  229. package/src/integration-profile/testers/oauth.testers.spec.ts +13 -0
  230. package/src/integration-profile/testers/remaining.testers.spec.ts +65 -1
  231. package/src/integration-profile/testers/tester-coverage.spec.ts +12 -0
  232. package/src/mail/mail.service.spec.ts +25 -0
  233. package/src/mail/mail.service.ts +18 -1
  234. package/src/mcp/mcp-server.service.spec.ts +16 -0
  235. package/src/mcp/mcp-server.service.ts +10 -1
  236. package/src/mcp/mcp.module.ts +2 -0
  237. package/src/mcp/tools/core-access-log.mcp-tools.ts +66 -0
  238. package/src/mcp/tools/core-files.mcp-tools.ts +4 -4
  239. package/src/meeting/meeting-provider.registry.ts +80 -0
  240. package/src/oauth/oauth-token-broker.service.spec.ts +108 -0
  241. package/src/oauth/oauth-token-broker.service.ts +129 -2
  242. package/src/oauth/providers/microsoft-entra-id.provider.spec.ts +133 -0
  243. package/src/oauth/providers/microsoft-entra-id.provider.ts +29 -2
  244. package/src/pdf/document/charts/chart-bar.ts +217 -0
  245. package/src/pdf/document/charts/chart-donut.ts +163 -0
  246. package/src/pdf/document/charts/chart-gauge.ts +94 -0
  247. package/src/pdf/document/charts/chart-line.ts +159 -0
  248. package/src/pdf/document/charts/chart-primitives.ts +188 -0
  249. package/src/pdf/document/charts/chart-radar.ts +136 -0
  250. package/src/pdf/document/charts/chart-scale.spec.ts +93 -0
  251. package/src/pdf/document/charts/chart-scale.ts +141 -0
  252. package/src/pdf/document/charts/charts.spec.ts +157 -0
  253. package/src/pdf/document/charts/index.ts +7 -0
  254. package/src/pdf/document/index.ts +7 -0
  255. package/src/pdf/document/pdf-document.chrome.ts +38 -0
  256. package/src/pdf/document/pdf-document.css.ts +326 -0
  257. package/src/pdf/document/pdf-document.renderer.spec.ts +172 -0
  258. package/src/pdf/document/pdf-document.renderer.ts +317 -0
  259. package/src/pdf/document/pdf-document.service.ts +77 -0
  260. package/src/pdf/document/pdf-document.theme.ts +146 -0
  261. package/src/pdf/document/pdf-document.types.ts +149 -0
  262. package/src/pdf/pdf.module.ts +3 -2
  263. package/src/pdf/pdf.service.spec.ts +131 -0
  264. package/src/pdf/pdf.service.ts +256 -82
  265. package/src/pdf/templates/commercial-proposal.template.spec.ts +177 -0
  266. package/src/pdf/templates/commercial-proposal.template.ts +400 -0
  267. package/src/setting/favicon.controller.spec.ts +92 -0
  268. package/src/setting/favicon.controller.ts +135 -0
  269. package/src/setting/setting.module.ts +2 -1
  270. package/src/setting/setting.service.ts +3 -0
  271. package/src/task/task.service.spec.ts +77 -1
  272. package/src/task/task.service.ts +58 -14
  273. package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +263 -0
  274. package/src/whatsapp-sender/whatsapp-sender.service.ts +154 -16
@@ -17,6 +17,7 @@ import {
17
17
  AiPromptResolverService,
18
18
  applyGeminiModelParams,
19
19
  applyOpenAiModelParams,
20
+ applyOpenAiResponsesParams,
20
21
  mergeModelParams,
21
22
  type PromptSource,
22
23
  } from '../ai-instruction/ai-prompt-resolver.service';
@@ -97,6 +98,56 @@ type AiAttachment = {
97
98
  buffer: Buffer;
98
99
  };
99
100
 
101
+ /**
102
+ * Como o anexo efetivamente chegou ao modelo na última chamada.
103
+ *
104
+ * Sem isso quem chama só consegue adivinhar — foi exatamente o que o extrator
105
+ * de notas fiscais fazia, deduzindo pelo tamanho do texto do PDF, e por isso
106
+ * insistia em reperguntar ao modelo sobre um anexo que ele nunca recebeu.
107
+ * `unreadable-pdf` e `binary` são os dois casos em que o modelo recebeu um
108
+ * marcador no lugar do documento.
109
+ */
110
+ export type AiAttachmentDelivery =
111
+ | 'none'
112
+ | 'image'
113
+ | 'pdf-document'
114
+ | 'pdf-text'
115
+ | 'text'
116
+ | 'unreadable-pdf'
117
+ | 'binary';
118
+
119
+ /** Anexo que o modelo realmente leu, em vez de um marcador dizendo que não deu. */
120
+ const DELIVERED_TO_MODEL: ReadonlySet<AiAttachmentDelivery> = new Set([
121
+ 'image',
122
+ 'pdf-document',
123
+ 'pdf-text',
124
+ 'text',
125
+ ]);
126
+
127
+ /** Entregas em que o modelo enxergou os pixels da página, não só texto. */
128
+ const DELIVERED_AS_PIXELS: ReadonlySet<AiAttachmentDelivery> = new Set([
129
+ 'image',
130
+ 'pdf-document',
131
+ ]);
132
+
133
+ export function isAttachmentDeliveredToModel(
134
+ delivery?: AiAttachmentDelivery | null,
135
+ ): boolean {
136
+ return !!delivery && DELIVERED_TO_MODEL.has(delivery);
137
+ }
138
+
139
+ export function isAttachmentDeliveredAsPixels(
140
+ delivery?: AiAttachmentDelivery | null,
141
+ ): boolean {
142
+ return !!delivery && DELIVERED_AS_PIXELS.has(delivery);
143
+ }
144
+
145
+ type AiChatResult = {
146
+ content: string;
147
+ usage?: { input: number; output: number; total: number; cached?: number };
148
+ delivery?: AiAttachmentDelivery;
149
+ };
150
+
100
151
  type AiProfileCredentials = {
101
152
  apiKey: string;
102
153
  organization?: string;
@@ -105,6 +156,29 @@ type AiProfileCredentials = {
105
156
  baseUrl?: string;
106
157
  };
107
158
 
159
+ /**
160
+ * O setting de um provider aponta para o perfil de outro provider.
161
+ *
162
+ * Classe própria, e não um `BadRequestException` solto, para uma escada de
163
+ * fallback conseguir parar antes de disparar chamadas que vão dar 4xx com
164
+ * certeza — era o que o extrator de notas fiscais fazia oito vezes por upload,
165
+ * engolindo cada uma delas.
166
+ */
167
+ export class AiProfileProviderMismatchError extends BadRequestException {
168
+ constructor(
169
+ readonly requestedProvider: string,
170
+ readonly profileSlug: string,
171
+ readonly profileProviderSlug: string,
172
+ readonly settingSlug: string,
173
+ ) {
174
+ super(
175
+ `AI profile "${profileSlug}" (setting "${settingSlug}") belongs to provider ` +
176
+ `"${profileProviderSlug}", not "${requestedProvider}". ` +
177
+ `Point Settings → AI → "${settingSlug}" at a ${requestedProvider} profile, or leave it empty.`,
178
+ );
179
+ }
180
+ }
181
+
108
182
  @Injectable()
109
183
  export class AiService implements OnModuleInit {
110
184
  private readonly logger = new Logger(AiService.name);
@@ -148,7 +222,7 @@ export class AiService implements OnModuleInit {
148
222
  const stepStart = Date.now();
149
223
 
150
224
  try {
151
- const { content, usage } = await this.withTimeout(
225
+ const { content, usage, delivery } = await this.withTimeout(
152
226
  this.chatWithProvider(provider, {
153
227
  message: data.message,
154
228
  model,
@@ -157,6 +231,7 @@ export class AiService implements OnModuleInit {
157
231
  apiKey: aiProfile.apiKey,
158
232
  baseUrl: aiProfile.baseUrl,
159
233
  params,
234
+ timeoutMs,
160
235
  }),
161
236
  timeoutMs,
162
237
  );
@@ -173,7 +248,12 @@ export class AiService implements OnModuleInit {
173
248
  });
174
249
  await this.aiExecution.finish(executionId, 'completed');
175
250
 
176
- return { provider, model, content };
251
+ return {
252
+ provider,
253
+ model,
254
+ content,
255
+ attachmentDelivery: delivery ?? 'none',
256
+ };
177
257
  } catch (err) {
178
258
  await this.aiExecution.finish(executionId, 'failed', String(err));
179
259
  throw err;
@@ -501,21 +581,30 @@ export class AiService implements OnModuleInit {
501
581
  // Assistants API — que a OpenAI desligou, e que já ignorava model e
502
582
  // instructions do banco porque só mandava o `assistant_id`.
503
583
  const aiProfile = await this.getAiProfile(agent.provider);
584
+ const timeoutMs = aiProfile.timeoutMs ?? DEFAULT_AI_CALL_TIMEOUT_MS;
504
585
 
505
- const { content } = await this.chatWithProvider(agent.provider, {
506
- message: data.message,
507
- model: agent.model || this.getDefaultModel(agent.provider),
508
- systemPrompt: agent.instructions || undefined,
509
- attachment,
510
- apiKey: aiProfile.apiKey,
511
- baseUrl: aiProfile.baseUrl,
512
- });
586
+ // Sem o `withTimeout` este era o único ponto público sem prazo nenhum: uma
587
+ // chamada travada no provider segurava a requisição HTTP de quem chamou
588
+ // indefinidamente.
589
+ const { content, delivery } = await this.withTimeout(
590
+ this.chatWithProvider(agent.provider, {
591
+ message: data.message,
592
+ model: agent.model || this.getDefaultModel(agent.provider),
593
+ systemPrompt: agent.instructions || undefined,
594
+ attachment,
595
+ apiKey: aiProfile.apiKey,
596
+ baseUrl: aiProfile.baseUrl,
597
+ timeoutMs,
598
+ }),
599
+ timeoutMs,
600
+ );
513
601
 
514
602
  return {
515
603
  slug: agent.slug,
516
604
  provider: agent.provider,
517
605
  model: agent.model,
518
606
  content,
607
+ attachmentDelivery: delivery ?? 'none',
519
608
  };
520
609
  }
521
610
 
@@ -587,7 +676,7 @@ export class AiService implements OnModuleInit {
587
676
  sha256: createHash('sha256').update(attachment.buffer).digest('hex'),
588
677
  };
589
678
 
590
- this.logger.warn(
679
+ this.logger.debug(
591
680
  `[AI-ATTACHMENT-DEBUG] fileId=${fileId || 'upload'} filename="${result.filename}" mime=${result.mimeType} mode=${result.mode} bytes=${result.bytes} textLength=${result.textLength}`,
592
681
  );
593
682
 
@@ -656,8 +745,9 @@ export class AiService implements OnModuleInit {
656
745
  apiKey?: string;
657
746
  baseUrl?: string;
658
747
  params?: AiChatModelParams;
748
+ timeoutMs?: number;
659
749
  },
660
- ) {
750
+ ): Promise<AiChatResult> {
661
751
  if (provider === 'gemini') {
662
752
  return this.chatWithGemini(input);
663
753
  }
@@ -728,7 +818,49 @@ export class AiService implements OnModuleInit {
728
818
  );
729
819
  }
730
820
 
731
- return (await this.loadAiProfileBySlug(profileSlug)).config;
821
+ const loaded = await this.loadAiProfileBySlug(profileSlug);
822
+ const mapped = PROVIDER_BY_INTEGRATION_SLUG[loaded.providerSlug];
823
+
824
+ // `resolveProviderAndProfile` já protegia o caminho do perfil explícito
825
+ // ("pedir openai e apontar um perfil Claude bateria na API errada com a
826
+ // chave errada"); o caminho do setting global ficara de fora. Sem isso, um
827
+ // perfil OpenAI no campo do Gemini fazia o serviço postar a chave da OpenAI
828
+ // no endpoint do Google e engolir o 4xx de cada modelo tentado.
829
+ if (mapped !== provider) {
830
+ this.logger.error(
831
+ `[AI-PROFILE] "${slug}" = "${profileSlug}" é um perfil ${loaded.providerSlug}, ` +
832
+ `mas ${provider} foi pedido.`,
833
+ );
834
+ throw new AiProfileProviderMismatchError(
835
+ provider,
836
+ profileSlug,
837
+ loaded.providerSlug,
838
+ slug,
839
+ );
840
+ }
841
+
842
+ return loaded.config;
843
+ }
844
+
845
+ /**
846
+ * "Este provider dá para usar?", sem lançar.
847
+ *
848
+ * Existe para uma escada de fallback perguntar antes de tentar, em vez de
849
+ * gastar uma requisição HTTP por modelo só para descobrir que a credencial
850
+ * nem é daquele provider.
851
+ */
852
+ async isProviderConfigured(
853
+ provider: AiProvider,
854
+ ): Promise<{ ok: boolean; reason?: string }> {
855
+ try {
856
+ await this.getAiProfile(provider);
857
+ return { ok: true };
858
+ } catch (error) {
859
+ return {
860
+ ok: false,
861
+ reason: error instanceof Error ? error.message : String(error),
862
+ };
863
+ }
732
864
  }
733
865
 
734
866
  private async loadAiProfileBySlug(profileSlug: string): Promise<{
@@ -783,6 +915,7 @@ export class AiService implements OnModuleInit {
783
915
  provider = 'openai',
784
916
  baseUrl = 'https://api.openai.com/v1',
785
917
  supportsImages = true,
918
+ timeoutMs,
786
919
  }: {
787
920
  message: string;
788
921
  model: string;
@@ -794,16 +927,44 @@ export class AiService implements OnModuleInit {
794
927
  provider?: AiProvider;
795
928
  baseUrl?: string;
796
929
  supportsImages?: boolean;
797
- }): Promise<{ content: string; usage?: { input: number; output: number; total: number } }> {
930
+ timeoutMs?: number;
931
+ }): Promise<AiChatResult> {
798
932
  const openai = apiKey ?? (await this.getAiApiKey(provider));
799
933
 
934
+ if (this.shouldUseOpenAiResponses({ provider, baseUrl, model, attachment })) {
935
+ try {
936
+ return await this.chatWithOpenAiResponses({
937
+ message,
938
+ model,
939
+ systemPrompt,
940
+ attachment: attachment!,
941
+ apiKey: openai,
942
+ baseUrl,
943
+ params,
944
+ timeoutMs,
945
+ });
946
+ } catch (error) {
947
+ const status = Number((error as any)?.response?.status);
948
+ // 400/404/422 = este modelo ou esta conta não aceita `input_file`;
949
+ // cair para o chat/completions é melhor que falhar. Qualquer outra
950
+ // coisa (401, 429, 5xx, timeout) é falha de verdade e não pode ser
951
+ // mascarada por uma segunda chamada mais fraca, que só reproduziria o
952
+ // marcador de PDF ilegível e devolveria "não achei nada".
953
+ if (![400, 404, 422].includes(status)) throw error;
954
+ this.logger.warn(
955
+ `[AI-RESPONSES] /v1/responses recusou (status=${status}); usando chat/completions. ` +
956
+ `apiError=${(error as any)?.response?.data?.error?.message ?? 'n/a'}`,
957
+ );
958
+ }
959
+ }
960
+
800
961
  const messages: Array<any> = [];
801
962
 
802
963
  if (systemPrompt) {
803
964
  messages.push({ role: 'system', content: systemPrompt });
804
965
  }
805
966
 
806
- const userContent = await this.buildOpenAiUserContent(
967
+ const { content: userContent, delivery } = await this.buildOpenAiUserContent(
807
968
  message,
808
969
  attachment,
809
970
  supportsImages,
@@ -830,10 +991,200 @@ export class AiService implements OnModuleInit {
830
991
  const u = response.data?.usage;
831
992
  return {
832
993
  content: response.data?.choices?.[0]?.message?.content || '',
994
+ delivery,
833
995
  usage: u ? { input: u.prompt_tokens ?? 0, output: u.completion_tokens ?? 0, total: u.total_tokens ?? 0 } : undefined,
834
996
  };
835
997
  }
836
998
 
999
+ /**
1000
+ * Bytes crus de PDF acima disto não vão inline: base64 infla ~1,37x e a
1001
+ * OpenAI limita a requisição a 50 MB.
1002
+ */
1003
+ private static readonly OPENAI_RESPONSES_MAX_PDF_BYTES = 20 * 1024 * 1024;
1004
+
1005
+ /** Acima disto a conta de tokens das páginas renderizadas deixa de compensar. */
1006
+ private static readonly OPENAI_RESPONSES_MAX_PDF_PAGES = 20;
1007
+
1008
+ /**
1009
+ * Modelos que renderizam as páginas do PDF como imagem na Responses API.
1010
+ *
1011
+ * Um modelo sem visão recebendo `input_file` só recebe a camada de texto de
1012
+ * volta — o mesmo nada do chat/completions, num endpoint diferente. Deixá-lo
1013
+ * no caminho antigo mantém a falha conhecida em vez de criar uma nova.
1014
+ */
1015
+ private static readonly OPENAI_DOCUMENT_MODEL_RE =
1016
+ /^(gpt-4o|gpt-4\.1|gpt-4-turbo|gpt-5|o3|o4)/i;
1017
+
1018
+ private shouldUseOpenAiResponses(input: {
1019
+ provider: AiProvider;
1020
+ baseUrl: string;
1021
+ model: string;
1022
+ attachment?: AiAttachment | null;
1023
+ }): boolean {
1024
+ // O DeepSeek reusa o formato da OpenAI, mas não tem /v1/responses; e um
1025
+ // gateway apontado por `base_url` pode simplesmente não repassar a rota.
1026
+ if (input.provider !== 'openai') return false;
1027
+ if (!input.baseUrl.startsWith('https://api.openai.com')) return false;
1028
+ if (!input.attachment) return false;
1029
+ // Imagem já funciona pelo bloco de visão do chat/completions.
1030
+ if (!this.isPdfMime(input.attachment.mimeType)) return false;
1031
+ if (
1032
+ input.attachment.buffer.length > AiService.OPENAI_RESPONSES_MAX_PDF_BYTES
1033
+ ) {
1034
+ return false;
1035
+ }
1036
+ return AiService.OPENAI_DOCUMENT_MODEL_RE.test(input.model);
1037
+ }
1038
+
1039
+ private buildOpenAiResponsesBody(input: {
1040
+ message: string;
1041
+ model: string;
1042
+ systemPrompt?: string;
1043
+ attachment: AiAttachment;
1044
+ params?: AiChatModelParams;
1045
+ }): Record<string, unknown> {
1046
+ const base64 = input.attachment.buffer.toString('base64');
1047
+
1048
+ // Arquivo antes da instrução, como no exemplo da própria OpenAI: o modelo
1049
+ // lê o documento antes de saber o que tirar dele.
1050
+ const body: Record<string, unknown> = {
1051
+ input: [
1052
+ {
1053
+ role: 'user',
1054
+ content: [
1055
+ {
1056
+ type: 'input_file',
1057
+ filename: input.attachment.filename,
1058
+ file_data: `data:${input.attachment.mimeType};base64,${base64}`,
1059
+ },
1060
+ { type: 'input_text', text: input.message },
1061
+ ],
1062
+ },
1063
+ ],
1064
+ };
1065
+
1066
+ // `instructions` é o equivalente do `messages[0].role === 'system'`.
1067
+ if (input.systemPrompt) body.instructions = input.systemPrompt;
1068
+
1069
+ return applyOpenAiResponsesParams(
1070
+ body,
1071
+ {
1072
+ model: null,
1073
+ temperature: input.params?.temperature ?? null,
1074
+ maxTokens: input.params?.maxTokens ?? null,
1075
+ },
1076
+ {
1077
+ model: input.model,
1078
+ jsonObject: input.params?.responseFormat === 'json_object',
1079
+ },
1080
+ );
1081
+ }
1082
+
1083
+ /**
1084
+ * Texto da resposta da Responses API.
1085
+ *
1086
+ * `output_text` é conveniência do SDK — o JSON HTTP cru normalmente não traz.
1087
+ * O texto mora no array `output`, ao lado de itens `reasoning` que não têm
1088
+ * `content` nenhum. Percorrer o array é o caminho real; o atalho pelo
1089
+ * `output_text` só vale para quando o campo por acaso estiver lá.
1090
+ */
1091
+ private readOpenAiResponsesText(data: any): string {
1092
+ if (typeof data?.output_text === 'string' && data.output_text.trim()) {
1093
+ return data.output_text;
1094
+ }
1095
+
1096
+ const output: any[] = Array.isArray(data?.output) ? data.output : [];
1097
+ const chunks: string[] = [];
1098
+
1099
+ for (const item of output) {
1100
+ if (item?.type && item.type !== 'message') continue;
1101
+ for (const part of item?.content ?? []) {
1102
+ if (part?.type === 'output_text' && typeof part.text === 'string') {
1103
+ chunks.push(part.text);
1104
+ } else if (part?.type === 'refusal' && typeof part.refusal === 'string') {
1105
+ this.logger.warn(
1106
+ `[AI-RESPONSES] modelo recusou: ${part.refusal.slice(0, 200)}`,
1107
+ );
1108
+ }
1109
+ }
1110
+ }
1111
+
1112
+ return chunks.join('');
1113
+ }
1114
+
1115
+ /**
1116
+ * PDF como documento, via `POST /v1/responses`.
1117
+ *
1118
+ * É o único caminho em que um PDF escaneado chega ao modelo: para modelos com
1119
+ * visão a API extrai o texto e renderiza cada página como imagem. O
1120
+ * chat/completions, mesmo aceitando um content part de PDF, só extrai texto —
1121
+ * não serviria para um scan.
1122
+ */
1123
+ private async chatWithOpenAiResponses(input: {
1124
+ message: string;
1125
+ model: string;
1126
+ systemPrompt?: string;
1127
+ attachment: AiAttachment;
1128
+ apiKey: string;
1129
+ baseUrl: string;
1130
+ params?: AiChatModelParams;
1131
+ timeoutMs?: number;
1132
+ }): Promise<AiChatResult> {
1133
+ const pages = await this.countPdfPages(input.attachment.buffer);
1134
+
1135
+ if (pages > AiService.OPENAI_RESPONSES_MAX_PDF_PAGES) {
1136
+ this.logger.warn(
1137
+ `[AI-RESPONSES] "${input.attachment.filename}" tem ${pages} páginas ` +
1138
+ `(máx. ${AiService.OPENAI_RESPONSES_MAX_PDF_PAGES}); usando chat/completions.`,
1139
+ );
1140
+ // 422 é o status que o chamador trata como "degrade", e não como falha.
1141
+ throw Object.assign(new Error('PDF acima do limite de páginas'), {
1142
+ response: { status: 422, data: { error: { message: 'too many pages' } } },
1143
+ });
1144
+ }
1145
+
1146
+ const response = await axios.post(
1147
+ `${input.baseUrl.replace(/\/+$/, '')}/responses`,
1148
+ this.buildOpenAiResponsesBody(input),
1149
+ {
1150
+ headers: {
1151
+ Authorization: `Bearer ${input.apiKey}`,
1152
+ 'Content-Type': 'application/json',
1153
+ },
1154
+ // O corpo é o PDF em base64, na casa dos megabytes, e a renderização
1155
+ // das páginas é lenta: com os padrões do axios isso trunca ou trava.
1156
+ timeout: input.timeoutMs ?? DEFAULT_AI_CALL_TIMEOUT_MS,
1157
+ maxBodyLength: Infinity,
1158
+ maxContentLength: Infinity,
1159
+ },
1160
+ );
1161
+
1162
+ if (response.data?.status && response.data.status !== 'completed') {
1163
+ this.logger.warn(
1164
+ `[AI-RESPONSES] status=${response.data.status} ` +
1165
+ `reason=${response.data?.incomplete_details?.reason ?? 'n/a'} ` +
1166
+ `model=${input.model} file="${input.attachment.filename}"`,
1167
+ );
1168
+ }
1169
+
1170
+ const u = response.data?.usage;
1171
+ return {
1172
+ content: this.readOpenAiResponsesText(response.data),
1173
+ delivery: 'pdf-document',
1174
+ usage: u
1175
+ ? {
1176
+ input: u.input_tokens ?? 0,
1177
+ output: u.output_tokens ?? 0,
1178
+ total:
1179
+ u.total_tokens ?? (u.input_tokens ?? 0) + (u.output_tokens ?? 0),
1180
+ ...(u.input_tokens_details?.cached_tokens
1181
+ ? { cached: u.input_tokens_details.cached_tokens }
1182
+ : {}),
1183
+ }
1184
+ : undefined,
1185
+ };
1186
+ }
1187
+
837
1188
  /**
838
1189
  * Anthropic (Claude).
839
1190
  *
@@ -858,7 +1209,7 @@ export class AiService implements OnModuleInit {
858
1209
  systemPrompt?: string;
859
1210
  apiKey?: string;
860
1211
  params?: AiChatModelParams;
861
- }): Promise<{ content: string; usage?: { input: number; output: number; total: number } }> {
1212
+ }): Promise<AiChatResult> {
862
1213
  const key = apiKey ?? (await this.getAiApiKey('claude'));
863
1214
 
864
1215
  const body: Record<string, unknown> = {
@@ -908,24 +1259,27 @@ export class AiService implements OnModuleInit {
908
1259
  attachment?: AiAttachment | null;
909
1260
  apiKey?: string;
910
1261
  params?: AiChatModelParams;
911
- }): Promise<{ content: string; usage?: { input: number; output: number; total: number } }> {
1262
+ }): Promise<AiChatResult> {
912
1263
  const gemini = apiKey ?? (await this.getAiApiKey('gemini'));
913
1264
 
914
1265
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${gemini}`;
915
1266
 
916
1267
  const userParts: any[] = [{ text: message }];
1268
+ let delivery: AiAttachmentDelivery = 'none';
917
1269
 
918
1270
  if (attachment) {
919
1271
  if (this.isTextMime(attachment.mimeType)) {
920
1272
  userParts.push({
921
1273
  text: attachedFileBlock(attachment.filename, this.toUtf8Text(attachment.buffer)),
922
1274
  });
1275
+ delivery = 'text';
923
1276
  } else if (this.isPdfMime(attachment.mimeType)) {
924
1277
  const pdfText = await this.extractPdfText(attachment.buffer);
925
1278
  if (pdfText) {
926
1279
  userParts.push({
927
1280
  text: attachedFileBlock(attachment.filename, pdfText),
928
1281
  });
1282
+ delivery = 'pdf-text';
929
1283
  } else {
930
1284
  userParts.push({
931
1285
  inline_data: {
@@ -933,6 +1287,7 @@ export class AiService implements OnModuleInit {
933
1287
  data: attachment.buffer.toString('base64'),
934
1288
  },
935
1289
  });
1290
+ delivery = 'pdf-document';
936
1291
  }
937
1292
  } else {
938
1293
  userParts.push({
@@ -941,6 +1296,7 @@ export class AiService implements OnModuleInit {
941
1296
  data: attachment.buffer.toString('base64'),
942
1297
  },
943
1298
  });
1299
+ delivery = this.isImageMime(attachment.mimeType) ? 'image' : 'binary';
944
1300
  }
945
1301
  }
946
1302
 
@@ -971,6 +1327,7 @@ export class AiService implements OnModuleInit {
971
1327
  const u = response.data?.usageMetadata;
972
1328
  return {
973
1329
  content,
1330
+ delivery,
974
1331
  usage: u ? { input: u.promptTokenCount ?? 0, output: u.candidatesTokenCount ?? 0, total: u.totalTokenCount ?? 0 } : undefined,
975
1332
  };
976
1333
  }
@@ -1007,40 +1364,56 @@ export class AiService implements OnModuleInit {
1007
1364
  return null;
1008
1365
  }
1009
1366
 
1367
+ /**
1368
+ * Conteúdo da mensagem do usuário no chat/completions, mais como o anexo foi
1369
+ * parar lá dentro.
1370
+ *
1371
+ * O `delivery` sai daqui porque é aqui que a decisão acontece: um PDF sem
1372
+ * camada de texto vira o marcador de ilegível, e só este método sabe disso.
1373
+ */
1010
1374
  private async buildOpenAiUserContent(
1011
1375
  message: string,
1012
1376
  attachment?: AiAttachment | null,
1013
1377
  supportsImages = true,
1014
- ) {
1378
+ ): Promise<{ content: unknown; delivery: AiAttachmentDelivery }> {
1015
1379
  if (!attachment) {
1016
- return message;
1380
+ return { content: message, delivery: 'none' };
1017
1381
  }
1018
1382
 
1019
1383
  if (supportsImages && this.isImageMime(attachment.mimeType)) {
1020
1384
  const dataUri = `data:${attachment.mimeType};base64,${attachment.buffer.toString('base64')}`;
1021
- return [
1022
- { type: 'text', text: message },
1023
- { type: 'image_url', image_url: { url: dataUri } },
1024
- ];
1385
+ return {
1386
+ content: [
1387
+ { type: 'text', text: message },
1388
+ { type: 'image_url', image_url: { url: dataUri } },
1389
+ ],
1390
+ delivery: 'image',
1391
+ };
1025
1392
  }
1026
1393
 
1027
1394
  let text: string;
1395
+ let delivery: AiAttachmentDelivery;
1028
1396
 
1029
1397
  if (this.isTextMime(attachment.mimeType)) {
1030
1398
  text = this.toUtf8Text(attachment.buffer);
1399
+ delivery = 'text';
1031
1400
  } else if (this.isPdfMime(attachment.mimeType)) {
1032
- text =
1033
- (await this.extractPdfText(attachment.buffer)) ||
1034
- unreadablePdfMarker(attachment.filename);
1401
+ const pdfText = await this.extractPdfText(attachment.buffer);
1402
+ text = pdfText || unreadablePdfMarker(attachment.filename);
1403
+ delivery = pdfText ? 'pdf-text' : 'unreadable-pdf';
1035
1404
  } else {
1036
1405
  text = binaryAttachmentMarker(
1037
1406
  attachment.filename,
1038
1407
  attachment.mimeType,
1039
1408
  attachment.buffer.length,
1040
1409
  );
1410
+ delivery = 'binary';
1041
1411
  }
1042
1412
 
1043
- return `${message}${attachedFileBlock(attachment.filename, text)}`;
1413
+ return {
1414
+ content: `${message}${attachedFileBlock(attachment.filename, text)}`,
1415
+ delivery,
1416
+ };
1044
1417
  }
1045
1418
 
1046
1419
  private isImageMime(mimeType: string) {
@@ -1100,6 +1473,15 @@ export class AiService implements OnModuleInit {
1100
1473
  }
1101
1474
  }
1102
1475
 
1476
+ /** Páginas do PDF, ou 0 quando ele não abre — nesse caso o teto não barra. */
1477
+ private async countPdfPages(buffer: Buffer): Promise<number> {
1478
+ try {
1479
+ return (await pdfParse(buffer))?.numpages ?? 0;
1480
+ } catch {
1481
+ return 0;
1482
+ }
1483
+ }
1484
+
1103
1485
  private toUtf8Text(buffer: Buffer) {
1104
1486
  return buffer.toString('utf8');
1105
1487
  }
@@ -2,6 +2,7 @@ import {
2
2
  applyClaudeModelParams,
3
3
  applyGeminiModelParams,
4
4
  applyOpenAiModelParams,
5
+ applyOpenAiResponsesParams,
5
6
  AiPromptResolverService,
6
7
  mergeModelParams,
7
8
  type ResolvedPrompt,
@@ -279,6 +280,49 @@ describe('mergeModelParams', () => {
279
280
  });
280
281
  });
281
282
 
283
+ describe('applyOpenAiResponsesParams', () => {
284
+ const input = [{ role: 'user', content: [{ type: 'input_text', text: 'oi' }] }];
285
+
286
+ it('não adiciona nada quando o registro e os defaults estão vazios', () => {
287
+ const body = applyOpenAiResponsesParams({ input }, resolved(), {
288
+ model: 'gpt-4o',
289
+ });
290
+
291
+ expect(body).toEqual({ model: 'gpt-4o', input });
292
+ });
293
+
294
+ // Os três renames do endpoint moram só nesta função.
295
+ it('usa max_output_tokens, nunca max_tokens', () => {
296
+ const body = applyOpenAiResponsesParams({ input }, resolved({ maxTokens: 500 }), {
297
+ model: 'gpt-4o',
298
+ });
299
+
300
+ expect(body.max_output_tokens).toBe(500);
301
+ expect(body).not.toHaveProperty('max_tokens');
302
+ expect(body).not.toHaveProperty('max_completion_tokens');
303
+ });
304
+
305
+ it('põe o json_object sob text.format', () => {
306
+ const body = applyOpenAiResponsesParams({ input }, resolved(), {
307
+ model: 'gpt-4o',
308
+ jsonObject: true,
309
+ });
310
+
311
+ expect(body.text).toEqual({ format: { type: 'json_object' } });
312
+ expect(body).not.toHaveProperty('response_format');
313
+ });
314
+
315
+ it('o modelo do registro vence o default do código', () => {
316
+ const body = applyOpenAiResponsesParams(
317
+ { input },
318
+ resolved({ model: 'gpt-4.1', temperature: 0.3 }),
319
+ { model: 'gpt-4o' },
320
+ );
321
+
322
+ expect(body).toMatchObject({ model: 'gpt-4.1', temperature: 0.3 });
323
+ });
324
+ });
325
+
282
326
  describe('applyOpenAiModelParams', () => {
283
327
  const messages = [{ role: 'user', content: 'oi' }];
284
328