@fgv/ts-extras 5.1.0-2 → 5.1.0-21

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 (272) hide show
  1. package/dist/index.browser.js +2 -1
  2. package/dist/index.browser.js.map +1 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/packlets/ai-assist/apiClient.js +807 -67
  5. package/dist/packlets/ai-assist/apiClient.js.map +1 -0
  6. package/dist/packlets/ai-assist/chatRequestBuilders.js +180 -0
  7. package/dist/packlets/ai-assist/chatRequestBuilders.js.map +1 -0
  8. package/dist/packlets/ai-assist/converters.js +2 -1
  9. package/dist/packlets/ai-assist/converters.js.map +1 -0
  10. package/dist/packlets/ai-assist/endpoint.js +78 -0
  11. package/dist/packlets/ai-assist/endpoint.js.map +1 -0
  12. package/dist/packlets/ai-assist/index.js +4 -3
  13. package/dist/packlets/ai-assist/index.js.map +1 -0
  14. package/dist/packlets/ai-assist/model.js +20 -3
  15. package/dist/packlets/ai-assist/model.js.map +1 -0
  16. package/dist/packlets/ai-assist/registry.js +137 -10
  17. package/dist/packlets/ai-assist/registry.js.map +1 -0
  18. package/dist/packlets/ai-assist/sseParser.js +122 -0
  19. package/dist/packlets/ai-assist/sseParser.js.map +1 -0
  20. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js +192 -0
  21. package/dist/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -0
  22. package/dist/packlets/ai-assist/streamingAdapters/common.js +77 -0
  23. package/dist/packlets/ai-assist/streamingAdapters/common.js.map +1 -0
  24. package/dist/packlets/ai-assist/streamingAdapters/gemini.js +160 -0
  25. package/dist/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -0
  26. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js +150 -0
  27. package/dist/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -0
  28. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js +164 -0
  29. package/dist/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -0
  30. package/dist/packlets/ai-assist/streamingAdapters/proxy.js +157 -0
  31. package/dist/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -0
  32. package/dist/packlets/ai-assist/streamingClient.js +94 -0
  33. package/dist/packlets/ai-assist/streamingClient.js.map +1 -0
  34. package/dist/packlets/ai-assist/toolFormats.js.map +1 -0
  35. package/dist/packlets/conversion/converters.js +34 -1
  36. package/dist/packlets/conversion/converters.js.map +1 -0
  37. package/dist/packlets/conversion/index.js.map +1 -0
  38. package/dist/packlets/crypto-utils/constants.js.map +1 -0
  39. package/dist/packlets/crypto-utils/converters.js.map +1 -0
  40. package/dist/packlets/crypto-utils/directEncryptionProvider.js.map +1 -0
  41. package/dist/packlets/crypto-utils/encryptedFile.js.map +1 -0
  42. package/dist/packlets/crypto-utils/index.browser.js +2 -0
  43. package/dist/packlets/crypto-utils/index.browser.js.map +1 -0
  44. package/dist/packlets/crypto-utils/index.js +2 -0
  45. package/dist/packlets/crypto-utils/index.js.map +1 -0
  46. package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js +63 -0
  47. package/dist/packlets/crypto-utils/keyPairAlgorithmParams.js.map +1 -0
  48. package/dist/packlets/crypto-utils/keystore/converters.js +101 -9
  49. package/dist/packlets/crypto-utils/keystore/converters.js.map +1 -0
  50. package/dist/packlets/crypto-utils/keystore/index.js +1 -0
  51. package/dist/packlets/crypto-utils/keystore/index.js.map +1 -0
  52. package/dist/packlets/crypto-utils/keystore/keyStore.js +431 -118
  53. package/dist/packlets/crypto-utils/keystore/keyStore.js.map +1 -0
  54. package/dist/packlets/crypto-utils/keystore/model.js +22 -1
  55. package/dist/packlets/crypto-utils/keystore/model.js.map +1 -0
  56. package/dist/packlets/crypto-utils/keystore/privateKeyStorage.js +21 -0
  57. package/dist/packlets/crypto-utils/keystore/privateKeyStorage.js.map +1 -0
  58. package/dist/packlets/crypto-utils/model.js +10 -0
  59. package/dist/packlets/crypto-utils/model.js.map +1 -0
  60. package/dist/packlets/crypto-utils/nodeCryptoProvider.js +163 -1
  61. package/dist/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -0
  62. package/dist/packlets/csv/csvFileHelpers.js.map +1 -0
  63. package/dist/packlets/csv/csvHelpers.js.map +1 -0
  64. package/dist/packlets/csv/index.browser.js.map +1 -0
  65. package/dist/packlets/csv/index.js.map +1 -0
  66. package/dist/packlets/experimental/extendedArray.js.map +1 -0
  67. package/dist/packlets/experimental/formatter.js.map +1 -0
  68. package/dist/packlets/experimental/index.js.map +1 -0
  69. package/dist/packlets/experimental/rangeOf.js.map +1 -0
  70. package/dist/packlets/hash/index.browser.js.map +1 -0
  71. package/dist/packlets/hash/index.js.map +1 -0
  72. package/dist/packlets/hash/index.node.js.map +1 -0
  73. package/dist/packlets/hash/md5Normalizer.browser.js.map +1 -0
  74. package/dist/packlets/hash/md5Normalizer.js.map +1 -0
  75. package/dist/packlets/mustache/index.js.map +1 -0
  76. package/dist/packlets/mustache/interfaces.js.map +1 -0
  77. package/dist/packlets/mustache/mustacheTemplate.js.map +1 -0
  78. package/dist/packlets/record-jar/index.browser.js.map +1 -0
  79. package/dist/packlets/record-jar/index.js.map +1 -0
  80. package/dist/packlets/record-jar/recordJarFileHelpers.js.map +1 -0
  81. package/dist/packlets/record-jar/recordJarHelpers.js.map +1 -0
  82. package/dist/packlets/yaml/converters.js.map +1 -0
  83. package/dist/packlets/yaml/index.js +1 -0
  84. package/dist/packlets/yaml/index.js.map +1 -0
  85. package/dist/packlets/yaml/serializers.js +48 -0
  86. package/dist/packlets/yaml/serializers.js.map +1 -0
  87. package/dist/packlets/zip-file-tree/index.js.map +1 -0
  88. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +2 -2
  89. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -0
  90. package/dist/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -0
  91. package/dist/ts-extras.d.ts +1499 -41
  92. package/dist/tsdoc-metadata.json +1 -1
  93. package/lib/index.browser.d.ts +2 -1
  94. package/lib/index.browser.d.ts.map +1 -0
  95. package/lib/index.browser.js +3 -1
  96. package/lib/index.browser.js.map +1 -0
  97. package/lib/index.d.ts.map +1 -0
  98. package/lib/index.js.map +1 -0
  99. package/lib/packlets/ai-assist/apiClient.d.ts +140 -1
  100. package/lib/packlets/ai-assist/apiClient.d.ts.map +1 -0
  101. package/lib/packlets/ai-assist/apiClient.js +810 -66
  102. package/lib/packlets/ai-assist/apiClient.js.map +1 -0
  103. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts +89 -0
  104. package/lib/packlets/ai-assist/chatRequestBuilders.d.ts.map +1 -0
  105. package/lib/packlets/ai-assist/chatRequestBuilders.js +189 -0
  106. package/lib/packlets/ai-assist/chatRequestBuilders.js.map +1 -0
  107. package/lib/packlets/ai-assist/converters.d.ts.map +1 -0
  108. package/lib/packlets/ai-assist/converters.js +2 -1
  109. package/lib/packlets/ai-assist/converters.js.map +1 -0
  110. package/lib/packlets/ai-assist/endpoint.d.ts +28 -0
  111. package/lib/packlets/ai-assist/endpoint.d.ts.map +1 -0
  112. package/lib/packlets/ai-assist/endpoint.js +82 -0
  113. package/lib/packlets/ai-assist/endpoint.js.map +1 -0
  114. package/lib/packlets/ai-assist/index.d.ts +4 -3
  115. package/lib/packlets/ai-assist/index.d.ts.map +1 -0
  116. package/lib/packlets/ai-assist/index.js +12 -1
  117. package/lib/packlets/ai-assist/index.js.map +1 -0
  118. package/lib/packlets/ai-assist/model.d.ts +341 -3
  119. package/lib/packlets/ai-assist/model.d.ts.map +1 -0
  120. package/lib/packlets/ai-assist/model.js +21 -3
  121. package/lib/packlets/ai-assist/model.js.map +1 -0
  122. package/lib/packlets/ai-assist/registry.d.ts +34 -1
  123. package/lib/packlets/ai-assist/registry.d.ts.map +1 -0
  124. package/lib/packlets/ai-assist/registry.js +140 -11
  125. package/lib/packlets/ai-assist/registry.js.map +1 -0
  126. package/lib/packlets/ai-assist/sseParser.d.ts +45 -0
  127. package/lib/packlets/ai-assist/sseParser.d.ts.map +1 -0
  128. package/lib/packlets/ai-assist/sseParser.js +127 -0
  129. package/lib/packlets/ai-assist/sseParser.js.map +1 -0
  130. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts +18 -0
  131. package/lib/packlets/ai-assist/streamingAdapters/anthropic.d.ts.map +1 -0
  132. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js +195 -0
  133. package/lib/packlets/ai-assist/streamingAdapters/anthropic.js.map +1 -0
  134. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts +79 -0
  135. package/lib/packlets/ai-assist/streamingAdapters/common.d.ts.map +1 -0
  136. package/lib/packlets/ai-assist/streamingAdapters/common.js +81 -0
  137. package/lib/packlets/ai-assist/streamingAdapters/common.js.map +1 -0
  138. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts +19 -0
  139. package/lib/packlets/ai-assist/streamingAdapters/gemini.d.ts.map +1 -0
  140. package/lib/packlets/ai-assist/streamingAdapters/gemini.js +163 -0
  141. package/lib/packlets/ai-assist/streamingAdapters/gemini.js.map +1 -0
  142. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts +18 -0
  143. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.d.ts.map +1 -0
  144. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js +153 -0
  145. package/lib/packlets/ai-assist/streamingAdapters/openaiChat.js.map +1 -0
  146. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts +19 -0
  147. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.d.ts.map +1 -0
  148. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js +167 -0
  149. package/lib/packlets/ai-assist/streamingAdapters/openaiResponses.js.map +1 -0
  150. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts +34 -0
  151. package/lib/packlets/ai-assist/streamingAdapters/proxy.d.ts.map +1 -0
  152. package/lib/packlets/ai-assist/streamingAdapters/proxy.js +160 -0
  153. package/lib/packlets/ai-assist/streamingAdapters/proxy.js.map +1 -0
  154. package/lib/packlets/ai-assist/streamingClient.d.ts +33 -0
  155. package/lib/packlets/ai-assist/streamingClient.d.ts.map +1 -0
  156. package/lib/packlets/ai-assist/streamingClient.js +99 -0
  157. package/lib/packlets/ai-assist/streamingClient.js.map +1 -0
  158. package/lib/packlets/ai-assist/toolFormats.d.ts.map +1 -0
  159. package/lib/packlets/ai-assist/toolFormats.js.map +1 -0
  160. package/lib/packlets/conversion/converters.d.ts +8 -1
  161. package/lib/packlets/conversion/converters.d.ts.map +1 -0
  162. package/lib/packlets/conversion/converters.js +35 -2
  163. package/lib/packlets/conversion/converters.js.map +1 -0
  164. package/lib/packlets/conversion/index.d.ts.map +1 -0
  165. package/lib/packlets/conversion/index.js.map +1 -0
  166. package/lib/packlets/crypto-utils/constants.d.ts.map +1 -0
  167. package/lib/packlets/crypto-utils/constants.js.map +1 -0
  168. package/lib/packlets/crypto-utils/converters.d.ts.map +1 -0
  169. package/lib/packlets/crypto-utils/converters.js.map +1 -0
  170. package/lib/packlets/crypto-utils/directEncryptionProvider.d.ts.map +1 -0
  171. package/lib/packlets/crypto-utils/directEncryptionProvider.js.map +1 -0
  172. package/lib/packlets/crypto-utils/encryptedFile.d.ts.map +1 -0
  173. package/lib/packlets/crypto-utils/encryptedFile.js.map +1 -0
  174. package/lib/packlets/crypto-utils/index.browser.d.ts +1 -0
  175. package/lib/packlets/crypto-utils/index.browser.d.ts.map +1 -0
  176. package/lib/packlets/crypto-utils/index.browser.js +4 -1
  177. package/lib/packlets/crypto-utils/index.browser.js.map +1 -0
  178. package/lib/packlets/crypto-utils/index.d.ts +1 -0
  179. package/lib/packlets/crypto-utils/index.d.ts.map +1 -0
  180. package/lib/packlets/crypto-utils/index.js +4 -1
  181. package/lib/packlets/crypto-utils/index.js.map +1 -0
  182. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts +50 -0
  183. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.d.ts.map +1 -0
  184. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js +66 -0
  185. package/lib/packlets/crypto-utils/keyPairAlgorithmParams.js.map +1 -0
  186. package/lib/packlets/crypto-utils/keystore/converters.d.ts +68 -6
  187. package/lib/packlets/crypto-utils/keystore/converters.d.ts.map +1 -0
  188. package/lib/packlets/crypto-utils/keystore/converters.js +100 -8
  189. package/lib/packlets/crypto-utils/keystore/converters.js.map +1 -0
  190. package/lib/packlets/crypto-utils/keystore/index.d.ts +1 -0
  191. package/lib/packlets/crypto-utils/keystore/index.d.ts.map +1 -0
  192. package/lib/packlets/crypto-utils/keystore/index.js +1 -0
  193. package/lib/packlets/crypto-utils/keystore/index.js.map +1 -0
  194. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +125 -12
  195. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts.map +1 -0
  196. package/lib/packlets/crypto-utils/keystore/keyStore.js +431 -118
  197. package/lib/packlets/crypto-utils/keystore/keyStore.js.map +1 -0
  198. package/lib/packlets/crypto-utils/keystore/model.d.ts +248 -17
  199. package/lib/packlets/crypto-utils/keystore/model.d.ts.map +1 -0
  200. package/lib/packlets/crypto-utils/keystore/model.js +24 -2
  201. package/lib/packlets/crypto-utils/keystore/model.js.map +1 -0
  202. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.d.ts +50 -0
  203. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.d.ts.map +1 -0
  204. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.js +22 -0
  205. package/lib/packlets/crypto-utils/keystore/privateKeyStorage.js.map +1 -0
  206. package/lib/packlets/crypto-utils/model.d.ts +145 -0
  207. package/lib/packlets/crypto-utils/model.d.ts.map +1 -0
  208. package/lib/packlets/crypto-utils/model.js +11 -1
  209. package/lib/packlets/crypto-utils/model.js.map +1 -0
  210. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +51 -1
  211. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts.map +1 -0
  212. package/lib/packlets/crypto-utils/nodeCryptoProvider.js +162 -0
  213. package/lib/packlets/crypto-utils/nodeCryptoProvider.js.map +1 -0
  214. package/lib/packlets/csv/csvFileHelpers.d.ts.map +1 -0
  215. package/lib/packlets/csv/csvFileHelpers.js.map +1 -0
  216. package/lib/packlets/csv/csvHelpers.d.ts.map +1 -0
  217. package/lib/packlets/csv/csvHelpers.js.map +1 -0
  218. package/lib/packlets/csv/index.browser.d.ts.map +1 -0
  219. package/lib/packlets/csv/index.browser.js.map +1 -0
  220. package/lib/packlets/csv/index.d.ts.map +1 -0
  221. package/lib/packlets/csv/index.js.map +1 -0
  222. package/lib/packlets/experimental/extendedArray.d.ts.map +1 -0
  223. package/lib/packlets/experimental/extendedArray.js.map +1 -0
  224. package/lib/packlets/experimental/formatter.d.ts.map +1 -0
  225. package/lib/packlets/experimental/formatter.js.map +1 -0
  226. package/lib/packlets/experimental/index.d.ts.map +1 -0
  227. package/lib/packlets/experimental/index.js.map +1 -0
  228. package/lib/packlets/experimental/rangeOf.d.ts.map +1 -0
  229. package/lib/packlets/experimental/rangeOf.js.map +1 -0
  230. package/lib/packlets/hash/index.browser.d.ts.map +1 -0
  231. package/lib/packlets/hash/index.browser.js.map +1 -0
  232. package/lib/packlets/hash/index.d.ts.map +1 -0
  233. package/lib/packlets/hash/index.js.map +1 -0
  234. package/lib/packlets/hash/index.node.d.ts.map +1 -0
  235. package/lib/packlets/hash/index.node.js.map +1 -0
  236. package/lib/packlets/hash/md5Normalizer.browser.d.ts.map +1 -0
  237. package/lib/packlets/hash/md5Normalizer.browser.js.map +1 -0
  238. package/lib/packlets/hash/md5Normalizer.d.ts.map +1 -0
  239. package/lib/packlets/hash/md5Normalizer.js.map +1 -0
  240. package/lib/packlets/mustache/index.d.ts.map +1 -0
  241. package/lib/packlets/mustache/index.js.map +1 -0
  242. package/lib/packlets/mustache/interfaces.d.ts.map +1 -0
  243. package/lib/packlets/mustache/interfaces.js.map +1 -0
  244. package/lib/packlets/mustache/mustacheTemplate.d.ts.map +1 -0
  245. package/lib/packlets/mustache/mustacheTemplate.js.map +1 -0
  246. package/lib/packlets/record-jar/index.browser.d.ts.map +1 -0
  247. package/lib/packlets/record-jar/index.browser.js.map +1 -0
  248. package/lib/packlets/record-jar/index.d.ts.map +1 -0
  249. package/lib/packlets/record-jar/index.js.map +1 -0
  250. package/lib/packlets/record-jar/recordJarFileHelpers.d.ts.map +1 -0
  251. package/lib/packlets/record-jar/recordJarFileHelpers.js.map +1 -0
  252. package/lib/packlets/record-jar/recordJarHelpers.d.ts.map +1 -0
  253. package/lib/packlets/record-jar/recordJarHelpers.js.map +1 -0
  254. package/lib/packlets/yaml/converters.d.ts.map +1 -0
  255. package/lib/packlets/yaml/converters.js.map +1 -0
  256. package/lib/packlets/yaml/index.d.ts +1 -0
  257. package/lib/packlets/yaml/index.d.ts.map +1 -0
  258. package/lib/packlets/yaml/index.js +1 -0
  259. package/lib/packlets/yaml/index.js.map +1 -0
  260. package/lib/packlets/yaml/serializers.d.ts +45 -0
  261. package/lib/packlets/yaml/serializers.d.ts.map +1 -0
  262. package/lib/packlets/yaml/serializers.js +84 -0
  263. package/lib/packlets/yaml/serializers.js.map +1 -0
  264. package/lib/packlets/zip-file-tree/index.d.ts.map +1 -0
  265. package/lib/packlets/zip-file-tree/index.js.map +1 -0
  266. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +2 -2
  267. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts.map +1 -0
  268. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +2 -2
  269. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -0
  270. package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts.map +1 -0
  271. package/lib/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -0
  272. package/package.json +24 -23
@@ -30,41 +30,77 @@
30
30
  * @packageDocumentation
31
31
  */
32
32
  import { isJsonObject } from '@fgv/ts-json-base';
33
- import { fail, succeed, Validators } from '@fgv/ts-utils';
33
+ import { fail, mapResults, succeed, Validators } from '@fgv/ts-utils';
34
34
  import { resolveModel } from './model';
35
+ import { buildAnthropicMessages, buildGeminiContents, buildMessages, buildOpenAiChatUserContent, buildOpenAiResponsesUserContent } from './chatRequestBuilders';
36
+ import { bearerAuthHeader, resolveEffectiveBaseUrl } from './endpoint';
37
+ import { DEFAULT_MODEL_CAPABILITY_CONFIG, resolveImageCapability, supportsImageGeneration } from './registry';
35
38
  import { toAnthropicTools, toGeminiTools, toResponsesApiTools } from './toolFormats';
36
39
  // ============================================================================
37
40
  // Shared helpers
38
41
  // ============================================================================
39
42
  /**
40
- * Builds the messages array from prompt + optional correction messages.
43
+ * Makes an HTTP request and returns the parsed JSON, or a failure.
41
44
  * @internal
42
45
  */
43
- function buildMessages(prompt, additionalMessages) {
44
- const messages = [
45
- { role: 'system', content: prompt.system },
46
- { role: 'user', content: prompt.user }
47
- ];
48
- if (additionalMessages) {
49
- for (const msg of additionalMessages) {
50
- messages.push({ role: msg.role, content: msg.content });
51
- }
46
+ async function fetchJson(url, headers, body, logger, signal) {
47
+ /* c8 ignore next 1 - optional logger */
48
+ logger === null || logger === void 0 ? void 0 : logger.detail(`AI API request: POST ${url}`);
49
+ let response;
50
+ try {
51
+ response = await fetch(url, {
52
+ method: 'POST',
53
+ headers: Object.assign({ 'Content-Type': 'application/json' }, headers),
54
+ body: JSON.stringify(body),
55
+ signal
56
+ });
57
+ }
58
+ catch (err) {
59
+ const detail = err instanceof Error ? err.message : String(err);
60
+ /* c8 ignore next 1 - optional logger */
61
+ logger === null || logger === void 0 ? void 0 : logger.error(`AI API request failed: ${detail}`);
62
+ return fail(`AI API request failed: ${detail}`);
63
+ }
64
+ if (!response.ok) {
65
+ const errorText = await response.text().catch(() => 'unknown error');
66
+ /* c8 ignore next 1 - optional logger */
67
+ logger === null || logger === void 0 ? void 0 : logger.error(`AI API returned ${response.status}: ${errorText}`);
68
+ return fail(`AI API returned ${response.status}: ${errorText}`);
52
69
  }
53
- return messages;
70
+ /* c8 ignore next 1 - optional logger */
71
+ logger === null || logger === void 0 ? void 0 : logger.detail(`AI API response: ${response.status}`);
72
+ let json;
73
+ try {
74
+ json = await response.json();
75
+ }
76
+ catch (_a) {
77
+ /* c8 ignore next 1 - optional logger */
78
+ logger === null || logger === void 0 ? void 0 : logger.error('AI API returned invalid JSON response');
79
+ return fail('AI API returned invalid JSON response');
80
+ }
81
+ if (!isJsonObject(json)) {
82
+ /* c8 ignore next 1 - optional logger */
83
+ logger === null || logger === void 0 ? void 0 : logger.error('AI API returned non-object JSON response');
84
+ return fail('AI API returned non-object JSON response');
85
+ }
86
+ return succeed(json);
54
87
  }
55
88
  /**
56
- * Makes an HTTP request and returns the parsed JSON, or a failure.
89
+ * Makes a multipart/form-data POST request and returns the parsed JSON, or a
90
+ * failure. The Content-Type header (with boundary) is set automatically by
91
+ * `fetch` from the `FormData` body — callers must NOT pass it explicitly.
57
92
  * @internal
58
93
  */
59
- async function fetchJson(url, headers, body, logger) {
94
+ async function fetchMultipart(url, headers, body, logger, signal) {
60
95
  /* c8 ignore next 1 - optional logger */
61
- logger === null || logger === void 0 ? void 0 : logger.detail(`AI API request: POST ${url}`);
96
+ logger === null || logger === void 0 ? void 0 : logger.detail(`AI API request: POST ${url} (multipart)`);
62
97
  let response;
63
98
  try {
64
99
  response = await fetch(url, {
65
100
  method: 'POST',
66
- headers: Object.assign({ 'Content-Type': 'application/json' }, headers),
67
- body: JSON.stringify(body)
101
+ headers,
102
+ body,
103
+ signal
68
104
  });
69
105
  }
70
106
  catch (err) {
@@ -97,6 +133,95 @@ async function fetchJson(url, headers, body, logger) {
97
133
  }
98
134
  return succeed(json);
99
135
  }
136
+ /**
137
+ * Decodes a base64-encoded image attachment into a `Blob` suitable for use as
138
+ * a multipart file field. On Node hands the `Buffer` straight to `Blob`
139
+ * (Buffer extends Uint8Array) to skip an intermediate copy; falls back to
140
+ * `atob` in browsers. Inputs come from `FileReader` or prior provider
141
+ * responses, which are trusted to be valid. Note that Node's
142
+ * `Buffer.from(..., 'base64')` silently strips invalid characters rather
143
+ * than throwing, so failures are only observable in the browser path.
144
+ * @internal
145
+ */
146
+ function attachmentToBlob(attachment) {
147
+ if (typeof Buffer !== 'undefined') {
148
+ return succeed(new Blob([Buffer.from(attachment.base64, 'base64')], { type: attachment.mimeType }));
149
+ }
150
+ /* c8 ignore start - Browser-only fallback cannot be tested in Node.js environment */
151
+ try {
152
+ const binary = atob(attachment.base64);
153
+ const bytes = new Uint8Array(binary.length);
154
+ for (let i = 0; i < binary.length; i++) {
155
+ bytes[i] = binary.charCodeAt(i);
156
+ }
157
+ return succeed(new Blob([bytes], { type: attachment.mimeType }));
158
+ }
159
+ catch (e) {
160
+ const message = e instanceof Error ? e.message : String(e);
161
+ return fail(`Invalid base64: ${message}`);
162
+ }
163
+ /* c8 ignore stop */
164
+ }
165
+ /**
166
+ * Maps a MIME type to a sensible file extension for multipart filenames.
167
+ * @internal
168
+ */
169
+ function extensionForMimeType(mimeType) {
170
+ switch (mimeType) {
171
+ case 'image/png':
172
+ return 'png';
173
+ case 'image/jpeg':
174
+ case 'image/jpg':
175
+ return 'jpg';
176
+ case 'image/webp':
177
+ return 'webp';
178
+ case 'image/gif':
179
+ return 'gif';
180
+ default:
181
+ return 'bin';
182
+ }
183
+ }
184
+ /**
185
+ * Makes an HTTP GET request and returns the parsed JSON, or a failure.
186
+ * @internal
187
+ */
188
+ async function fetchGetJson(url, headers, logger, signal) {
189
+ /* c8 ignore next 1 - optional logger */
190
+ logger === null || logger === void 0 ? void 0 : logger.detail(`AI API request: GET ${url}`);
191
+ let response;
192
+ try {
193
+ response = await fetch(url, { method: 'GET', headers, signal });
194
+ }
195
+ catch (err) {
196
+ const detail = err instanceof Error ? err.message : String(err);
197
+ /* c8 ignore next 1 - optional logger */
198
+ logger === null || logger === void 0 ? void 0 : logger.error(`AI API request failed: ${detail}`);
199
+ return fail(`AI API request failed: ${detail}`);
200
+ }
201
+ if (!response.ok) {
202
+ const errorText = await response.text().catch(() => 'unknown error');
203
+ /* c8 ignore next 1 - optional logger */
204
+ logger === null || logger === void 0 ? void 0 : logger.error(`AI API returned ${response.status}: ${errorText}`);
205
+ return fail(`AI API returned ${response.status}: ${errorText}`);
206
+ }
207
+ /* c8 ignore next 1 - optional logger */
208
+ logger === null || logger === void 0 ? void 0 : logger.detail(`AI API response: ${response.status}`);
209
+ let json;
210
+ try {
211
+ json = await response.json();
212
+ }
213
+ catch (_a) {
214
+ /* c8 ignore next 1 - optional logger */
215
+ logger === null || logger === void 0 ? void 0 : logger.error('AI API returned invalid JSON response');
216
+ return fail('AI API returned invalid JSON response');
217
+ }
218
+ if (!isJsonObject(json)) {
219
+ /* c8 ignore next 1 - optional logger */
220
+ logger === null || logger === void 0 ? void 0 : logger.error('AI API returned non-object JSON response');
221
+ return fail('AI API returned non-object JSON response');
222
+ }
223
+ return succeed(json);
224
+ }
100
225
  const openAiMessage = Validators.object({
101
226
  content: Validators.string
102
227
  });
@@ -149,16 +274,16 @@ const geminiResponse = Validators.object({
149
274
  * Works for xAI Grok, OpenAI, Groq, and Mistral.
150
275
  * @internal
151
276
  */
152
- async function callOpenAiCompletion(config, prompt, additionalMessages, temperature = 0.7, logger) {
277
+ async function callOpenAiCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, signal) {
153
278
  const url = `${config.baseUrl}/chat/completions`;
154
- const messages = buildMessages(prompt, additionalMessages);
279
+ const messages = buildMessages(prompt.system, buildOpenAiChatUserContent(prompt), {
280
+ tail: additionalMessages
281
+ });
155
282
  const body = { model: config.model, messages, temperature };
156
- const headers = {
157
- Authorization: `Bearer ${config.apiKey}`
158
- };
283
+ const headers = bearerAuthHeader(config.apiKey);
159
284
  /* c8 ignore next 1 - optional logger */
160
285
  logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI completion: model=${config.model}`);
161
- const jsonResult = await fetchJson(url, headers, body, logger);
286
+ const jsonResult = await fetchJson(url, headers, body, logger, signal);
162
287
  if (jsonResult.isFailure()) {
163
288
  return fail(jsonResult.message);
164
289
  }
@@ -197,21 +322,21 @@ function extractResponsesApiText(output) {
197
322
  * Used when tools are configured for an openai-format provider.
198
323
  * @internal
199
324
  */
200
- async function callOpenAiResponsesCompletion(config, prompt, tools, additionalMessages, temperature = 0.7, logger) {
325
+ async function callOpenAiResponsesCompletion(config, prompt, tools, additionalMessages, temperature = 0.7, logger, signal) {
201
326
  const url = `${config.baseUrl}/responses`;
202
- const input = buildMessages(prompt, additionalMessages);
327
+ const input = buildMessages(prompt.system, buildOpenAiResponsesUserContent(prompt), {
328
+ tail: additionalMessages
329
+ });
203
330
  const body = {
204
331
  model: config.model,
205
332
  input,
206
333
  tools: toResponsesApiTools(tools),
207
334
  temperature
208
335
  };
209
- const headers = {
210
- Authorization: `Bearer ${config.apiKey}`
211
- };
336
+ const headers = bearerAuthHeader(config.apiKey);
212
337
  /* c8 ignore next 1 - optional logger */
213
338
  logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI Responses API: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`);
214
- const jsonResult = await fetchJson(url, headers, body, logger);
339
+ const jsonResult = await fetchJson(url, headers, body, logger, signal);
215
340
  if (jsonResult.isFailure()) {
216
341
  return fail(jsonResult.message);
217
342
  }
@@ -256,18 +381,10 @@ function extractAnthropicText(content) {
256
381
  * mixed content block responses.
257
382
  * @internal
258
383
  */
259
- async function callAnthropicCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, tools) {
384
+ async function callAnthropicCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, tools, signal) {
260
385
  const url = `${config.baseUrl}/messages`;
261
386
  // Anthropic uses system as a top-level field, not in messages
262
- const messages = [{ role: 'user', content: prompt.user }];
263
- if (additionalMessages) {
264
- for (const msg of additionalMessages) {
265
- // Anthropic doesn't have a system role in messages
266
- if (msg.role !== 'system') {
267
- messages.push({ role: msg.role, content: msg.content });
268
- }
269
- }
270
- }
387
+ const messages = buildAnthropicMessages(prompt, { tail: additionalMessages });
271
388
  const body = {
272
389
  model: config.model,
273
390
  system: prompt.system,
@@ -289,7 +406,7 @@ async function callAnthropicCompletion(config, prompt, additionalMessages, tempe
289
406
  'anthropic-version': '2023-06-01',
290
407
  'anthropic-dangerous-direct-browser-access': 'true'
291
408
  };
292
- const jsonResult = await fetchJson(url, headers, body, logger);
409
+ const jsonResult = await fetchJson(url, headers, body, logger, signal);
293
410
  if (jsonResult.isFailure()) {
294
411
  return fail(jsonResult.message);
295
412
  }
@@ -324,22 +441,10 @@ async function callAnthropicCompletion(config, prompt, additionalMessages, tempe
324
441
  * When tools are configured, includes Google Search grounding.
325
442
  * @internal
326
443
  */
327
- async function callGeminiCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, tools) {
444
+ async function callGeminiCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, tools, signal) {
328
445
  const url = `${config.baseUrl}/models/${config.model}:generateContent`;
329
446
  // Gemini uses 'contents' with 'parts', and 'model' role instead of 'assistant'
330
- const contents = [
331
- { role: 'user', parts: [{ text: prompt.user }] }
332
- ];
333
- if (additionalMessages) {
334
- for (const msg of additionalMessages) {
335
- if (msg.role !== 'system') {
336
- contents.push({
337
- role: msg.role === 'assistant' ? 'model' : msg.role,
338
- parts: [{ text: msg.content }]
339
- });
340
- }
341
- }
342
- }
447
+ const contents = buildGeminiContents(prompt, { tail: additionalMessages });
343
448
  const body = {
344
449
  systemInstruction: { parts: [{ text: prompt.system }] },
345
450
  contents,
@@ -357,7 +462,7 @@ async function callGeminiCompletion(config, prompt, additionalMessages, temperat
357
462
  const headers = {
358
463
  'x-goog-api-key': config.apiKey
359
464
  };
360
- const jsonResult = await fetchJson(url, headers, body, logger);
465
+ const jsonResult = await fetchJson(url, headers, body, logger, signal);
361
466
  if (jsonResult.isFailure()) {
362
467
  return fail(jsonResult.message);
363
468
  }
@@ -393,16 +498,24 @@ async function callGeminiCompletion(config, prompt, additionalMessages, temperat
393
498
  * @public
394
499
  */
395
500
  export async function callProviderCompletion(params) {
396
- const { descriptor, apiKey, prompt, additionalMessages, temperature = 0.7, modelOverride, logger, tools } = params;
397
- if (!descriptor.baseUrl) {
398
- return fail(`provider "${descriptor.id}" has no API endpoint configured`);
501
+ const { descriptor, apiKey, prompt, additionalMessages, temperature = 0.7, modelOverride, logger, tools, signal, endpoint } = params;
502
+ const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);
503
+ if (baseUrlResult.isFailure()) {
504
+ return fail(baseUrlResult.message);
505
+ }
506
+ if (prompt.attachments.length > 0 && !descriptor.acceptsImageInput) {
507
+ return fail(`provider "${descriptor.id}" does not accept image input`);
399
508
  }
400
509
  const hasTools = tools !== undefined && tools.length > 0;
401
510
  const modelContext = hasTools ? 'tools' : undefined;
511
+ const model = resolveModel(modelOverride !== null && modelOverride !== void 0 ? modelOverride : descriptor.defaultModel, modelContext);
512
+ if (model.length === 0) {
513
+ return fail(`provider "${descriptor.id}": no model resolved; pass modelOverride or set descriptor.defaultModel`);
514
+ }
402
515
  const config = {
403
- baseUrl: descriptor.baseUrl,
516
+ baseUrl: baseUrlResult.value,
404
517
  apiKey,
405
- model: resolveModel(modelOverride !== null && modelOverride !== void 0 ? modelOverride : descriptor.defaultModel, modelContext)
518
+ model
406
519
  };
407
520
  /* c8 ignore next 8 - optional logger diagnostic output */
408
521
  if (logger) {
@@ -414,13 +527,13 @@ export async function callProviderCompletion(params) {
414
527
  switch (descriptor.apiFormat) {
415
528
  case 'openai':
416
529
  if (hasTools) {
417
- return callOpenAiResponsesCompletion(config, prompt, tools, additionalMessages, temperature, logger);
530
+ return callOpenAiResponsesCompletion(config, prompt, tools, additionalMessages, temperature, logger, signal);
418
531
  }
419
- return callOpenAiCompletion(config, prompt, additionalMessages, temperature, logger);
532
+ return callOpenAiCompletion(config, prompt, additionalMessages, temperature, logger, signal);
420
533
  case 'anthropic':
421
- return callAnthropicCompletion(config, prompt, additionalMessages, temperature, logger, tools);
534
+ return callAnthropicCompletion(config, prompt, additionalMessages, temperature, logger, tools, signal);
422
535
  case 'gemini':
423
- return callGeminiCompletion(config, prompt, additionalMessages, temperature, logger, tools);
536
+ return callGeminiCompletion(config, prompt, additionalMessages, temperature, logger, tools, signal);
424
537
  /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
425
538
  default: {
426
539
  const _exhaustive = descriptor.apiFormat;
@@ -428,6 +541,579 @@ export async function callProviderCompletion(params) {
428
541
  }
429
542
  }
430
543
  }
544
+ const openAiImageItem = Validators.object({
545
+ b64_json: Validators.string,
546
+ revised_prompt: Validators.string.optional()
547
+ });
548
+ const openAiImageResponse = Validators.object({
549
+ data: Validators.arrayOf(openAiImageItem).withConstraint((arr) => arr.length > 0)
550
+ });
551
+ const imagenPrediction = Validators.object({
552
+ bytesBase64Encoded: Validators.string,
553
+ mimeType: Validators.string.optional()
554
+ });
555
+ const imagenResponse = Validators.object({
556
+ predictions: Validators.arrayOf(imagenPrediction).withConstraint((arr) => arr.length > 0)
557
+ });
558
+ const geminiImageInlineData = Validators.object({
559
+ mimeType: Validators.string,
560
+ data: Validators.string
561
+ });
562
+ const geminiImageOutPart = Validators.object({
563
+ text: Validators.string.optional(),
564
+ inlineData: geminiImageInlineData.optional()
565
+ });
566
+ const geminiImageOutContent = Validators.object({
567
+ parts: Validators.arrayOf(geminiImageOutPart).withConstraint((arr) => arr.length > 0)
568
+ });
569
+ const geminiImageOutCandidate = Validators.object({
570
+ content: geminiImageOutContent,
571
+ finishReason: Validators.string.optional()
572
+ });
573
+ const geminiImageOutResponse = Validators.object({
574
+ candidates: Validators.arrayOf(geminiImageOutCandidate).withConstraint((arr) => arr.length > 0)
575
+ });
576
+ // ---- Proxied image generation response ----
577
+ const proxiedGeneratedImage = Validators.object({
578
+ mimeType: Validators.string,
579
+ base64: Validators.string,
580
+ revisedPrompt: Validators.string.optional()
581
+ });
582
+ const proxiedImageGenerationResponse = Validators.object({
583
+ images: Validators.arrayOf(proxiedGeneratedImage).withConstraint((arr) => arr.length > 0)
584
+ });
585
+ const proxiedListModelsEntry = Validators.object({
586
+ id: Validators.string,
587
+ capabilities: Validators.arrayOf(Validators.enumeratedValue(['chat', 'tools', 'vision', 'image-generation'])),
588
+ displayName: Validators.string.optional()
589
+ });
590
+ const proxiedListModelsResponse = Validators.object({
591
+ models: Validators.arrayOf(proxiedListModelsEntry)
592
+ });
593
+ // ============================================================================
594
+ // Image generation — adapters
595
+ // ============================================================================
596
+ /**
597
+ * Calls the OpenAI Images API. Used for both `openai-images` and `xai-images`
598
+ * formats — the request shape is the same; the only difference is whether the
599
+ * `size` field is honored (OpenAI: yes, xAI: ignored at the provider).
600
+ *
601
+ * When `request.referenceImages` is non-empty, routes to `/images/edits`
602
+ * (multipart) instead of `/images/generations` (JSON). Per-model edit support
603
+ * is not validated here (e.g. dall-e-3 does not support edits) — the
604
+ * provider's 400 surfaces through the failure path.
605
+ *
606
+ * @internal
607
+ */
608
+ async function callOpenAiImageGeneration(config, request, defaultMimeType, logger, signal) {
609
+ var _a, _b, _c;
610
+ const opts = (_a = request.options) !== null && _a !== void 0 ? _a : {};
611
+ const refs = (_b = request.referenceImages) !== null && _b !== void 0 ? _b : [];
612
+ const headers = bearerAuthHeader(config.apiKey);
613
+ const n = (_c = opts.count) !== null && _c !== void 0 ? _c : 1;
614
+ const fetched = refs.length > 0
615
+ ? await callOpenAiImagesEdits(config, request, headers, n, refs, logger, signal)
616
+ : await callOpenAiImagesGenerations(config, request, headers, n, logger, signal);
617
+ return fetched.onSuccess((json) => openAiImageResponse
618
+ .validate(json)
619
+ .withErrorFormat((msg) => `OpenAI images API response: ${msg}`)
620
+ .onSuccess((response) => succeed({
621
+ images: response.data.map((item) => (Object.assign({ mimeType: defaultMimeType, base64: item.b64_json }, (item.revised_prompt !== undefined ? { revisedPrompt: item.revised_prompt } : {}))))
622
+ })));
623
+ }
624
+ /**
625
+ * Builds and posts the JSON `/images/generations` request (no refs).
626
+ * @internal
627
+ */
628
+ function callOpenAiImagesGenerations(config, request, headers, n, logger, signal) {
629
+ var _a;
630
+ const opts = (_a = request.options) !== null && _a !== void 0 ? _a : {};
631
+ const body = {
632
+ model: config.model,
633
+ prompt: request.prompt,
634
+ n,
635
+ response_format: 'b64_json'
636
+ };
637
+ if (opts.size !== undefined) {
638
+ body.size = opts.size;
639
+ }
640
+ if (opts.quality !== undefined) {
641
+ body.quality = opts.quality;
642
+ }
643
+ if (opts.seed !== undefined) {
644
+ body.seed = opts.seed;
645
+ }
646
+ /* c8 ignore next 1 - optional logger */
647
+ logger === null || logger === void 0 ? void 0 : logger.info(`Image generation: model=${config.model}, n=${n}`);
648
+ return fetchJson(`${config.baseUrl}/images/generations`, headers, body, logger, signal);
649
+ }
650
+ /**
651
+ * Builds and posts the multipart `/images/edits` request (with refs).
652
+ * @internal
653
+ */
654
+ async function callOpenAiImagesEdits(config, request, headers, n, refs, logger, signal) {
655
+ var _a;
656
+ const blobsResult = mapResults(refs.map((ref, i) => attachmentToBlob(ref).withErrorFormat((msg) => `reference image ${i}: ${msg}`)));
657
+ /* c8 ignore next 3 - decode failure unreachable via Node's Buffer.from (silently strips invalid input) */
658
+ if (blobsResult.isFailure()) {
659
+ return fail(blobsResult.message);
660
+ }
661
+ const opts = (_a = request.options) !== null && _a !== void 0 ? _a : {};
662
+ const form = new FormData();
663
+ form.append('model', config.model);
664
+ form.append('prompt', request.prompt);
665
+ form.append('n', String(n));
666
+ form.append('response_format', 'b64_json');
667
+ if (opts.size !== undefined) {
668
+ form.append('size', opts.size);
669
+ }
670
+ if (opts.quality !== undefined) {
671
+ form.append('quality', opts.quality);
672
+ }
673
+ if (opts.seed !== undefined) {
674
+ form.append('seed', String(opts.seed));
675
+ }
676
+ blobsResult.value.forEach((blob, i) => {
677
+ form.append('image[]', blob, `ref-${i}.${extensionForMimeType(refs[i].mimeType)}`);
678
+ });
679
+ /* c8 ignore next 1 - optional logger */
680
+ logger === null || logger === void 0 ? void 0 : logger.info(`Image edit: model=${config.model}, n=${n}, refs=${refs.length}`);
681
+ return fetchMultipart(`${config.baseUrl}/images/edits`, headers, form, logger, signal);
682
+ }
683
+ /**
684
+ * Calls Gemini's chat-style `:generateContent` endpoint for image output
685
+ * (Gemini 2.5 Flash Image / "Nano Banana"). Accepts reference images, which
686
+ * are passed as `inlineData` parts alongside the text prompt.
687
+ *
688
+ * @internal
689
+ */
690
+ async function callGeminiImageOutGeneration(config, request, logger, signal) {
691
+ var _a;
692
+ const url = `${config.baseUrl}/models/${config.model}:generateContent`;
693
+ const refs = (_a = request.referenceImages) !== null && _a !== void 0 ? _a : [];
694
+ const parts = [{ text: request.prompt }];
695
+ for (const ref of refs) {
696
+ parts.push({ inlineData: { mimeType: ref.mimeType, data: ref.base64 } });
697
+ }
698
+ const body = {
699
+ contents: [{ role: 'user', parts }]
700
+ };
701
+ const headers = {
702
+ 'x-goog-api-key': config.apiKey
703
+ };
704
+ /* c8 ignore next 1 - optional logger */
705
+ logger === null || logger === void 0 ? void 0 : logger.info(`Gemini image-out: model=${config.model}, refs=${refs.length}`);
706
+ return (await fetchJson(url, headers, body, logger, signal)).onSuccess((json) => geminiImageOutResponse
707
+ .validate(json)
708
+ .withErrorFormat((msg) => `Gemini image API response: ${msg}`)
709
+ .onSuccess((response) => {
710
+ const images = [];
711
+ for (const candidate of response.candidates) {
712
+ for (const part of candidate.content.parts) {
713
+ if (part.inlineData) {
714
+ images.push({
715
+ mimeType: part.inlineData.mimeType,
716
+ base64: part.inlineData.data
717
+ });
718
+ }
719
+ }
720
+ }
721
+ if (images.length === 0) {
722
+ return fail('Gemini image API response: no image parts in response');
723
+ }
724
+ return succeed({ images });
725
+ }));
726
+ }
727
+ /**
728
+ * Calls the Gemini Imagen `:predict` endpoint.
729
+ * @internal
730
+ */
731
+ async function callImagenGeneration(config, request, logger, signal) {
732
+ var _a, _b, _c, _d;
733
+ const url = `${config.baseUrl}/models/${config.model}:predict`;
734
+ const opts = (_a = request.options) !== null && _a !== void 0 ? _a : {};
735
+ const parameters = {
736
+ sampleCount: (_b = opts.count) !== null && _b !== void 0 ? _b : 1
737
+ };
738
+ if (((_c = opts.imagen) === null || _c === void 0 ? void 0 : _c.aspectRatio) !== undefined) {
739
+ parameters.aspectRatio = opts.imagen.aspectRatio;
740
+ }
741
+ if (((_d = opts.imagen) === null || _d === void 0 ? void 0 : _d.negativePrompt) !== undefined) {
742
+ parameters.negativePrompt = opts.imagen.negativePrompt;
743
+ }
744
+ if (opts.seed !== undefined) {
745
+ parameters.seed = opts.seed;
746
+ }
747
+ const body = {
748
+ instances: [{ prompt: request.prompt }],
749
+ parameters
750
+ };
751
+ const headers = {
752
+ 'x-goog-api-key': config.apiKey
753
+ };
754
+ /* c8 ignore next 1 - optional logger */
755
+ logger === null || logger === void 0 ? void 0 : logger.info(`Imagen generation: model=${config.model}, n=${parameters.sampleCount}`);
756
+ const jsonResult = await fetchJson(url, headers, body, logger, signal);
757
+ if (jsonResult.isFailure()) {
758
+ return fail(jsonResult.message);
759
+ }
760
+ return imagenResponse
761
+ .validate(jsonResult.value)
762
+ .withErrorFormat((msg) => `Imagen API response: ${msg}`)
763
+ .onSuccess((response) => {
764
+ const images = response.predictions.map((p) => {
765
+ var _a;
766
+ return ({
767
+ mimeType: (_a = p.mimeType) !== null && _a !== void 0 ? _a : 'image/png',
768
+ base64: p.bytesBase64Encoded
769
+ });
770
+ });
771
+ return succeed({ images });
772
+ });
773
+ }
774
+ // ============================================================================
775
+ // Image generation — dispatcher
776
+ // ============================================================================
777
+ /**
778
+ * Calls the appropriate image-generation API for a given provider.
779
+ *
780
+ * Resolves a {@link IAiImageModelCapability} from
781
+ * {@link IAiProviderDescriptor.imageGeneration} for the requested model and
782
+ * routes by its `format`:
783
+ * - `'openai-images'` for OpenAI (DALL-E, gpt-image-1)
784
+ * - `'xai-images'` for xAI Grok image models
785
+ * - `'gemini-imagen'` for Google Imagen `:predict`
786
+ * - `'gemini-image-out'` for Gemini chat-style image output (Nano Banana)
787
+ *
788
+ * Image-model selection reuses the existing `'image'` {@link ModelSpecKey}.
789
+ * When `request.referenceImages` is non-empty, the call is rejected up front
790
+ * unless the resolved capability declares `acceptsImageReferenceInput`.
791
+ *
792
+ * @param params - Request parameters including descriptor, API key, and prompt
793
+ * @returns The generated images, or a failure
794
+ * @public
795
+ */
796
+ export async function callProviderImageGeneration(params) {
797
+ var _a, _b;
798
+ const { descriptor, apiKey, params: request, modelOverride, logger, signal, endpoint } = params;
799
+ if (!supportsImageGeneration(descriptor)) {
800
+ return fail(`provider "${descriptor.id}" does not support image generation`);
801
+ }
802
+ const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);
803
+ if (baseUrlResult.isFailure()) {
804
+ return fail(baseUrlResult.message);
805
+ }
806
+ const model = resolveModel(modelOverride !== null && modelOverride !== void 0 ? modelOverride : descriptor.defaultModel, 'image');
807
+ if (model.length === 0) {
808
+ return fail(`provider "${descriptor.id}": no image model resolved; ` +
809
+ `pass modelOverride or set descriptor.defaultModel ` +
810
+ `(a plain string, or an object with an "image" entry)`);
811
+ }
812
+ const capability = resolveImageCapability(descriptor, model);
813
+ if (capability === undefined) {
814
+ return fail(`provider "${descriptor.id}" does not support image generation for model "${model}"`);
815
+ }
816
+ if (((_b = (_a = request.referenceImages) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0 && !capability.acceptsImageReferenceInput) {
817
+ return fail(`model "${model}" does not support reference images`);
818
+ }
819
+ const config = {
820
+ baseUrl: baseUrlResult.value,
821
+ apiKey,
822
+ model
823
+ };
824
+ /* c8 ignore next 6 - optional logger diagnostic output */
825
+ if (logger) {
826
+ logger.info(`AI image generation: provider=${descriptor.id}, format=${capability.format}, ` +
827
+ `model=${config.model}`);
828
+ }
829
+ switch (capability.format) {
830
+ case 'openai-images':
831
+ return callOpenAiImageGeneration(config, request, 'image/png', logger, signal);
832
+ case 'xai-images':
833
+ return callOpenAiImageGeneration(config, request, 'image/jpeg', logger, signal);
834
+ case 'gemini-imagen':
835
+ return callImagenGeneration(config, request, logger, signal);
836
+ case 'gemini-image-out':
837
+ return callGeminiImageOutGeneration(config, request, logger, signal);
838
+ /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
839
+ default: {
840
+ const _exhaustive = capability.format;
841
+ return fail(`unsupported image API format: ${String(_exhaustive)}`);
842
+ }
843
+ }
844
+ }
845
+ const openAiListEntry = Validators.object({
846
+ id: Validators.string
847
+ });
848
+ const openAiListResponse = Validators.object({
849
+ data: Validators.arrayOf(openAiListEntry)
850
+ });
851
+ const anthropicListEntry = Validators.object({
852
+ id: Validators.string,
853
+ display_name: Validators.string.optional()
854
+ });
855
+ const anthropicListResponse = Validators.object({
856
+ data: Validators.arrayOf(anthropicListEntry)
857
+ });
858
+ const geminiListEntry = Validators.object({
859
+ name: Validators.string,
860
+ displayName: Validators.string.optional(),
861
+ supportedGenerationMethods: Validators.arrayOf(Validators.string).optional()
862
+ });
863
+ const geminiListResponse = Validators.object({
864
+ models: Validators.arrayOf(geminiListEntry)
865
+ });
866
+ // ============================================================================
867
+ // List models — capability resolution
868
+ // ============================================================================
869
+ /**
870
+ * Translates Gemini's `supportedGenerationMethods` strings into our abstract
871
+ * capability vocabulary. Methods without a mapping are ignored.
872
+ * @internal
873
+ */
874
+ function geminiMethodsToCapabilities(methods) {
875
+ const out = [];
876
+ for (const m of methods) {
877
+ if (m === 'generateContent') {
878
+ out.push('chat');
879
+ }
880
+ else if (m === 'predict') {
881
+ out.push('image-generation');
882
+ }
883
+ }
884
+ return out;
885
+ }
886
+ /**
887
+ * Strips the `models/` prefix Gemini includes on listed model names.
888
+ * @internal
889
+ */
890
+ function geminiBareId(name) {
891
+ return name.startsWith('models/') ? name.substring('models/'.length) : name;
892
+ }
893
+ /**
894
+ * Applies a capability config to a model id. Walks per-provider rules then
895
+ * global rules; unions all matching rules' capabilities. Returns the union
896
+ * and the first matching `displayName` (if any).
897
+ * @internal
898
+ */
899
+ function applyCapabilityConfig(config, providerId, modelId) {
900
+ var _a, _b, _c;
901
+ const caps = new Set();
902
+ let displayName;
903
+ const rulesets = [
904
+ (_b = (_a = config.perProvider) === null || _a === void 0 ? void 0 : _a[providerId]) !== null && _b !== void 0 ? _b : [],
905
+ (_c = config.global) !== null && _c !== void 0 ? _c : []
906
+ ];
907
+ for (const rules of rulesets) {
908
+ for (const rule of rules) {
909
+ rule.idPattern.lastIndex = 0;
910
+ if (rule.idPattern.test(modelId)) {
911
+ for (const cap of rule.capabilities) {
912
+ caps.add(cap);
913
+ }
914
+ if (displayName === undefined && rule.displayName !== undefined) {
915
+ displayName = typeof rule.displayName === 'function' ? rule.displayName(modelId) : rule.displayName;
916
+ }
917
+ }
918
+ }
919
+ }
920
+ return { capabilities: Array.from(caps), displayName };
921
+ }
922
+ /**
923
+ * Combines provider-native capability info (when supplied) and config-derived
924
+ * capability info into a final {@link IAiModelInfo}.
925
+ * @internal
926
+ */
927
+ function buildModelInfo(providerId, id, nativeCapabilities, nativeDisplayName, config) {
928
+ const fromConfig = applyCapabilityConfig(config, providerId, id);
929
+ const all = new Set([...nativeCapabilities, ...fromConfig.capabilities]);
930
+ return Object.assign({ id, capabilities: all }, (nativeDisplayName !== undefined
931
+ ? { displayName: nativeDisplayName }
932
+ : fromConfig.displayName !== undefined
933
+ ? { displayName: fromConfig.displayName }
934
+ : {}));
935
+ }
936
+ // ============================================================================
937
+ // List models — adapters
938
+ // ============================================================================
939
+ /**
940
+ * Calls the OpenAI-style `GET /models` endpoint. Used by openai, xai-grok,
941
+ * groq, and mistral. Provider supplies no capability info — capabilities are
942
+ * derived entirely from the config.
943
+ * @internal
944
+ */
945
+ async function callOpenAiListModels(config, providerId, capabilityConfig, logger, signal) {
946
+ const url = `${config.baseUrl}/models`;
947
+ const headers = bearerAuthHeader(config.apiKey);
948
+ /* c8 ignore next 1 - optional logger */
949
+ logger === null || logger === void 0 ? void 0 : logger.info(`List models: provider=${providerId}, format=openai`);
950
+ const jsonResult = await fetchGetJson(url, headers, logger, signal);
951
+ if (jsonResult.isFailure()) {
952
+ return fail(jsonResult.message);
953
+ }
954
+ return openAiListResponse
955
+ .validate(jsonResult.value)
956
+ .withErrorFormat((msg) => `OpenAI models API response: ${msg}`)
957
+ .onSuccess((response) => {
958
+ const models = response.data.map((entry) => buildModelInfo(providerId, entry.id, [], undefined, capabilityConfig));
959
+ return succeed(models);
960
+ });
961
+ }
962
+ /**
963
+ * Calls the Anthropic `GET /models` endpoint. Provider supplies a
964
+ * `display_name` but no native capability info.
965
+ * @internal
966
+ */
967
+ async function callAnthropicListModels(config, providerId, capabilityConfig, logger, signal) {
968
+ const url = `${config.baseUrl}/models`;
969
+ const headers = {
970
+ 'x-api-key': config.apiKey,
971
+ 'anthropic-version': '2023-06-01',
972
+ 'anthropic-dangerous-direct-browser-access': 'true'
973
+ };
974
+ /* c8 ignore next 1 - optional logger */
975
+ logger === null || logger === void 0 ? void 0 : logger.info(`List models: provider=${providerId}, format=anthropic`);
976
+ const jsonResult = await fetchGetJson(url, headers, logger, signal);
977
+ if (jsonResult.isFailure()) {
978
+ return fail(jsonResult.message);
979
+ }
980
+ return anthropicListResponse
981
+ .validate(jsonResult.value)
982
+ .withErrorFormat((msg) => `Anthropic models API response: ${msg}`)
983
+ .onSuccess((response) => {
984
+ const models = response.data.map((entry) => buildModelInfo(providerId, entry.id, [], entry.display_name, capabilityConfig));
985
+ return succeed(models);
986
+ });
987
+ }
988
+ /**
989
+ * Calls the Gemini `GET /models` endpoint. Provider supplies both a
990
+ * `displayName` and `supportedGenerationMethods` — translated to native
991
+ * capabilities and unioned with config-derived capabilities.
992
+ * @internal
993
+ */
994
+ async function callGeminiListModels(config, providerId, capabilityConfig, logger, signal) {
995
+ const url = `${config.baseUrl}/models`;
996
+ const headers = {
997
+ 'x-goog-api-key': config.apiKey
998
+ };
999
+ /* c8 ignore next 1 - optional logger */
1000
+ logger === null || logger === void 0 ? void 0 : logger.info(`List models: provider=${providerId}, format=gemini`);
1001
+ const jsonResult = await fetchGetJson(url, headers, logger, signal);
1002
+ if (jsonResult.isFailure()) {
1003
+ return fail(jsonResult.message);
1004
+ }
1005
+ return geminiListResponse
1006
+ .validate(jsonResult.value)
1007
+ .withErrorFormat((msg) => `Gemini models API response: ${msg}`)
1008
+ .onSuccess((response) => {
1009
+ const models = response.models.map((entry) => {
1010
+ const id = geminiBareId(entry.name);
1011
+ const native = entry.supportedGenerationMethods
1012
+ ? geminiMethodsToCapabilities(entry.supportedGenerationMethods)
1013
+ : [];
1014
+ return buildModelInfo(providerId, id, native, entry.displayName, capabilityConfig);
1015
+ });
1016
+ return succeed(models);
1017
+ });
1018
+ }
1019
+ // ============================================================================
1020
+ // List models — dispatcher
1021
+ // ============================================================================
1022
+ /**
1023
+ * Lists models available from a provider, with capabilities resolved from
1024
+ * native provider info (where supplied) and a configurable rule set.
1025
+ *
1026
+ * Routes based on `descriptor.apiFormat` — listing reuses the existing
1027
+ * format dispatch and does not require a separate descriptor field.
1028
+ *
1029
+ * @param params - Request parameters including descriptor, API key, and optional capability filter
1030
+ * @returns The resolved model list, or a failure
1031
+ * @public
1032
+ */
1033
+ export async function callProviderListModels(params) {
1034
+ const { descriptor, apiKey, capability, capabilityConfig, logger, signal, endpoint } = params;
1035
+ const baseUrlResult = resolveEffectiveBaseUrl(descriptor, endpoint);
1036
+ if (baseUrlResult.isFailure()) {
1037
+ return fail(baseUrlResult.message);
1038
+ }
1039
+ const config = {
1040
+ baseUrl: baseUrlResult.value,
1041
+ apiKey,
1042
+ model: '' // unused by listing
1043
+ };
1044
+ const effectiveConfig = capabilityConfig !== null && capabilityConfig !== void 0 ? capabilityConfig : DEFAULT_MODEL_CAPABILITY_CONFIG;
1045
+ let listResult;
1046
+ switch (descriptor.apiFormat) {
1047
+ case 'openai':
1048
+ listResult = await callOpenAiListModels(config, descriptor.id, effectiveConfig, logger, signal);
1049
+ break;
1050
+ case 'anthropic':
1051
+ listResult = await callAnthropicListModels(config, descriptor.id, effectiveConfig, logger, signal);
1052
+ break;
1053
+ case 'gemini':
1054
+ listResult = await callGeminiListModels(config, descriptor.id, effectiveConfig, logger, signal);
1055
+ break;
1056
+ /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
1057
+ default: {
1058
+ const _exhaustive = descriptor.apiFormat;
1059
+ return fail(`unsupported API format: ${String(_exhaustive)}`);
1060
+ }
1061
+ }
1062
+ if (listResult.isFailure()) {
1063
+ return listResult;
1064
+ }
1065
+ if (capability === undefined) {
1066
+ return listResult;
1067
+ }
1068
+ return succeed(listResult.value.filter((m) => m.capabilities.has(capability)));
1069
+ }
1070
+ // ============================================================================
1071
+ // Proxied list models
1072
+ // ============================================================================
1073
+ /**
1074
+ * Calls the model-listing endpoint on a proxy server.
1075
+ *
1076
+ * @remarks
1077
+ * Proxy contract:
1078
+ * - Endpoint: `POST ${proxyUrl}/api/ai/list-models`
1079
+ * - Request body: `{providerId, apiKey, capability?}`. Capability config is
1080
+ * not forwarded — the proxy applies its own (typically the same default
1081
+ * the library ships).
1082
+ * - Success response body: an `IAiModelInfo[]` (under key `models`) where
1083
+ * `capabilities` is serialized as a string array (not Set, which doesn't
1084
+ * round-trip through JSON).
1085
+ * - Error response body: `{error: string}`, surfaced as `proxy: ${error}`.
1086
+ *
1087
+ * @public
1088
+ */
1089
+ export async function callProxiedListModels(proxyUrl, params) {
1090
+ const { descriptor, apiKey, capability, logger, signal } = params;
1091
+ const body = {
1092
+ providerId: descriptor.id,
1093
+ apiKey
1094
+ };
1095
+ if (capability !== undefined) {
1096
+ body.capability = capability;
1097
+ }
1098
+ /* c8 ignore next 1 - optional logger */
1099
+ logger === null || logger === void 0 ? void 0 : logger.info(`AI list-models proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
1100
+ const url = `${proxyUrl}/api/ai/list-models`;
1101
+ const jsonResult = await fetchJson(url, {}, body, logger, signal);
1102
+ if (jsonResult.isFailure()) {
1103
+ return fail(jsonResult.message);
1104
+ }
1105
+ const response = jsonResult.value;
1106
+ if (typeof response.error === 'string') {
1107
+ return fail(`proxy: ${response.error}`);
1108
+ }
1109
+ return proxiedListModelsResponse
1110
+ .validate(response)
1111
+ .withErrorFormat((msg) => `proxy returned invalid response: ${msg}`)
1112
+ .onSuccess((parsed) => {
1113
+ const models = parsed.models.map((m) => (Object.assign({ id: m.id, capabilities: new Set(m.capabilities) }, (m.displayName !== undefined ? { displayName: m.displayName } : {}))));
1114
+ return succeed(models);
1115
+ });
1116
+ }
431
1117
  // ============================================================================
432
1118
  // Proxied completion (routes through a backend server)
433
1119
  // ============================================================================
@@ -445,11 +1131,15 @@ export async function callProviderCompletion(params) {
445
1131
  * @public
446
1132
  */
447
1133
  export async function callProxiedCompletion(proxyUrl, params) {
448
- const { descriptor, apiKey, prompt, additionalMessages, temperature, modelOverride, logger, tools } = params;
1134
+ const { descriptor, apiKey, prompt, additionalMessages, temperature, modelOverride, logger, tools, signal } = params;
1135
+ const promptBody = { system: prompt.system, user: prompt.user };
1136
+ if (prompt.attachments.length > 0) {
1137
+ promptBody.attachments = prompt.attachments;
1138
+ }
449
1139
  const body = {
450
1140
  providerId: descriptor.id,
451
1141
  apiKey,
452
- prompt: { system: prompt.system, user: prompt.user },
1142
+ prompt: promptBody,
453
1143
  temperature: temperature !== null && temperature !== void 0 ? temperature : 0.7
454
1144
  };
455
1145
  if (additionalMessages && additionalMessages.length > 0) {
@@ -464,7 +1154,7 @@ export async function callProxiedCompletion(proxyUrl, params) {
464
1154
  /* c8 ignore next 1 - optional logger */
465
1155
  logger === null || logger === void 0 ? void 0 : logger.info(`AI proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
466
1156
  const url = `${proxyUrl}/api/ai/completion`;
467
- const jsonResult = await fetchJson(url, {}, body, logger);
1157
+ const jsonResult = await fetchJson(url, {}, body, logger, signal);
468
1158
  if (jsonResult.isFailure()) {
469
1159
  return fail(jsonResult.message);
470
1160
  }
@@ -481,4 +1171,54 @@ export async function callProxiedCompletion(proxyUrl, params) {
481
1171
  truncated: response.truncated === true
482
1172
  });
483
1173
  }
1174
+ // ============================================================================
1175
+ // Proxied image generation
1176
+ // ============================================================================
1177
+ /**
1178
+ * Calls the image-generation endpoint on a proxy server instead of calling
1179
+ * the provider API directly from the browser.
1180
+ *
1181
+ * @remarks
1182
+ * The proxy contract:
1183
+ * - Endpoint: `POST ${proxyUrl}/api/ai/image-generation`
1184
+ * - Request body: `{providerId, apiKey, params, modelOverride?}`
1185
+ * - Success response body: an {@link IAiImageGenerationResponse}
1186
+ * - Error response body: `{error: string}` (surfaced as `proxy: ${error}`)
1187
+ *
1188
+ * The proxy server is responsible for descriptor lookup, model resolution,
1189
+ * provider dispatch, and response normalization. When `params.referenceImages`
1190
+ * is present, the proxy is also responsible for repackaging it into the
1191
+ * upstream wire format (e.g. multipart/form-data for OpenAI `/images/edits`,
1192
+ * `inlineData` parts for Gemini `:generateContent`).
1193
+ *
1194
+ * @param proxyUrl - Base URL of the proxy server (e.g. `http://localhost:3001`)
1195
+ * @param params - Same parameters as {@link callProviderImageGeneration}
1196
+ * @returns The generated images, or a failure
1197
+ * @public
1198
+ */
1199
+ export async function callProxiedImageGeneration(proxyUrl, params) {
1200
+ const { descriptor, apiKey, params: request, modelOverride, logger, signal } = params;
1201
+ const body = {
1202
+ providerId: descriptor.id,
1203
+ apiKey,
1204
+ params: request
1205
+ };
1206
+ if (modelOverride !== undefined) {
1207
+ body.modelOverride = modelOverride;
1208
+ }
1209
+ /* c8 ignore next 1 - optional logger */
1210
+ logger === null || logger === void 0 ? void 0 : logger.info(`AI image proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
1211
+ const url = `${proxyUrl}/api/ai/image-generation`;
1212
+ const jsonResult = await fetchJson(url, {}, body, logger, signal);
1213
+ if (jsonResult.isFailure()) {
1214
+ return fail(jsonResult.message);
1215
+ }
1216
+ const response = jsonResult.value;
1217
+ if (typeof response.error === 'string') {
1218
+ return fail(`proxy: ${response.error}`);
1219
+ }
1220
+ return proxiedImageGenerationResponse
1221
+ .validate(response)
1222
+ .withErrorFormat((msg) => `proxy returned invalid response: ${msg}`);
1223
+ }
484
1224
  //# sourceMappingURL=apiClient.js.map