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

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