@fgv/ts-extras 5.0.2 → 5.1.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/index.browser.js +6 -2
  2. package/dist/index.js +5 -1
  3. package/dist/packlets/ai-assist/apiClient.js +484 -0
  4. package/dist/packlets/ai-assist/converters.js +121 -0
  5. package/dist/packlets/ai-assist/index.js +10 -0
  6. package/dist/packlets/ai-assist/model.js +90 -0
  7. package/dist/packlets/ai-assist/registry.js +145 -0
  8. package/dist/packlets/ai-assist/toolFormats.js +160 -0
  9. package/dist/packlets/crypto-utils/constants.js +48 -0
  10. package/dist/packlets/crypto-utils/converters.js +155 -0
  11. package/dist/packlets/crypto-utils/directEncryptionProvider.js +86 -0
  12. package/dist/packlets/crypto-utils/encryptedFile.js +161 -0
  13. package/dist/packlets/crypto-utils/index.browser.js +41 -0
  14. package/dist/packlets/crypto-utils/index.js +41 -0
  15. package/dist/packlets/crypto-utils/keystore/converters.js +84 -0
  16. package/dist/packlets/crypto-utils/keystore/index.js +31 -0
  17. package/dist/packlets/crypto-utils/keystore/keyStore.js +758 -0
  18. package/dist/packlets/crypto-utils/keystore/model.js +64 -0
  19. package/dist/packlets/crypto-utils/model.js +39 -0
  20. package/dist/packlets/crypto-utils/nodeCryptoProvider.js +159 -0
  21. package/dist/packlets/experimental/formatter.js +1 -1
  22. package/dist/packlets/mustache/index.js +23 -0
  23. package/dist/packlets/mustache/interfaces.js +25 -0
  24. package/dist/packlets/mustache/mustacheTemplate.js +242 -0
  25. package/dist/packlets/record-jar/recordJarHelpers.js +1 -1
  26. package/dist/packlets/yaml/converters.js +46 -0
  27. package/dist/packlets/yaml/index.js +23 -0
  28. package/dist/packlets/zip-file-tree/index.js +1 -0
  29. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +43 -2
  30. package/dist/packlets/zip-file-tree/zipFileTreeWriter.js +40 -0
  31. package/dist/ts-extras.d.ts +1990 -112
  32. package/dist/tsdoc-metadata.json +1 -1
  33. package/lib/index.browser.d.ts +3 -1
  34. package/lib/index.browser.js +6 -1
  35. package/lib/index.d.ts +5 -1
  36. package/lib/index.js +9 -1
  37. package/lib/packlets/ai-assist/apiClient.d.ts +60 -0
  38. package/lib/packlets/ai-assist/apiClient.js +488 -0
  39. package/lib/packlets/ai-assist/converters.d.ts +55 -0
  40. package/lib/packlets/ai-assist/converters.js +124 -0
  41. package/lib/packlets/ai-assist/index.d.ts +10 -0
  42. package/lib/packlets/ai-assist/index.js +33 -0
  43. package/lib/packlets/ai-assist/model.d.ts +222 -0
  44. package/lib/packlets/ai-assist/model.js +95 -0
  45. package/lib/packlets/ai-assist/registry.d.ts +25 -0
  46. package/lib/packlets/ai-assist/registry.js +150 -0
  47. package/lib/packlets/ai-assist/toolFormats.d.ts +44 -0
  48. package/lib/packlets/ai-assist/toolFormats.js +166 -0
  49. package/lib/packlets/crypto-utils/constants.d.ts +26 -0
  50. package/lib/packlets/crypto-utils/constants.js +51 -0
  51. package/lib/packlets/crypto-utils/converters.d.ts +58 -0
  52. package/lib/packlets/crypto-utils/converters.js +192 -0
  53. package/lib/packlets/crypto-utils/directEncryptionProvider.d.ts +69 -0
  54. package/lib/packlets/crypto-utils/directEncryptionProvider.js +90 -0
  55. package/lib/packlets/crypto-utils/encryptedFile.d.ts +88 -0
  56. package/lib/packlets/crypto-utils/encryptedFile.js +201 -0
  57. package/lib/packlets/crypto-utils/index.browser.d.ts +14 -0
  58. package/lib/packlets/crypto-utils/index.browser.js +91 -0
  59. package/lib/packlets/crypto-utils/index.d.ts +15 -0
  60. package/lib/packlets/crypto-utils/index.js +88 -0
  61. package/lib/packlets/crypto-utils/keystore/converters.d.ts +29 -0
  62. package/lib/packlets/crypto-utils/keystore/converters.js +87 -0
  63. package/lib/packlets/crypto-utils/keystore/index.d.ts +9 -0
  64. package/lib/packlets/crypto-utils/keystore/index.js +71 -0
  65. package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +239 -0
  66. package/lib/packlets/crypto-utils/keystore/keyStore.js +795 -0
  67. package/lib/packlets/crypto-utils/keystore/model.d.ts +245 -0
  68. package/lib/packlets/crypto-utils/keystore/model.js +68 -0
  69. package/lib/packlets/crypto-utils/model.d.ts +236 -0
  70. package/lib/packlets/crypto-utils/model.js +76 -0
  71. package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +62 -0
  72. package/lib/packlets/crypto-utils/nodeCryptoProvider.js +196 -0
  73. package/lib/packlets/experimental/formatter.d.ts +1 -1
  74. package/lib/packlets/experimental/formatter.js +1 -1
  75. package/lib/packlets/mustache/index.d.ts +3 -0
  76. package/lib/packlets/mustache/index.js +27 -0
  77. package/lib/packlets/mustache/interfaces.d.ts +97 -0
  78. package/lib/packlets/mustache/interfaces.js +26 -0
  79. package/lib/packlets/mustache/mustacheTemplate.d.ts +76 -0
  80. package/lib/packlets/mustache/mustacheTemplate.js +249 -0
  81. package/lib/packlets/record-jar/recordJarHelpers.js +1 -1
  82. package/lib/packlets/yaml/converters.d.ts +9 -0
  83. package/lib/packlets/yaml/converters.js +82 -0
  84. package/lib/packlets/yaml/index.d.ts +2 -0
  85. package/lib/packlets/yaml/index.js +39 -0
  86. package/lib/packlets/zip-file-tree/index.d.ts +1 -0
  87. package/lib/packlets/zip-file-tree/index.js +15 -0
  88. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +31 -2
  89. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +42 -1
  90. package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts +27 -0
  91. package/lib/packlets/zip-file-tree/zipFileTreeWriter.js +43 -0
  92. package/package.json +37 -18
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.54.0"
8
+ "packageVersion": "7.57.6"
9
9
  }
10
10
  ]
11
11
  }
@@ -1,8 +1,10 @@
1
+ import * as Crypto from './packlets/crypto-utils/index.browser';
1
2
  import * as Csv from './packlets/csv/index.browser';
2
3
  import * as Experimental from './packlets/experimental';
3
4
  import * as Hash from './packlets/hash/index.browser';
5
+ import * as Mustache from './packlets/mustache';
4
6
  import * as RecordJar from './packlets/record-jar/index.browser';
5
7
  import * as ZipFileTree from './packlets/zip-file-tree';
6
8
  import { Converters } from './packlets/conversion';
7
- export { Converters, Csv, Experimental, Hash, RecordJar, ZipFileTree };
9
+ export { Converters, Crypto, Csv, Experimental, Hash, Mustache, RecordJar, ZipFileTree };
8
10
  //# sourceMappingURL=index.browser.d.ts.map
@@ -54,9 +54,12 @@ var __importStar = (this && this.__importStar) || (function () {
54
54
  };
55
55
  })();
56
56
  Object.defineProperty(exports, "__esModule", { value: true });
57
- exports.ZipFileTree = exports.RecordJar = exports.Hash = exports.Experimental = exports.Csv = exports.Converters = void 0;
57
+ exports.ZipFileTree = exports.RecordJar = exports.Mustache = exports.Hash = exports.Experimental = exports.Csv = exports.Crypto = exports.Converters = void 0;
58
58
  /* c8 ignore start - Browser-specific export used conditionally in package.json */
59
59
  // eslint-disable-next-line @rushstack/packlets/mechanics
60
+ const Crypto = __importStar(require("./packlets/crypto-utils/index.browser"));
61
+ exports.Crypto = Crypto;
62
+ // eslint-disable-next-line @rushstack/packlets/mechanics
60
63
  const Csv = __importStar(require("./packlets/csv/index.browser"));
61
64
  exports.Csv = Csv;
62
65
  const Experimental = __importStar(require("./packlets/experimental"));
@@ -64,6 +67,8 @@ exports.Experimental = Experimental;
64
67
  // eslint-disable-next-line @rushstack/packlets/mechanics
65
68
  const Hash = __importStar(require("./packlets/hash/index.browser"));
66
69
  exports.Hash = Hash;
70
+ const Mustache = __importStar(require("./packlets/mustache"));
71
+ exports.Mustache = Mustache;
67
72
  // eslint-disable-next-line @rushstack/packlets/mechanics
68
73
  const RecordJar = __importStar(require("./packlets/record-jar/index.browser"));
69
74
  exports.RecordJar = RecordJar;
package/lib/index.d.ts CHANGED
@@ -1,8 +1,12 @@
1
+ import * as AiAssist from './packlets/ai-assist';
2
+ import * as CryptoUtils from './packlets/crypto-utils';
1
3
  import * as Csv from './packlets/csv';
2
4
  import * as Experimental from './packlets/experimental';
3
5
  import * as Hash from './packlets/hash';
6
+ import * as Mustache from './packlets/mustache';
4
7
  import * as RecordJar from './packlets/record-jar';
8
+ import * as Yaml from './packlets/yaml';
5
9
  import * as ZipFileTree from './packlets/zip-file-tree';
6
10
  import { Converters } from './packlets/conversion';
7
- export { Converters, Csv, Experimental, Hash, RecordJar, ZipFileTree };
11
+ export { AiAssist, Converters, CryptoUtils, Csv, Experimental, Hash, Mustache, RecordJar, Yaml, ZipFileTree };
8
12
  //# sourceMappingURL=index.d.ts.map
package/lib/index.js CHANGED
@@ -54,15 +54,23 @@ var __importStar = (this && this.__importStar) || (function () {
54
54
  };
55
55
  })();
56
56
  Object.defineProperty(exports, "__esModule", { value: true });
57
- exports.ZipFileTree = exports.RecordJar = exports.Hash = exports.Experimental = exports.Csv = exports.Converters = void 0;
57
+ exports.ZipFileTree = exports.Yaml = exports.RecordJar = exports.Mustache = exports.Hash = exports.Experimental = exports.Csv = exports.CryptoUtils = exports.Converters = exports.AiAssist = void 0;
58
+ const AiAssist = __importStar(require("./packlets/ai-assist"));
59
+ exports.AiAssist = AiAssist;
60
+ const CryptoUtils = __importStar(require("./packlets/crypto-utils"));
61
+ exports.CryptoUtils = CryptoUtils;
58
62
  const Csv = __importStar(require("./packlets/csv"));
59
63
  exports.Csv = Csv;
60
64
  const Experimental = __importStar(require("./packlets/experimental"));
61
65
  exports.Experimental = Experimental;
62
66
  const Hash = __importStar(require("./packlets/hash"));
63
67
  exports.Hash = Hash;
68
+ const Mustache = __importStar(require("./packlets/mustache"));
69
+ exports.Mustache = Mustache;
64
70
  const RecordJar = __importStar(require("./packlets/record-jar"));
65
71
  exports.RecordJar = RecordJar;
72
+ const Yaml = __importStar(require("./packlets/yaml"));
73
+ exports.Yaml = Yaml;
66
74
  const ZipFileTree = __importStar(require("./packlets/zip-file-tree"));
67
75
  exports.ZipFileTree = ZipFileTree;
68
76
  const conversion_1 = require("./packlets/conversion");
@@ -0,0 +1,60 @@
1
+ import { type Logging, Result } from '@fgv/ts-utils';
2
+ import { AiPrompt, type AiServerToolConfig, type IAiCompletionResponse, type IAiProviderDescriptor, type IChatMessage, type ModelSpec } from './model';
3
+ /**
4
+ * Parameters for a provider completion request.
5
+ * @public
6
+ */
7
+ export interface IProviderCompletionParams {
8
+ /** The provider descriptor */
9
+ readonly descriptor: IAiProviderDescriptor;
10
+ /** API key for authentication */
11
+ readonly apiKey: string;
12
+ /** The structured prompt to send */
13
+ readonly prompt: AiPrompt;
14
+ /**
15
+ * Additional messages to append after system+user (e.g. for correction retries).
16
+ * These are appended in order after the initial system and user messages.
17
+ */
18
+ readonly additionalMessages?: ReadonlyArray<IChatMessage>;
19
+ /** Sampling temperature (default: 0.7) */
20
+ readonly temperature?: number;
21
+ /** Optional model override — string or context-aware map (uses descriptor.defaultModel otherwise) */
22
+ readonly modelOverride?: ModelSpec;
23
+ /** Optional logger for request/response observability. */
24
+ readonly logger?: Logging.ILogger;
25
+ /** Server-side tools to include in the request. Overrides settings-level tool config when provided. */
26
+ readonly tools?: ReadonlyArray<AiServerToolConfig>;
27
+ }
28
+ /**
29
+ * Calls the appropriate chat completion API for a given provider.
30
+ *
31
+ * Routes based on the provider descriptor's `apiFormat` field:
32
+ * - `'openai'` for xAI, OpenAI, Groq, Mistral
33
+ * - `'anthropic'` for Anthropic Claude
34
+ * - `'gemini'` for Google Gemini
35
+ *
36
+ * When tools are provided and the provider supports them:
37
+ * - OpenAI-format providers switch to the Responses API
38
+ * - Anthropic includes tools in the Messages API request
39
+ * - Gemini includes Google Search grounding
40
+ *
41
+ * @param params - Request parameters including descriptor, API key, prompt, and optional tools
42
+ * @returns The completion response with content and truncation status, or a failure
43
+ * @public
44
+ */
45
+ export declare function callProviderCompletion(params: IProviderCompletionParams): Promise<Result<IAiCompletionResponse>>;
46
+ /**
47
+ * Calls the AI completion endpoint on a proxy server instead of calling
48
+ * the provider API directly from the browser.
49
+ *
50
+ * The proxy server handles provider dispatch, CORS, and API key forwarding.
51
+ * The request shape mirrors {@link IProviderCompletionParams} but is serialized
52
+ * as JSON for the proxy endpoint.
53
+ *
54
+ * @param proxyUrl - Base URL of the proxy server (e.g. `http://localhost:3001`)
55
+ * @param params - Same parameters as {@link callProviderCompletion}
56
+ * @returns The completion response, or a failure
57
+ * @public
58
+ */
59
+ export declare function callProxiedCompletion(proxyUrl: string, params: IProviderCompletionParams): Promise<Result<IAiCompletionResponse>>;
60
+ //# sourceMappingURL=apiClient.d.ts.map
@@ -0,0 +1,488 @@
1
+ "use strict";
2
+ // Copyright (c) 2026 Erik Fortune
3
+ //
4
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ // of this software and associated documentation files (the "Software"), to deal
6
+ // in the Software without restriction, including without limitation the rights
7
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ // copies of the Software, and to permit persons to whom the Software is
9
+ // furnished to do so, subject to the following conditions:
10
+ //
11
+ // The above copyright notice and this permission notice shall be included in all
12
+ // copies or substantial portions of the Software.
13
+ //
14
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ // SOFTWARE.
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.callProviderCompletion = callProviderCompletion;
23
+ exports.callProxiedCompletion = callProxiedCompletion;
24
+ /**
25
+ * Chat completion client for AI assist with support for multiple provider APIs.
26
+ *
27
+ * Supports OpenAI-compatible providers (xAI, OpenAI, Groq, Mistral) directly,
28
+ * plus adapters for Anthropic and Google Gemini.
29
+ *
30
+ * When server-side tools (e.g. web_search) are configured, providers that support
31
+ * them will include tool configuration in the request and handle tool-augmented
32
+ * responses.
33
+ *
34
+ * @packageDocumentation
35
+ */
36
+ const ts_json_base_1 = require("@fgv/ts-json-base");
37
+ const ts_utils_1 = require("@fgv/ts-utils");
38
+ const model_1 = require("./model");
39
+ const toolFormats_1 = require("./toolFormats");
40
+ // ============================================================================
41
+ // Shared helpers
42
+ // ============================================================================
43
+ /**
44
+ * Builds the messages array from prompt + optional correction messages.
45
+ * @internal
46
+ */
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
+ }
56
+ }
57
+ return messages;
58
+ }
59
+ /**
60
+ * Makes an HTTP request and returns the parsed JSON, or a failure.
61
+ * @internal
62
+ */
63
+ async function fetchJson(url, headers, body, logger) {
64
+ /* c8 ignore next 1 - optional logger */
65
+ logger === null || logger === void 0 ? void 0 : logger.detail(`AI API request: POST ${url}`);
66
+ let response;
67
+ try {
68
+ response = await fetch(url, {
69
+ method: 'POST',
70
+ headers: Object.assign({ 'Content-Type': 'application/json' }, headers),
71
+ body: JSON.stringify(body)
72
+ });
73
+ }
74
+ catch (err) {
75
+ const detail = err instanceof Error ? err.message : String(err);
76
+ /* c8 ignore next 1 - optional logger */
77
+ logger === null || logger === void 0 ? void 0 : logger.error(`AI API request failed: ${detail}`);
78
+ return (0, ts_utils_1.fail)(`AI API request failed: ${detail}`);
79
+ }
80
+ if (!response.ok) {
81
+ const errorText = await response.text().catch(() => 'unknown error');
82
+ /* c8 ignore next 1 - optional logger */
83
+ logger === null || logger === void 0 ? void 0 : logger.error(`AI API returned ${response.status}: ${errorText}`);
84
+ return (0, ts_utils_1.fail)(`AI API returned ${response.status}: ${errorText}`);
85
+ }
86
+ /* c8 ignore next 1 - optional logger */
87
+ logger === null || logger === void 0 ? void 0 : logger.detail(`AI API response: ${response.status}`);
88
+ let json;
89
+ try {
90
+ json = await response.json();
91
+ }
92
+ catch (_a) {
93
+ /* c8 ignore next 1 - optional logger */
94
+ logger === null || logger === void 0 ? void 0 : logger.error('AI API returned invalid JSON response');
95
+ return (0, ts_utils_1.fail)('AI API returned invalid JSON response');
96
+ }
97
+ if (!(0, ts_json_base_1.isJsonObject)(json)) {
98
+ /* c8 ignore next 1 - optional logger */
99
+ logger === null || logger === void 0 ? void 0 : logger.error('AI API returned non-object JSON response');
100
+ return (0, ts_utils_1.fail)('AI API returned non-object JSON response');
101
+ }
102
+ return (0, ts_utils_1.succeed)(json);
103
+ }
104
+ const openAiMessage = ts_utils_1.Validators.object({
105
+ content: ts_utils_1.Validators.string
106
+ });
107
+ const openAiChoice = ts_utils_1.Validators.object({
108
+ message: openAiMessage,
109
+ finish_reason: ts_utils_1.Validators.string
110
+ });
111
+ const openAiResponse = ts_utils_1.Validators.object({
112
+ choices: ts_utils_1.Validators.arrayOf(openAiChoice).withConstraint((arr) => arr.length > 0)
113
+ });
114
+ const responsesApiOutputText = ts_utils_1.Validators.object({
115
+ type: ts_utils_1.Validators.literal('output_text'),
116
+ text: ts_utils_1.Validators.string
117
+ });
118
+ const responsesApiMessage = ts_utils_1.Validators.object({
119
+ type: ts_utils_1.Validators.literal('message'),
120
+ role: ts_utils_1.Validators.string,
121
+ content: ts_utils_1.Validators.arrayOf(responsesApiOutputText).withConstraint((arr) => arr.length > 0)
122
+ });
123
+ const responsesApiOutputItem = ts_utils_1.Validators.isA('object', (v) => typeof v === 'object' && v !== null);
124
+ const responsesApiResponse = ts_utils_1.Validators.object({
125
+ output: ts_utils_1.Validators.arrayOf(responsesApiOutputItem).withConstraint((arr) => arr.length > 0),
126
+ status: ts_utils_1.Validators.string
127
+ });
128
+ const anthropicContentBlock = ts_utils_1.Validators.object({
129
+ text: ts_utils_1.Validators.string
130
+ });
131
+ const anthropicResponse = ts_utils_1.Validators.object({
132
+ content: ts_utils_1.Validators.arrayOf(anthropicContentBlock).withConstraint((arr) => arr.length > 0),
133
+ stop_reason: ts_utils_1.Validators.string
134
+ });
135
+ const geminiPart = ts_utils_1.Validators.object({
136
+ text: ts_utils_1.Validators.string
137
+ });
138
+ const geminiContent = ts_utils_1.Validators.object({
139
+ parts: ts_utils_1.Validators.arrayOf(geminiPart).withConstraint((arr) => arr.length > 0)
140
+ });
141
+ const geminiCandidate = ts_utils_1.Validators.object({
142
+ content: geminiContent,
143
+ finishReason: ts_utils_1.Validators.string
144
+ });
145
+ const geminiResponse = ts_utils_1.Validators.object({
146
+ candidates: ts_utils_1.Validators.arrayOf(geminiCandidate).withConstraint((arr) => arr.length > 0)
147
+ });
148
+ // ============================================================================
149
+ // OpenAI-compatible client (Chat Completions — no tools)
150
+ // ============================================================================
151
+ /**
152
+ * Calls an OpenAI-compatible chat completion endpoint.
153
+ * Works for xAI Grok, OpenAI, Groq, and Mistral.
154
+ * @internal
155
+ */
156
+ async function callOpenAiCompletion(config, prompt, additionalMessages, temperature = 0.7, logger) {
157
+ const url = `${config.baseUrl}/chat/completions`;
158
+ const messages = buildMessages(prompt, additionalMessages);
159
+ const body = { model: config.model, messages, temperature };
160
+ const headers = {
161
+ Authorization: `Bearer ${config.apiKey}`
162
+ };
163
+ /* c8 ignore next 1 - optional logger */
164
+ logger === null || logger === void 0 ? void 0 : logger.info(`OpenAI completion: model=${config.model}`);
165
+ const jsonResult = await fetchJson(url, headers, body, logger);
166
+ if (jsonResult.isFailure()) {
167
+ return (0, ts_utils_1.fail)(jsonResult.message);
168
+ }
169
+ return openAiResponse
170
+ .validate(jsonResult.value)
171
+ .withErrorFormat((msg) => `OpenAI API response: ${msg}`)
172
+ .onSuccess((response) => {
173
+ const choice = response.choices[0];
174
+ return (0, ts_utils_1.succeed)({
175
+ content: choice.message.content,
176
+ truncated: choice.finish_reason === 'length'
177
+ });
178
+ });
179
+ }
180
+ // ============================================================================
181
+ // OpenAI/xAI Responses API (with tools)
182
+ // ============================================================================
183
+ /**
184
+ * Extracts text content from a Responses API output array.
185
+ * Finds the first message-type output item and concatenates its text content blocks.
186
+ * @internal
187
+ */
188
+ function extractResponsesApiText(output) {
189
+ for (const item of output) {
190
+ if (item.type === 'message') {
191
+ const messageResult = responsesApiMessage.validate(item);
192
+ if (messageResult.isSuccess()) {
193
+ return (0, ts_utils_1.succeed)(messageResult.value.content.map((c) => c.text).join(''));
194
+ }
195
+ }
196
+ }
197
+ return (0, ts_utils_1.fail)('Responses API output contained no message with text content');
198
+ }
199
+ /**
200
+ * Calls the xAI/OpenAI Responses API with server-side tools.
201
+ * Used when tools are configured for an openai-format provider.
202
+ * @internal
203
+ */
204
+ async function callOpenAiResponsesCompletion(config, prompt, tools, additionalMessages, temperature = 0.7, logger) {
205
+ const url = `${config.baseUrl}/responses`;
206
+ const input = buildMessages(prompt, additionalMessages);
207
+ const body = {
208
+ model: config.model,
209
+ input,
210
+ tools: (0, toolFormats_1.toResponsesApiTools)(tools),
211
+ temperature
212
+ };
213
+ const headers = {
214
+ Authorization: `Bearer ${config.apiKey}`
215
+ };
216
+ /* c8 ignore next 1 - optional logger */
217
+ 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);
219
+ if (jsonResult.isFailure()) {
220
+ return (0, ts_utils_1.fail)(jsonResult.message);
221
+ }
222
+ return responsesApiResponse
223
+ .validate(jsonResult.value)
224
+ .withErrorFormat((msg) => `Responses API response: ${msg}`)
225
+ .onSuccess((response) => {
226
+ return extractResponsesApiText(response.output).onSuccess((text) => (0, ts_utils_1.succeed)({
227
+ content: text,
228
+ truncated: response.status === 'incomplete'
229
+ }));
230
+ });
231
+ }
232
+ // ============================================================================
233
+ // Anthropic adapter
234
+ // ============================================================================
235
+ /**
236
+ * Extracts text content from Anthropic response content blocks.
237
+ * When tools are used, the content array contains mixed block types
238
+ * (text, server_tool_use, web_search_tool_result). We extract and
239
+ * concatenate only the text blocks.
240
+ * @internal
241
+ */
242
+ function extractAnthropicText(content) {
243
+ const textParts = [];
244
+ for (const block of content) {
245
+ if (typeof block === 'object' && block !== null && 'type' in block) {
246
+ const typed = block;
247
+ if (typed.type === 'text' && typeof typed.text === 'string') {
248
+ textParts.push(typed.text);
249
+ }
250
+ }
251
+ }
252
+ if (textParts.length === 0) {
253
+ return (0, ts_utils_1.fail)('Anthropic response contained no text content blocks');
254
+ }
255
+ return (0, ts_utils_1.succeed)(textParts.join(''));
256
+ }
257
+ /**
258
+ * Calls the Anthropic Messages API.
259
+ * When tools are configured, includes them in the request and handles
260
+ * mixed content block responses.
261
+ * @internal
262
+ */
263
+ async function callAnthropicCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, tools) {
264
+ const url = `${config.baseUrl}/messages`;
265
+ // 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
+ }
275
+ const body = {
276
+ model: config.model,
277
+ system: prompt.system,
278
+ messages,
279
+ max_tokens: 4096,
280
+ temperature
281
+ };
282
+ if (tools && tools.length > 0) {
283
+ body.tools = (0, toolFormats_1.toAnthropicTools)(tools);
284
+ /* c8 ignore next 3 - optional logger diagnostic output */
285
+ logger === null || logger === void 0 ? void 0 : logger.info(`Anthropic completion: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`);
286
+ }
287
+ else {
288
+ /* c8 ignore next 1 - optional logger */
289
+ logger === null || logger === void 0 ? void 0 : logger.info(`Anthropic completion: model=${config.model}`);
290
+ }
291
+ const headers = {
292
+ 'x-api-key': config.apiKey,
293
+ 'anthropic-version': '2023-06-01',
294
+ 'anthropic-dangerous-direct-browser-access': 'true'
295
+ };
296
+ const jsonResult = await fetchJson(url, headers, body, logger);
297
+ if (jsonResult.isFailure()) {
298
+ return (0, ts_utils_1.fail)(jsonResult.message);
299
+ }
300
+ // When tools are used, the response content is a mixed array of block types.
301
+ // We need to extract text from all text blocks.
302
+ if (tools && tools.length > 0) {
303
+ const rawContent = jsonResult.value.content;
304
+ const stopReason = jsonResult.value.stop_reason;
305
+ if (!Array.isArray(rawContent)) {
306
+ return (0, ts_utils_1.fail)('Anthropic API response: content is not an array');
307
+ }
308
+ return extractAnthropicText(rawContent).onSuccess((text) => (0, ts_utils_1.succeed)({
309
+ content: text,
310
+ truncated: stopReason === 'max_tokens'
311
+ }));
312
+ }
313
+ return anthropicResponse
314
+ .validate(jsonResult.value)
315
+ .withErrorFormat((msg) => `Anthropic API response: ${msg}`)
316
+ .onSuccess((response) => {
317
+ return (0, ts_utils_1.succeed)({
318
+ content: response.content[0].text,
319
+ truncated: response.stop_reason === 'max_tokens'
320
+ });
321
+ });
322
+ }
323
+ // ============================================================================
324
+ // Google Gemini adapter
325
+ // ============================================================================
326
+ /**
327
+ * Calls the Google Gemini generateContent API.
328
+ * When tools are configured, includes Google Search grounding.
329
+ * @internal
330
+ */
331
+ async function callGeminiCompletion(config, prompt, additionalMessages, temperature = 0.7, logger, tools) {
332
+ const url = `${config.baseUrl}/models/${config.model}:generateContent`;
333
+ // 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
+ }
347
+ const body = {
348
+ systemInstruction: { parts: [{ text: prompt.system }] },
349
+ contents,
350
+ generationConfig: { temperature }
351
+ };
352
+ if (tools && tools.length > 0) {
353
+ body.tools = (0, toolFormats_1.toGeminiTools)(tools);
354
+ /* c8 ignore next 1 - optional logger */
355
+ logger === null || logger === void 0 ? void 0 : logger.info(`Gemini completion: model=${config.model}, tools=${tools.map((t) => t.type).join(',')}`);
356
+ }
357
+ else {
358
+ /* c8 ignore next 1 - optional logger */
359
+ logger === null || logger === void 0 ? void 0 : logger.info(`Gemini completion: model=${config.model}`);
360
+ }
361
+ const headers = {
362
+ 'x-goog-api-key': config.apiKey
363
+ };
364
+ const jsonResult = await fetchJson(url, headers, body, logger);
365
+ if (jsonResult.isFailure()) {
366
+ return (0, ts_utils_1.fail)(jsonResult.message);
367
+ }
368
+ return geminiResponse
369
+ .validate(jsonResult.value)
370
+ .withErrorFormat((msg) => `Gemini API response: ${msg}`)
371
+ .onSuccess((response) => {
372
+ const candidate = response.candidates[0];
373
+ return (0, ts_utils_1.succeed)({
374
+ content: candidate.content.parts[0].text,
375
+ truncated: candidate.finishReason === 'MAX_TOKENS'
376
+ });
377
+ });
378
+ }
379
+ // ============================================================================
380
+ // Provider dispatcher
381
+ // ============================================================================
382
+ /**
383
+ * Calls the appropriate chat completion API for a given provider.
384
+ *
385
+ * Routes based on the provider descriptor's `apiFormat` field:
386
+ * - `'openai'` for xAI, OpenAI, Groq, Mistral
387
+ * - `'anthropic'` for Anthropic Claude
388
+ * - `'gemini'` for Google Gemini
389
+ *
390
+ * When tools are provided and the provider supports them:
391
+ * - OpenAI-format providers switch to the Responses API
392
+ * - Anthropic includes tools in the Messages API request
393
+ * - Gemini includes Google Search grounding
394
+ *
395
+ * @param params - Request parameters including descriptor, API key, prompt, and optional tools
396
+ * @returns The completion response with content and truncation status, or a failure
397
+ * @public
398
+ */
399
+ async function callProviderCompletion(params) {
400
+ const { descriptor, apiKey, prompt, additionalMessages, temperature = 0.7, modelOverride, logger, tools } = params;
401
+ if (!descriptor.baseUrl) {
402
+ return (0, ts_utils_1.fail)(`provider "${descriptor.id}" has no API endpoint configured`);
403
+ }
404
+ const hasTools = tools !== undefined && tools.length > 0;
405
+ const modelContext = hasTools ? 'tools' : undefined;
406
+ const config = {
407
+ baseUrl: descriptor.baseUrl,
408
+ apiKey,
409
+ model: (0, model_1.resolveModel)(modelOverride !== null && modelOverride !== void 0 ? modelOverride : descriptor.defaultModel, modelContext)
410
+ };
411
+ /* c8 ignore next 8 - optional logger diagnostic output */
412
+ if (logger) {
413
+ const toolTypes = hasTools ? tools.map((t) => t.type).join(',') : 'none';
414
+ const supported = descriptor.supportedTools.length > 0 ? descriptor.supportedTools.join(',') : 'none';
415
+ logger.info(`AI completion: provider=${descriptor.id}, format=${descriptor.apiFormat}, model=${config.model}, ` +
416
+ `tools=${toolTypes}, supported=${supported}`);
417
+ }
418
+ switch (descriptor.apiFormat) {
419
+ case 'openai':
420
+ if (hasTools) {
421
+ return callOpenAiResponsesCompletion(config, prompt, tools, additionalMessages, temperature, logger);
422
+ }
423
+ return callOpenAiCompletion(config, prompt, additionalMessages, temperature, logger);
424
+ case 'anthropic':
425
+ return callAnthropicCompletion(config, prompt, additionalMessages, temperature, logger, tools);
426
+ case 'gemini':
427
+ return callGeminiCompletion(config, prompt, additionalMessages, temperature, logger, tools);
428
+ /* c8 ignore next 4 - defensive coding: exhaustive switch guaranteed by TypeScript */
429
+ default: {
430
+ const _exhaustive = descriptor.apiFormat;
431
+ return (0, ts_utils_1.fail)(`unsupported API format: ${String(_exhaustive)}`);
432
+ }
433
+ }
434
+ }
435
+ // ============================================================================
436
+ // Proxied completion (routes through a backend server)
437
+ // ============================================================================
438
+ /**
439
+ * Calls the AI completion endpoint on a proxy server instead of calling
440
+ * the provider API directly from the browser.
441
+ *
442
+ * The proxy server handles provider dispatch, CORS, and API key forwarding.
443
+ * The request shape mirrors {@link IProviderCompletionParams} but is serialized
444
+ * as JSON for the proxy endpoint.
445
+ *
446
+ * @param proxyUrl - Base URL of the proxy server (e.g. `http://localhost:3001`)
447
+ * @param params - Same parameters as {@link callProviderCompletion}
448
+ * @returns The completion response, or a failure
449
+ * @public
450
+ */
451
+ async function callProxiedCompletion(proxyUrl, params) {
452
+ const { descriptor, apiKey, prompt, additionalMessages, temperature, modelOverride, logger, tools } = params;
453
+ const body = {
454
+ providerId: descriptor.id,
455
+ apiKey,
456
+ prompt: { system: prompt.system, user: prompt.user },
457
+ temperature: temperature !== null && temperature !== void 0 ? temperature : 0.7
458
+ };
459
+ if (additionalMessages && additionalMessages.length > 0) {
460
+ body.additionalMessages = additionalMessages;
461
+ }
462
+ if (modelOverride !== undefined) {
463
+ body.modelOverride = modelOverride;
464
+ }
465
+ if (tools && tools.length > 0) {
466
+ body.tools = tools;
467
+ }
468
+ /* c8 ignore next 1 - optional logger */
469
+ logger === null || logger === void 0 ? void 0 : logger.info(`AI proxy request: provider=${descriptor.id}, proxy=${proxyUrl}`);
470
+ const url = `${proxyUrl}/api/ai/completion`;
471
+ const jsonResult = await fetchJson(url, {}, body, logger);
472
+ if (jsonResult.isFailure()) {
473
+ return (0, ts_utils_1.fail)(jsonResult.message);
474
+ }
475
+ // Check for error response from proxy
476
+ const response = jsonResult.value;
477
+ if (typeof response.error === 'string') {
478
+ return (0, ts_utils_1.fail)(`proxy: ${response.error}`);
479
+ }
480
+ if (typeof response.content !== 'string') {
481
+ return (0, ts_utils_1.fail)('proxy returned invalid response: missing content');
482
+ }
483
+ return (0, ts_utils_1.succeed)({
484
+ content: response.content,
485
+ truncated: response.truncated === true
486
+ });
487
+ }
488
+ //# sourceMappingURL=apiClient.js.map