@omnicross/core 0.1.0 → 0.1.1

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 (139) hide show
  1. package/dist/ApiConverter.cjs +799 -0
  2. package/dist/ApiConverter.d.cts +82 -0
  3. package/dist/ApiConverter.d.ts +82 -0
  4. package/dist/ApiConverter.js +763 -0
  5. package/dist/BuiltinToolExecutor-BluWyeob.d.ts +81 -0
  6. package/dist/BuiltinToolExecutor-CS2WpXhM.d.cts +81 -0
  7. package/dist/CompletionService-7fCmKAP3.d.ts +212 -0
  8. package/dist/CompletionService-DtOF_War.d.cts +212 -0
  9. package/dist/{ProviderProxy-f_8ziIhW.d.cts → ProviderProxy-C-xqrkKi.d.ts} +7 -2
  10. package/dist/{ProviderProxy-vjt8sQQk.d.ts → ProviderProxy-CnMQYN59.d.cts} +7 -2
  11. package/dist/completion/BuiltinToolExecutor.cjs +327 -0
  12. package/dist/completion/BuiltinToolExecutor.d.cts +4 -0
  13. package/dist/completion/BuiltinToolExecutor.d.ts +4 -0
  14. package/dist/completion/BuiltinToolExecutor.js +296 -0
  15. package/dist/completion/CompletionService.cjs +3487 -0
  16. package/dist/completion/CompletionService.d.cts +21 -0
  17. package/dist/completion/CompletionService.d.ts +21 -0
  18. package/dist/completion/CompletionService.js +3461 -0
  19. package/dist/completion/NativeSearchInjector.cjs +196 -0
  20. package/dist/completion/NativeSearchInjector.d.cts +42 -0
  21. package/dist/completion/NativeSearchInjector.d.ts +42 -0
  22. package/dist/completion/NativeSearchInjector.js +167 -0
  23. package/dist/completion/ProviderSearchInjector.cjs +87 -0
  24. package/dist/completion/ProviderSearchInjector.d.cts +47 -0
  25. package/dist/completion/ProviderSearchInjector.d.ts +47 -0
  26. package/dist/completion/ProviderSearchInjector.js +60 -0
  27. package/dist/completion/native-search-types.cjs +67 -0
  28. package/dist/completion/native-search-types.d.cts +3 -0
  29. package/dist/completion/native-search-types.d.ts +3 -0
  30. package/dist/completion/native-search-types.js +38 -0
  31. package/dist/completion/openrouter-headers.cjs +72 -0
  32. package/dist/completion/openrouter-headers.d.cts +44 -0
  33. package/dist/completion/openrouter-headers.d.ts +44 -0
  34. package/dist/completion/openrouter-headers.js +42 -0
  35. package/dist/completion/openrouter-models.cjs +86 -0
  36. package/dist/completion/openrouter-models.d.cts +27 -0
  37. package/dist/completion/openrouter-models.d.ts +27 -0
  38. package/dist/completion/openrouter-models.js +59 -0
  39. package/dist/completion/types.cjs +18 -0
  40. package/dist/completion/types.d.cts +3 -0
  41. package/dist/completion/types.d.ts +3 -0
  42. package/dist/completion/types.js +0 -0
  43. package/dist/completion/url-builder.cjs +138 -0
  44. package/dist/completion/url-builder.d.cts +87 -0
  45. package/dist/completion/url-builder.d.ts +87 -0
  46. package/dist/completion/url-builder.js +104 -0
  47. package/dist/completion.d.cts +148 -7
  48. package/dist/completion.d.ts +148 -7
  49. package/dist/index.cjs +1 -0
  50. package/dist/index.d.cts +27 -90
  51. package/dist/index.d.ts +27 -90
  52. package/dist/index.js +1 -0
  53. package/dist/outbound-api/routeResolver.cjs +221 -0
  54. package/dist/outbound-api/routeResolver.d.cts +18 -0
  55. package/dist/outbound-api/routeResolver.d.ts +18 -0
  56. package/dist/outbound-api/routeResolver.js +192 -0
  57. package/dist/outbound-api/subscriptionRegistryPort.d.cts +5 -2
  58. package/dist/outbound-api/subscriptionRegistryPort.d.ts +5 -2
  59. package/dist/outbound-api/types.cjs +18 -0
  60. package/dist/{types-CbCN2NQP.d.ts → outbound-api/types.d.cts} +17 -3
  61. package/dist/{types-CGGrKqC_.d.cts → outbound-api/types.d.ts} +17 -3
  62. package/dist/outbound-api/types.js +0 -0
  63. package/dist/outbound-api.cjs +1 -0
  64. package/dist/outbound-api.d.cts +14 -87
  65. package/dist/outbound-api.d.ts +14 -87
  66. package/dist/outbound-api.js +1 -0
  67. package/dist/pipeline/AuthSource.cjs +18 -0
  68. package/dist/pipeline/AuthSource.d.cts +101 -0
  69. package/dist/pipeline/AuthSource.d.ts +101 -0
  70. package/dist/pipeline/AuthSource.js +0 -0
  71. package/dist/pipeline/LlmConfigProviderAuth.cjs +169 -0
  72. package/dist/pipeline/LlmConfigProviderAuth.d.cts +86 -0
  73. package/dist/pipeline/LlmConfigProviderAuth.d.ts +86 -0
  74. package/dist/pipeline/LlmConfigProviderAuth.js +142 -0
  75. package/dist/pipeline/SubscriptionAuthSource.d.cts +165 -3
  76. package/dist/pipeline/SubscriptionAuthSource.d.ts +165 -3
  77. package/dist/pipeline/executeProviderCall.cjs +70 -0
  78. package/dist/pipeline/executeProviderCall.d.cts +149 -0
  79. package/dist/pipeline/executeProviderCall.d.ts +149 -0
  80. package/dist/pipeline/executeProviderCall.js +45 -0
  81. package/dist/pipeline/resolveProviderChain.cjs +47 -0
  82. package/dist/pipeline/resolveProviderChain.d.cts +58 -0
  83. package/dist/pipeline/resolveProviderChain.d.ts +58 -0
  84. package/dist/pipeline/resolveProviderChain.js +22 -0
  85. package/dist/pipeline/resolveSubscriptionChain.cjs +68 -0
  86. package/dist/pipeline/resolveSubscriptionChain.d.cts +68 -0
  87. package/dist/pipeline/resolveSubscriptionChain.d.ts +68 -0
  88. package/dist/pipeline/resolveSubscriptionChain.js +43 -0
  89. package/dist/ports/provider-config-source.cjs +18 -0
  90. package/dist/ports/provider-config-source.d.cts +51 -0
  91. package/dist/ports/provider-config-source.d.ts +51 -0
  92. package/dist/ports/provider-config-source.js +0 -0
  93. package/dist/ports/web-search-backend.cjs +18 -0
  94. package/dist/ports/web-search-backend.d.cts +29 -0
  95. package/dist/ports/web-search-backend.d.ts +29 -0
  96. package/dist/ports/web-search-backend.js +0 -0
  97. package/dist/ports.d.cts +10 -7
  98. package/dist/ports.d.ts +10 -7
  99. package/dist/provider-proxy/ProviderProxy.cjs +4643 -0
  100. package/dist/provider-proxy/ProviderProxy.d.cts +16 -0
  101. package/dist/provider-proxy/ProviderProxy.d.ts +16 -0
  102. package/dist/provider-proxy/ProviderProxy.js +4618 -0
  103. package/dist/provider-proxy/ingress/providerProxyShared.d.cts +5 -2
  104. package/dist/provider-proxy/ingress/providerProxyShared.d.ts +5 -2
  105. package/dist/provider-proxy/types.d.cts +406 -8
  106. package/dist/provider-proxy/types.d.ts +406 -8
  107. package/dist/provider-proxy.cjs +1 -0
  108. package/dist/provider-proxy.d.cts +8 -5
  109. package/dist/provider-proxy.d.ts +8 -5
  110. package/dist/provider-proxy.js +1 -0
  111. package/dist/routeResolver-BrbK6ja9.d.cts +88 -0
  112. package/dist/routeResolver-HE-ZO0fO.d.ts +88 -0
  113. package/dist/transformer/anthropicBetaInject.cjs +51 -0
  114. package/dist/transformer/anthropicBetaInject.d.cts +20 -0
  115. package/dist/transformer/anthropicBetaInject.d.ts +20 -0
  116. package/dist/transformer/anthropicBetaInject.js +25 -0
  117. package/dist/transformer/transformers/AnthropicTransformer.cjs +1017 -0
  118. package/dist/transformer/transformers/AnthropicTransformer.d.cts +148 -0
  119. package/dist/transformer/transformers/AnthropicTransformer.d.ts +148 -0
  120. package/dist/transformer/transformers/AnthropicTransformer.js +990 -0
  121. package/dist/transformer/transformers/ReasoningTransformer.cjs +273 -0
  122. package/dist/transformer/transformers/ReasoningTransformer.d.cts +47 -0
  123. package/dist/transformer/transformers/ReasoningTransformer.d.ts +47 -0
  124. package/dist/transformer/transformers/ReasoningTransformer.js +253 -0
  125. package/dist/transformer/transformers.cjs +3206 -0
  126. package/dist/transformer/transformers.d.cts +100 -0
  127. package/dist/transformer/transformers.d.ts +100 -0
  128. package/dist/transformer/transformers.js +3174 -0
  129. package/dist/transformer.d.cts +8 -31
  130. package/dist/transformer.d.ts +8 -31
  131. package/dist/types-BScIHmPr.d.cts +153 -0
  132. package/dist/types-BScIHmPr.d.ts +153 -0
  133. package/package.json +3 -3
  134. package/dist/SubscriptionAuthSource-Cr4fVEYY.d.cts +0 -264
  135. package/dist/SubscriptionAuthSource-D89zmiSS.d.ts +0 -264
  136. package/dist/index-BTSmc9Sm.d.ts +0 -645
  137. package/dist/index-DXazdTzZ.d.cts +0 -645
  138. package/dist/types-DCzHkhJt.d.ts +0 -467
  139. package/dist/types-DZIQbgp0.d.cts +0 -467
@@ -0,0 +1,3487 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/completion/CompletionService.ts
31
+ var CompletionService_exports = {};
32
+ __export(CompletionService_exports, {
33
+ CompletionService: () => CompletionService
34
+ });
35
+ module.exports = __toCommonJS(CompletionService_exports);
36
+
37
+ // src/completion/url-builder.ts
38
+ var import_endpoint_resolver = require("@omnicross/contracts/endpoint-resolver");
39
+ function resolveApiFormat(provider) {
40
+ if (provider.apiFormat) {
41
+ return provider.apiFormat;
42
+ }
43
+ if (provider.chatApiFormat) {
44
+ return provider.chatApiFormat;
45
+ }
46
+ if (provider.apiType === "claudecode" || provider.apiType === "anthropic") {
47
+ return "anthropic";
48
+ }
49
+ if (provider.apiType === "google") {
50
+ return "google";
51
+ }
52
+ return "openai";
53
+ }
54
+ function buildOpenAIApiUrl(baseUrl) {
55
+ const url = baseUrl.replace(/\/+$/, "");
56
+ if (url.endsWith("/chat/completions")) {
57
+ return url;
58
+ }
59
+ if (/\/v\d+$/.test(url)) {
60
+ return url + "/chat/completions";
61
+ }
62
+ if (url.includes("/chat/completions")) {
63
+ return url;
64
+ }
65
+ return url + "/v1/chat/completions";
66
+ }
67
+ function buildAnthropicApiUrl(baseUrl) {
68
+ const url = baseUrl.replace(/\/+$/, "");
69
+ if (url.endsWith("/messages")) {
70
+ return url;
71
+ }
72
+ if (url.includes("/messages")) {
73
+ return url;
74
+ }
75
+ if (/\/v\d+$/.test(url)) {
76
+ return url + "/messages";
77
+ }
78
+ return url + "/v1/messages";
79
+ }
80
+ function buildGeminiModelActionUrl(baseUrl, model, action) {
81
+ let url = baseUrl.replace(/\/+$/, "");
82
+ if (url.includes("/models/")) {
83
+ return url;
84
+ }
85
+ if (!/\/v\d+(?:beta)?(?:$|\/)/.test(url)) {
86
+ url += "/v1beta";
87
+ }
88
+ return `${url}/models/${model}:${action}`;
89
+ }
90
+ function buildGeminiApiUrl(baseUrl, model, stream) {
91
+ const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
92
+ return buildGeminiModelActionUrl(baseUrl, model, action);
93
+ }
94
+ function normalizeAzureEndpoint(endpoint) {
95
+ return endpoint.replace(/\/+$/, "").replace(/\/openai(\/v\d+)?$/, "");
96
+ }
97
+ function buildAzureOpenAIApiUrl(baseUrl, model, apiVersion) {
98
+ const endpoint = normalizeAzureEndpoint(baseUrl);
99
+ return `${endpoint}/openai/deployments/${model}/chat/completions?api-version=${apiVersion}`;
100
+ }
101
+ function buildOpenAIResponseApiUrl(baseUrl) {
102
+ const url = baseUrl.replace(/\/+$/, "");
103
+ if (url.endsWith("/responses")) {
104
+ return url;
105
+ }
106
+ if (/\/v\d+$/.test(url)) {
107
+ return url + "/responses";
108
+ }
109
+ return url + "/v1/responses";
110
+ }
111
+ var resolveProviderEndpoint = import_endpoint_resolver.resolveProviderEndpoint;
112
+ function buildProviderApiUrl(provider, options = {}) {
113
+ const format = resolveApiFormat(provider);
114
+ const { baseUrl } = resolveProviderEndpoint(provider);
115
+ switch (format) {
116
+ case "anthropic":
117
+ return buildAnthropicApiUrl(baseUrl);
118
+ case "google":
119
+ return buildGeminiApiUrl(baseUrl, options.model || "", options.stream || false);
120
+ case "azure-openai":
121
+ return buildAzureOpenAIApiUrl(baseUrl, options.model || "", provider.apiVersion || "2024-08-01-preview");
122
+ case "openai-response":
123
+ return buildOpenAIResponseApiUrl(baseUrl);
124
+ case "openai":
125
+ default:
126
+ return buildOpenAIApiUrl(baseUrl);
127
+ }
128
+ }
129
+
130
+ // src/openrouter.ts
131
+ function isOpenRouterProvider(provider) {
132
+ const baseUrl = (provider.api_base_url || "").toLowerCase();
133
+ return baseUrl.includes("openrouter.ai");
134
+ }
135
+ var OPENROUTER_APP_HEADERS = {
136
+ "HTTP-Referer": "https://omnicross.dev",
137
+ "X-Title": "omnicross"
138
+ };
139
+
140
+ // src/completion/header-builder.ts
141
+ function getProviderHeaders(provider, apiKey) {
142
+ const format = resolveApiFormat(provider);
143
+ let headers;
144
+ switch (format) {
145
+ case "anthropic":
146
+ headers = {
147
+ "Content-Type": "application/json",
148
+ "x-api-key": apiKey,
149
+ "anthropic-version": "2025-01-10"
150
+ // Required for extended thinking feature
151
+ };
152
+ break;
153
+ case "google":
154
+ headers = {
155
+ "Content-Type": "application/json",
156
+ "x-goog-api-key": apiKey
157
+ };
158
+ break;
159
+ case "azure-openai":
160
+ headers = {
161
+ "Content-Type": "application/json",
162
+ "api-key": apiKey
163
+ };
164
+ break;
165
+ case "openai":
166
+ case "openai-response":
167
+ default:
168
+ headers = {
169
+ "Content-Type": "application/json",
170
+ "Authorization": `Bearer ${apiKey}`
171
+ };
172
+ break;
173
+ }
174
+ if (isOpenRouterProvider(provider)) {
175
+ return { ...headers, ...OPENROUTER_APP_HEADERS };
176
+ }
177
+ return headers;
178
+ }
179
+
180
+ // src/completion/message-converter.ts
181
+ var OPENROUTER_AUDIO_FORMAT_MAP = {
182
+ "audio/wav": "wav",
183
+ "audio/x-wav": "wav",
184
+ "audio/wave": "wav",
185
+ "audio/mpeg": "mp3",
186
+ "audio/mp3": "mp3",
187
+ "audio/aiff": "aiff",
188
+ "audio/x-aiff": "aiff",
189
+ "audio/aac": "aac",
190
+ "audio/ogg": "ogg",
191
+ "audio/flac": "flac",
192
+ "audio/x-flac": "flac",
193
+ "audio/mp4": "m4a",
194
+ "audio/m4a": "m4a",
195
+ "audio/x-m4a": "m4a",
196
+ "audio/L16": "pcm16",
197
+ "audio/L24": "pcm24"
198
+ };
199
+ var MAX_INLINE_VIDEO_BYTES = 25 * 1024 * 1024;
200
+ function decodeDataUrl(url) {
201
+ const match = url.match(/^data:([^;]+);base64,(.+)$/);
202
+ if (!match) return null;
203
+ return { mimeType: match[1], data: match[2] };
204
+ }
205
+ function looksLikeRemoteUrl(url) {
206
+ return /^https?:\/\//i.test(url);
207
+ }
208
+ function resolveAudioFormat(mimeType, sourceUrl) {
209
+ const candidate = (mimeType || "").toLowerCase();
210
+ const mapped = OPENROUTER_AUDIO_FORMAT_MAP[candidate];
211
+ if (mapped) return mapped;
212
+ const decoded = decodeDataUrl(sourceUrl);
213
+ if (decoded) {
214
+ const mappedFromUrl = OPENROUTER_AUDIO_FORMAT_MAP[decoded.mimeType.toLowerCase()];
215
+ if (mappedFromUrl) return mappedFromUrl;
216
+ }
217
+ throw new Error(
218
+ `Unsupported audio format for OpenAI/OpenRouter chat input: ${mimeType || "unknown"}. Supported: wav, mp3, aiff, aac, ogg, flac, m4a, pcm16, pcm24.`
219
+ );
220
+ }
221
+ function convertMessageToOpenAI(msg) {
222
+ const role = msg.role;
223
+ const hasImages = msg.images && msg.images.length > 0;
224
+ const hasAudios = msg.audios && msg.audios.length > 0;
225
+ const hasVideos = msg.videos && msg.videos.length > 0;
226
+ if (!hasImages && !hasAudios && !hasVideos) {
227
+ return { role, content: msg.content };
228
+ }
229
+ const content = [];
230
+ if (msg.content) {
231
+ content.push({ type: "text", text: msg.content });
232
+ }
233
+ if (msg.images) {
234
+ for (const img of msg.images) {
235
+ content.push({
236
+ type: "image_url",
237
+ image_url: { url: img.url }
238
+ });
239
+ }
240
+ }
241
+ if (msg.audios) {
242
+ for (const audio of msg.audios) {
243
+ const decoded = decodeDataUrl(audio.url);
244
+ if (decoded) {
245
+ const format = resolveAudioFormat(decoded.mimeType, audio.url);
246
+ content.push({ type: "input_audio", input_audio: { data: decoded.data, format } });
247
+ } else if (audio.mimeType) {
248
+ content.push({ type: "audio_url", audio_url: { url: audio.url, format: audio.mimeType } });
249
+ } else {
250
+ content.push({ type: "audio_url", audio_url: { url: audio.url } });
251
+ }
252
+ }
253
+ }
254
+ if (msg.videos) {
255
+ for (const video of msg.videos) {
256
+ if (looksLikeRemoteUrl(video.url)) {
257
+ content.push({ type: "video_url", video_url: { url: video.url } });
258
+ continue;
259
+ }
260
+ const decoded = decodeDataUrl(video.url);
261
+ if (decoded) {
262
+ const sizeBytes = Math.floor(decoded.data.length * 3 / 4);
263
+ if (sizeBytes > MAX_INLINE_VIDEO_BYTES) {
264
+ const sizeMb = (sizeBytes / 1024 / 1024).toFixed(1);
265
+ throw new Error(
266
+ `Video too large for inline upload: ${sizeMb} MB exceeds the 25 MB cap. Use a publicly accessible HTTPS URL instead.`
267
+ );
268
+ }
269
+ content.push({ type: "video_url", video_url: { url: video.url } });
270
+ } else {
271
+ content.push({ type: "video_url", video_url: { url: video.url } });
272
+ }
273
+ }
274
+ }
275
+ return { role, content };
276
+ }
277
+ function convertMessageToAnthropic(msg) {
278
+ const role = msg.role === "system" ? "user" : msg.role;
279
+ const hasImages = msg.images && msg.images.length > 0;
280
+ const hasAudios = msg.audios && msg.audios.length > 0;
281
+ if (!hasImages && !hasAudios) {
282
+ return { role, content: msg.content };
283
+ }
284
+ const content = [];
285
+ if (msg.content) {
286
+ content.push({ type: "text", text: msg.content });
287
+ }
288
+ if (msg.images) {
289
+ for (const img of msg.images) {
290
+ let base64Data = img.url;
291
+ let mediaType = img.mimeType || "image/jpeg";
292
+ if (img.url.startsWith("data:")) {
293
+ const match = img.url.match(/^data:([^;]+);base64,(.+)$/);
294
+ if (match) {
295
+ mediaType = match[1];
296
+ base64Data = match[2];
297
+ }
298
+ }
299
+ content.push({
300
+ type: "image",
301
+ source: { type: "base64", media_type: mediaType, data: base64Data }
302
+ });
303
+ }
304
+ }
305
+ if (msg.audios) {
306
+ for (const audio of msg.audios) {
307
+ let base64Data = audio.url;
308
+ let mediaType = audio.mimeType || "audio/wav";
309
+ if (audio.url.startsWith("data:")) {
310
+ const match = audio.url.match(/^data:([^;]+);base64,(.+)$/);
311
+ if (match) {
312
+ mediaType = match[1];
313
+ base64Data = match[2];
314
+ }
315
+ }
316
+ content.push({
317
+ type: "audio",
318
+ source: { type: "base64", media_type: mediaType, data: base64Data }
319
+ });
320
+ }
321
+ }
322
+ return { role, content };
323
+ }
324
+ function convertMessageToGemini(msg) {
325
+ const role = msg.role === "assistant" ? "model" : msg.role;
326
+ const parts = [];
327
+ if (msg.content) {
328
+ parts.push({ text: msg.content });
329
+ }
330
+ if (msg.images && msg.images.length > 0) {
331
+ for (const img of msg.images) {
332
+ let base64Data = img.url;
333
+ let mimeType = img.mimeType || "image/jpeg";
334
+ if (img.url.startsWith("data:")) {
335
+ const match = img.url.match(/^data:([^;]+);base64,(.+)$/);
336
+ if (match) {
337
+ mimeType = match[1];
338
+ base64Data = match[2];
339
+ }
340
+ }
341
+ parts.push({ inlineData: { mimeType, data: base64Data } });
342
+ }
343
+ }
344
+ if (msg.audios && msg.audios.length > 0) {
345
+ for (const audio of msg.audios) {
346
+ let base64Data = audio.url;
347
+ let mimeType = audio.mimeType || "audio/wav";
348
+ if (audio.url.startsWith("data:")) {
349
+ const match = audio.url.match(/^data:([^;]+);base64,(.+)$/);
350
+ if (match) {
351
+ mimeType = match[1];
352
+ base64Data = match[2];
353
+ }
354
+ }
355
+ parts.push({ inlineData: { mimeType, data: base64Data } });
356
+ }
357
+ }
358
+ return { role, parts };
359
+ }
360
+
361
+ // src/completion/openrouter-utils.ts
362
+ function getOpenRouterProviderConfig(provider, modelId) {
363
+ if (!isOpenRouterProvider(provider)) {
364
+ return void 0;
365
+ }
366
+ const modelConfig = provider.modelConfigs?.find((m) => m.id === modelId);
367
+ return modelConfig?.openRouterProvider;
368
+ }
369
+ function addOpenRouterProviderToRequest(requestBody, provider, modelId) {
370
+ const providerRouting = getOpenRouterProviderConfig(provider, modelId);
371
+ if (!providerRouting) {
372
+ return requestBody;
373
+ }
374
+ const hasConfig = Object.values(providerRouting).some(
375
+ (v) => v !== void 0 && v !== null && (Array.isArray(v) ? v.length > 0 : true)
376
+ );
377
+ if (!hasConfig) {
378
+ return requestBody;
379
+ }
380
+ return {
381
+ ...requestBody,
382
+ provider: providerRouting
383
+ };
384
+ }
385
+
386
+ // src/completion/native-search-types.ts
387
+ var NATIVE_SEARCH_TOOL_NAMES = [
388
+ "web_search",
389
+ "web_search_20250305",
390
+ "web_search_20260209",
391
+ "google_search",
392
+ "googleSearchRetrieval"
393
+ ];
394
+
395
+ // src/completion/NativeSearchInjector.ts
396
+ function applyAugmentation(requestBody, augmentation) {
397
+ if (augmentation.additionalTools?.length) {
398
+ const existing = requestBody.tools ?? [];
399
+ requestBody.tools = [...existing, ...augmentation.additionalTools];
400
+ }
401
+ if (augmentation.bodyFields) {
402
+ Object.assign(requestBody, augmentation.bodyFields);
403
+ }
404
+ return requestBody;
405
+ }
406
+
407
+ // src/completion/DirectApiHandler.ts
408
+ var import_thinking_config2 = require("@omnicross/contracts/thinking-config");
409
+
410
+ // src/sse-parser.ts
411
+ var DEBUG_SSE = true;
412
+ function debugSSE(prefix, ...args) {
413
+ if (DEBUG_SSE) {
414
+ console.log(`[SSE-Parser] ${prefix}`, ...args);
415
+ }
416
+ }
417
+ function createSSEParser(format, callbacks) {
418
+ const state = {
419
+ buffer: "",
420
+ content: "",
421
+ reasoning: "",
422
+ usage: void 0,
423
+ isDone: false,
424
+ streamStartTime: void 0,
425
+ firstTokenTime: void 0,
426
+ streamEndTime: void 0,
427
+ blocks: [],
428
+ currentBlockIndex: -1,
429
+ currentBlockType: "",
430
+ inputJsonBuffer: "",
431
+ audios: [],
432
+ videos: []
433
+ };
434
+ function parseOpenAIEvent(data) {
435
+ if (data === "[DONE]") {
436
+ debugSSE("OpenAI [DONE] received, content length:", state.content.length);
437
+ state.isDone = true;
438
+ callbacks.onDone?.();
439
+ return;
440
+ }
441
+ try {
442
+ const json = JSON.parse(data);
443
+ callbacks.onRawEvent?.({ data: json, raw: data });
444
+ const delta = json.choices?.[0]?.delta;
445
+ const finishReason = json.choices?.[0]?.finish_reason;
446
+ if (finishReason) {
447
+ debugSSE("OpenAI finish_reason:", finishReason, "content length so far:", state.content.length);
448
+ }
449
+ if (delta?.content) {
450
+ if (!state.firstTokenTime) {
451
+ state.firstTokenTime = Date.now();
452
+ }
453
+ state.content += delta.content;
454
+ callbacks.onDelta?.(delta.content);
455
+ }
456
+ if (delta?.reasoning_content) {
457
+ state.reasoning += delta.reasoning_content;
458
+ callbacks.onReasoning?.(delta.reasoning_content);
459
+ }
460
+ if (delta?.audio) {
461
+ const audioData = delta.audio;
462
+ if (audioData.data) {
463
+ const mimeType = audioData.format ? `audio/${audioData.format}` : "audio/wav";
464
+ const audio = {
465
+ url: audioData.data.startsWith("data:") ? audioData.data : `data:${mimeType};base64,${audioData.data}`,
466
+ mimeType
467
+ };
468
+ state.audios.push(audio);
469
+ callbacks.onAudio?.(audio);
470
+ }
471
+ }
472
+ if (json.usage) {
473
+ debugSSE("OpenAI usage received:", JSON.stringify(json.usage));
474
+ debugSSE("OpenAI final chunk full data:", JSON.stringify(json));
475
+ state.usage = {
476
+ promptTokens: json.usage.prompt_tokens || 0,
477
+ completionTokens: json.usage.completion_tokens || 0,
478
+ totalTokens: json.usage.total_tokens || 0
479
+ };
480
+ callbacks.onUsage?.(state.usage);
481
+ }
482
+ if (finishReason === "stop" || finishReason === "end_turn" || finishReason === "length") {
483
+ debugSSE("OpenAI stream marked done due to finish_reason:", finishReason);
484
+ }
485
+ } catch {
486
+ }
487
+ }
488
+ function parseAnthropicEvent(data) {
489
+ try {
490
+ const json = JSON.parse(data);
491
+ callbacks.onRawEvent?.({ type: json.type, data: json, raw: data });
492
+ if (json.type === "content_block_start") {
493
+ const contentBlock = json.content_block;
494
+ const blockIndex = json.index ?? -1;
495
+ if (contentBlock?.type === "thinking") {
496
+ debugSSE("THINKING BLOCK STARTED!");
497
+ }
498
+ if (contentBlock?.type === "server_tool_use") {
499
+ state.currentBlockIndex = blockIndex;
500
+ state.currentBlockType = "server_tool_use";
501
+ state.inputJsonBuffer = "";
502
+ const toolUseBlock = {
503
+ id: contentBlock.id || `block_${Date.now()}`,
504
+ type: "tool_use",
505
+ toolId: contentBlock.id || "",
506
+ toolName: contentBlock.name || "web_search",
507
+ input: contentBlock.input || {},
508
+ status: "running"
509
+ };
510
+ state.blocks.push(toolUseBlock);
511
+ callbacks.onBlock?.(toolUseBlock);
512
+ }
513
+ if (contentBlock?.type === "web_search_tool_result") {
514
+ state.currentBlockIndex = blockIndex;
515
+ state.currentBlockType = "web_search_tool_result";
516
+ const searchResults = contentBlock.content;
517
+ let output = "";
518
+ if (Array.isArray(searchResults)) {
519
+ output = searchResults.filter((r) => r.type === "web_search_result").map(
520
+ (r) => `**${r.title || "Untitled"}**
521
+ ${r.url || ""}
522
+ ${r.encrypted_content ? "(encrypted)" : r.page_content || r.snippet || ""}`
523
+ ).join("\n\n");
524
+ }
525
+ const lastToolUse = [...state.blocks].reverse().find(
526
+ (b) => b.type === "tool_use"
527
+ );
528
+ const toolResultBlock = {
529
+ id: `result_${Date.now()}`,
530
+ type: "tool_result",
531
+ toolId: lastToolUse?.toolId || "",
532
+ toolName: lastToolUse?.toolName || "web_search",
533
+ output: output || "(no results)"
534
+ };
535
+ state.blocks.push(toolResultBlock);
536
+ callbacks.onBlock?.(toolResultBlock);
537
+ if (lastToolUse) {
538
+ lastToolUse.status = "completed";
539
+ callbacks.onBlock?.(lastToolUse);
540
+ }
541
+ }
542
+ }
543
+ if (json.type === "content_block_delta") {
544
+ const delta = json.delta;
545
+ if (delta?.type === "text_delta" && delta?.text) {
546
+ if (!state.firstTokenTime) {
547
+ state.firstTokenTime = Date.now();
548
+ }
549
+ state.content += delta.text;
550
+ callbacks.onDelta?.(delta.text);
551
+ }
552
+ if (delta?.type === "thinking_delta" && delta?.thinking) {
553
+ state.reasoning += delta.thinking;
554
+ callbacks.onReasoning?.(delta.thinking);
555
+ }
556
+ if (delta?.type === "input_json_delta" && delta?.partial_json) {
557
+ state.inputJsonBuffer += delta.partial_json;
558
+ }
559
+ }
560
+ if (json.type === "content_block_stop") {
561
+ if (state.currentBlockType === "server_tool_use" && state.inputJsonBuffer) {
562
+ const lastToolUse = [...state.blocks].reverse().find(
563
+ (b) => b.type === "tool_use"
564
+ );
565
+ if (lastToolUse) {
566
+ try {
567
+ lastToolUse.input = JSON.parse(state.inputJsonBuffer);
568
+ } catch {
569
+ lastToolUse.input = { raw: state.inputJsonBuffer };
570
+ }
571
+ callbacks.onBlock?.(lastToolUse);
572
+ }
573
+ }
574
+ state.currentBlockIndex = -1;
575
+ state.currentBlockType = "";
576
+ state.inputJsonBuffer = "";
577
+ }
578
+ if (json.type === "message_delta" && json.usage) {
579
+ state.usage = {
580
+ promptTokens: json.usage.input_tokens || 0,
581
+ completionTokens: json.usage.output_tokens || 0,
582
+ totalTokens: (json.usage.input_tokens || 0) + (json.usage.output_tokens || 0)
583
+ };
584
+ callbacks.onUsage?.(state.usage);
585
+ }
586
+ if (json.type === "message_stop") {
587
+ state.isDone = true;
588
+ callbacks.onDone?.();
589
+ }
590
+ if (json.type === "error") {
591
+ callbacks.onError?.(json.error?.message || "Unknown error");
592
+ }
593
+ } catch {
594
+ }
595
+ }
596
+ function parseGeminiEvent(data) {
597
+ try {
598
+ const json = JSON.parse(data);
599
+ callbacks.onRawEvent?.({ data: json, raw: data });
600
+ const candidates = json.candidates;
601
+ if (candidates && candidates.length > 0) {
602
+ const candidate = candidates[0];
603
+ const content = candidate.content;
604
+ if (content?.parts) {
605
+ for (const part of content.parts) {
606
+ if (part.thought === true && part.text) {
607
+ state.reasoning += part.text;
608
+ callbacks.onReasoning?.(part.text);
609
+ } else if (part.text) {
610
+ if (!state.firstTokenTime) {
611
+ state.firstTokenTime = Date.now();
612
+ }
613
+ state.content += part.text;
614
+ callbacks.onDelta?.(part.text);
615
+ }
616
+ }
617
+ }
618
+ const finishReason = candidate.finishReason;
619
+ if (finishReason === "STOP" || finishReason === "MAX_TOKENS" || finishReason === "SAFETY") {
620
+ debugSSE("Gemini finish reason:", finishReason);
621
+ }
622
+ }
623
+ const usage = json.usageMetadata;
624
+ if (usage) {
625
+ state.usage = {
626
+ promptTokens: usage.promptTokenCount || 0,
627
+ completionTokens: usage.candidatesTokenCount || 0,
628
+ totalTokens: usage.totalTokenCount || 0
629
+ };
630
+ callbacks.onUsage?.(state.usage);
631
+ debugSSE("Gemini usage:", state.usage);
632
+ }
633
+ if (json.error) {
634
+ callbacks.onError?.(json.error.message || "Gemini API error");
635
+ }
636
+ } catch {
637
+ }
638
+ }
639
+ function parseOpenAIResponseEvent(data) {
640
+ try {
641
+ const json = JSON.parse(data);
642
+ callbacks.onRawEvent?.({ type: json.type, data: json, raw: data });
643
+ switch (json.type) {
644
+ case "response.output_text.delta":
645
+ if (json.delta) {
646
+ if (!state.firstTokenTime) {
647
+ state.firstTokenTime = Date.now();
648
+ }
649
+ state.content += json.delta;
650
+ callbacks.onDelta?.(json.delta);
651
+ }
652
+ break;
653
+ case "response.reasoning_summary_text.delta":
654
+ if (json.delta) {
655
+ state.reasoning += json.delta;
656
+ callbacks.onReasoning?.(json.delta);
657
+ }
658
+ break;
659
+ case "response.completed": {
660
+ const response = json.response;
661
+ if (response?.usage) {
662
+ state.usage = {
663
+ promptTokens: response.usage.input_tokens || 0,
664
+ completionTokens: response.usage.output_tokens || 0,
665
+ totalTokens: (response.usage.input_tokens || 0) + (response.usage.output_tokens || 0)
666
+ };
667
+ callbacks.onUsage?.(state.usage);
668
+ }
669
+ state.isDone = true;
670
+ callbacks.onDone?.();
671
+ break;
672
+ }
673
+ case "error":
674
+ callbacks.onError?.(json.error?.message || json.message || "Unknown error");
675
+ break;
676
+ // Informational events — no-op
677
+ // response.created, response.in_progress, response.output_item.added,
678
+ // response.content_part.added, response.output_text.done,
679
+ // response.content_part.done, response.output_item.done, etc.
680
+ default:
681
+ break;
682
+ }
683
+ } catch {
684
+ }
685
+ }
686
+ function processEventBlock(eventBlock) {
687
+ const lines = eventBlock.split("\n");
688
+ const dataLines = [];
689
+ for (const line of lines) {
690
+ if (line.startsWith("data: ")) {
691
+ dataLines.push(line.slice(6));
692
+ } else if (line.trim() !== "" && !line.startsWith(":")) {
693
+ }
694
+ }
695
+ if (dataLines.length > 0) {
696
+ const data = dataLines.join("\n");
697
+ if (format === "openai") {
698
+ parseOpenAIEvent(data);
699
+ } else if (format === "gemini") {
700
+ parseGeminiEvent(data);
701
+ } else if (format === "openai-response") {
702
+ parseOpenAIResponseEvent(data);
703
+ } else {
704
+ parseAnthropicEvent(data);
705
+ }
706
+ }
707
+ }
708
+ return {
709
+ /**
710
+ * Push a chunk of data to the parser
711
+ * @param chunk - Raw SSE chunk from response stream
712
+ */
713
+ push(chunk) {
714
+ if (state.isDone) return;
715
+ if (!state.streamStartTime) {
716
+ state.streamStartTime = Date.now();
717
+ }
718
+ state.buffer += chunk;
719
+ const events = state.buffer.split("\n\n");
720
+ state.buffer = events.pop() || "";
721
+ for (const eventBlock of events) {
722
+ processEventBlock(eventBlock);
723
+ if (state.isDone) break;
724
+ }
725
+ },
726
+ /**
727
+ * Flush any remaining data in the buffer
728
+ */
729
+ flush() {
730
+ debugSSE("flush() called, buffer length:", state.buffer.length, "isDone:", state.isDone);
731
+ if (state.buffer.trim()) {
732
+ debugSSE("flush() processing remaining buffer:", state.buffer.substring(0, 200));
733
+ processEventBlock(state.buffer);
734
+ state.buffer = "";
735
+ }
736
+ state.streamEndTime = Date.now();
737
+ if (!state.isDone) {
738
+ debugSSE("flush() marking stream as done, final content length:", state.content.length);
739
+ state.isDone = true;
740
+ callbacks.onDone?.();
741
+ }
742
+ },
743
+ /**
744
+ * Get accumulated results
745
+ */
746
+ getResult() {
747
+ let metrics;
748
+ if (state.usage && state.firstTokenTime && state.streamEndTime) {
749
+ const timeCompletionMs = state.streamEndTime - state.firstTokenTime;
750
+ const timeFirstTokenMs = state.streamStartTime ? state.firstTokenTime - state.streamStartTime : void 0;
751
+ metrics = {
752
+ completionTokens: state.usage.completionTokens,
753
+ timeCompletionMs: timeCompletionMs > 0 ? timeCompletionMs : 1,
754
+ // Ensure at least 1ms
755
+ timeFirstTokenMs
756
+ };
757
+ }
758
+ if (state.reasoning) {
759
+ const thinkingBlock = {
760
+ id: `thinking_${Date.now()}`,
761
+ type: "thinking",
762
+ content: state.reasoning
763
+ };
764
+ state.blocks.unshift(thinkingBlock);
765
+ }
766
+ return {
767
+ content: state.content,
768
+ reasoning: state.reasoning,
769
+ usage: state.usage,
770
+ metrics,
771
+ blocks: state.blocks,
772
+ audios: state.audios,
773
+ videos: state.videos
774
+ };
775
+ },
776
+ /**
777
+ * Check if stream is complete
778
+ */
779
+ isDone() {
780
+ return state.isDone;
781
+ },
782
+ /**
783
+ * Reset parser state for reuse
784
+ */
785
+ reset() {
786
+ state.buffer = "";
787
+ state.content = "";
788
+ state.reasoning = "";
789
+ state.usage = void 0;
790
+ state.isDone = false;
791
+ state.streamStartTime = void 0;
792
+ state.firstTokenTime = void 0;
793
+ state.streamEndTime = void 0;
794
+ state.blocks = [];
795
+ state.currentBlockIndex = -1;
796
+ state.currentBlockType = "";
797
+ state.inputJsonBuffer = "";
798
+ state.audios = [];
799
+ state.videos = [];
800
+ }
801
+ };
802
+ }
803
+ async function streamSSEResponse(response, format, callbacks) {
804
+ debugSSE("streamSSEResponse started, format:", format);
805
+ const reader = response.body?.getReader();
806
+ if (!reader) {
807
+ callbacks.onError?.("No response body");
808
+ return { content: "", reasoning: "", blocks: [], audios: [], videos: [] };
809
+ }
810
+ const decoder = new TextDecoder();
811
+ const parser = createSSEParser(format, callbacks);
812
+ let chunkCount = 0;
813
+ try {
814
+ while (true) {
815
+ const { done, value } = await reader.read();
816
+ if (done) {
817
+ debugSSE("Reader done signal received after", chunkCount, "chunks");
818
+ break;
819
+ }
820
+ const chunk = decoder.decode(value, { stream: true });
821
+ chunkCount++;
822
+ parser.push(chunk);
823
+ if (parser.isDone()) {
824
+ debugSSE("Parser isDone after", chunkCount, "chunks");
825
+ break;
826
+ }
827
+ }
828
+ parser.flush();
829
+ const result = parser.getResult();
830
+ debugSSE("streamSSEResponse complete, content length:", result.content.length, "completionTokens:", result.usage?.completionTokens, "usage:", result.usage);
831
+ return result;
832
+ } finally {
833
+ reader.releaseLock();
834
+ }
835
+ }
836
+
837
+ // src/provider-proxy/ProviderProxy.ts
838
+ var import_node_http = __toESM(require("http"), 1);
839
+
840
+ // src/provider-proxy/providerProxyRouteMap.ts
841
+ var import_node_crypto = require("crypto");
842
+ var DEFAULT_ROUTE_IDLE_MS = 10 * 60 * 1e3;
843
+
844
+ // src/pipeline/resolveProviderChain.ts
845
+ async function resolveProviderChain(llmConfig, providerId, model) {
846
+ const cachedChain = await llmConfig.resolveTransformerChain(providerId, model);
847
+ const mainTransformer = await llmConfig.getMainTransformer(providerId);
848
+ const chain = {
849
+ providerTransformers: [...cachedChain.providerTransformers],
850
+ modelTransformers: [...cachedChain.modelTransformers]
851
+ };
852
+ if (mainTransformer) {
853
+ const alreadyInChain = chain.providerTransformers.some(
854
+ (t) => t.name === mainTransformer.name
855
+ );
856
+ if (!alreadyInChain) {
857
+ chain.providerTransformers.unshift(mainTransformer);
858
+ }
859
+ }
860
+ const hasTransformers = chain.providerTransformers.length > 0 || chain.modelTransformers.length > 0;
861
+ return { chain, mainTransformer, hasTransformers };
862
+ }
863
+
864
+ // src/transformer/anthropicBetaInject.ts
865
+ var import_extended_context = require("@omnicross/contracts/extended-context");
866
+ var EXTENDED_CONTEXT_BETA = "context-1m-2025-08-07";
867
+ var ANTHROPIC_BETA_HEADER = "anthropic-beta";
868
+ function injectExtendedContextBeta(headers, model, useExtendedContext) {
869
+ if (!useExtendedContext) return;
870
+ if (!(0, import_extended_context.isExtendedContextCapable)(model)) return;
871
+ let existingValue = "";
872
+ for (const key of Object.keys(headers)) {
873
+ if (key.toLowerCase() === ANTHROPIC_BETA_HEADER) {
874
+ const v = headers[key];
875
+ if (typeof v === "string") existingValue = v;
876
+ if (key !== ANTHROPIC_BETA_HEADER) delete headers[key];
877
+ }
878
+ }
879
+ const parts = existingValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
880
+ if (!parts.includes(EXTENDED_CONTEXT_BETA)) {
881
+ parts.push(EXTENDED_CONTEXT_BETA);
882
+ }
883
+ headers[ANTHROPIC_BETA_HEADER] = parts.join(",");
884
+ }
885
+
886
+ // src/pipeline/executeProviderCall.ts
887
+ async function executeProviderCall(ctx) {
888
+ const {
889
+ executor,
890
+ request,
891
+ provider,
892
+ chain,
893
+ endpointTransformer,
894
+ extendedContext,
895
+ resolveUrl,
896
+ buildHeaders,
897
+ prepareBody,
898
+ fetchFn,
899
+ runResponseChain = false,
900
+ responseChainRequest
901
+ } = ctx;
902
+ const { requestBody, config } = await executor.executeRequestChain(
903
+ request,
904
+ provider,
905
+ chain,
906
+ { endpointTransformer, extendedContext }
907
+ );
908
+ const finalBody = prepareBody ? await prepareBody(requestBody, config) : requestBody;
909
+ const url = resolveUrl(config);
910
+ const headers = buildHeaders(config);
911
+ const fetched = await fetchFn(url, headers, finalBody);
912
+ let response = fetched;
913
+ if (runResponseChain) {
914
+ response = await executor.executeResponseChain(
915
+ // Preserve the EXACT first arg each site passed: proxy → requestBody,
916
+ // unified ingresses → their pre-transform request. The caller supplies
917
+ // it via `responseChainRequest`; fall back to `requestBody` (the proxy
918
+ // shape) so the contract is never silently `undefined`.
919
+ responseChainRequest ?? requestBody,
920
+ fetched,
921
+ provider,
922
+ chain,
923
+ { endpointTransformer }
924
+ );
925
+ }
926
+ return { response, requestBody, finalBody, config, url, headers };
927
+ }
928
+
929
+ // src/transformer/TransformerChainExecutor.ts
930
+ var defaultLogger = {
931
+ debug: (msg, ...args) => console.debug(`[ChainExecutor] ${msg}`, ...args),
932
+ info: (msg, ...args) => console.info(`[ChainExecutor] ${msg}`, ...args),
933
+ warn: (msg, ...args) => console.warn(`[ChainExecutor] ${msg}`, ...args),
934
+ error: (msg, ...args) => console.error(`[ChainExecutor] ${msg}`, ...args)
935
+ };
936
+ var TransformerChainExecutor = class {
937
+ logger;
938
+ constructor(logger) {
939
+ this.logger = logger ?? defaultLogger;
940
+ }
941
+ /**
942
+ * Execute the request transformation chain
943
+ *
944
+ * @param request - Original request body
945
+ * @param provider - LLM provider configuration
946
+ * @param chain - Resolved transformer chain
947
+ * @param options - Execution options
948
+ * @returns Transformed request result
949
+ */
950
+ async executeRequestChain(request, provider, chain, options = {}) {
951
+ const { endpointTransformer, headers, extendedContext } = options;
952
+ const context = {
953
+ logger: this.logger,
954
+ providerName: provider.name
955
+ };
956
+ let requestBody = request;
957
+ let config = {};
958
+ let bypass = false;
959
+ bypass = this.shouldBypassTransformers(
960
+ chain,
961
+ endpointTransformer,
962
+ requestBody
963
+ );
964
+ if (bypass) {
965
+ if (headers) {
966
+ const cleanHeaders = this.cleanHeaders(headers);
967
+ config.headers = cleanHeaders;
968
+ }
969
+ this.logger.debug("Bypass mode enabled - skipping transformations");
970
+ }
971
+ if (!bypass && endpointTransformer?.transformRequestOut) {
972
+ this.logger.debug("Executing transformRequestOut");
973
+ try {
974
+ const transformOut = await endpointTransformer.transformRequestOut(requestBody, context);
975
+ if (transformOut && typeof transformOut === "object") {
976
+ if ("body" in transformOut) {
977
+ requestBody = transformOut.body;
978
+ config = transformOut.config ?? {};
979
+ } else {
980
+ requestBody = transformOut;
981
+ }
982
+ }
983
+ } catch (error) {
984
+ this.logger.error(`transformRequestOut error: ${this.getErrorMessage(error)}`);
985
+ throw error;
986
+ }
987
+ }
988
+ if (!bypass && chain.providerTransformers.length > 0) {
989
+ this.logger.debug(`Executing ${chain.providerTransformers.length} provider transformers`);
990
+ for (const transformer of chain.providerTransformers) {
991
+ if (transformer.transformRequestIn) {
992
+ try {
993
+ const transformIn = await transformer.transformRequestIn(
994
+ requestBody,
995
+ provider,
996
+ context
997
+ );
998
+ if (transformIn && typeof transformIn === "object") {
999
+ if ("body" in transformIn) {
1000
+ requestBody = transformIn.body;
1001
+ config = { ...config, ...transformIn.config };
1002
+ } else {
1003
+ requestBody = transformIn;
1004
+ }
1005
+ }
1006
+ } catch (error) {
1007
+ this.logger.error(
1008
+ `Provider transformer ${transformer.name} error: ${this.getErrorMessage(error)}`
1009
+ );
1010
+ throw error;
1011
+ }
1012
+ }
1013
+ }
1014
+ }
1015
+ if (!bypass && chain.modelTransformers.length > 0) {
1016
+ this.logger.debug(`Executing ${chain.modelTransformers.length} model transformers`);
1017
+ for (const transformer of chain.modelTransformers) {
1018
+ if (transformer.transformRequestIn) {
1019
+ try {
1020
+ const result = await transformer.transformRequestIn(
1021
+ requestBody,
1022
+ provider,
1023
+ context
1024
+ );
1025
+ requestBody = result;
1026
+ } catch (error) {
1027
+ this.logger.error(
1028
+ `Model transformer ${transformer.name} error: ${this.getErrorMessage(error)}`
1029
+ );
1030
+ throw error;
1031
+ }
1032
+ }
1033
+ }
1034
+ }
1035
+ if (extendedContext?.enabled) {
1036
+ if (!config.headers || typeof config.headers !== "object") {
1037
+ config.headers = {};
1038
+ }
1039
+ injectExtendedContextBeta(
1040
+ config.headers,
1041
+ extendedContext.model,
1042
+ true
1043
+ );
1044
+ }
1045
+ return { requestBody, config, bypass };
1046
+ }
1047
+ /**
1048
+ * Execute the response transformation chain
1049
+ *
1050
+ * @param request - Original request (for context)
1051
+ * @param response - Response from provider
1052
+ * @param provider - LLM provider configuration
1053
+ * @param chain - Resolved transformer chain
1054
+ * @param options - Execution options
1055
+ * @returns Transformed response
1056
+ */
1057
+ async executeResponseChain(request, response, provider, chain, options = {}) {
1058
+ const { endpointTransformer } = options;
1059
+ const context = {
1060
+ logger: this.logger,
1061
+ providerName: provider.name
1062
+ };
1063
+ let finalResponse = response;
1064
+ const bypass = this.shouldBypassTransformers(chain, endpointTransformer, request);
1065
+ if (bypass) {
1066
+ this.logger.debug("Bypass mode - skipping response transformations");
1067
+ return finalResponse;
1068
+ }
1069
+ if (chain.modelTransformers.length > 0) {
1070
+ const reversedModelTransformers = [...chain.modelTransformers].reverse();
1071
+ this.logger.debug(
1072
+ `Executing ${reversedModelTransformers.length} model response transformers (reversed)`
1073
+ );
1074
+ for (const transformer of reversedModelTransformers) {
1075
+ if (transformer.transformResponseOut) {
1076
+ try {
1077
+ finalResponse = await transformer.transformResponseOut(finalResponse, context);
1078
+ } catch (error) {
1079
+ this.logger.error(
1080
+ `Model transformer ${transformer.name} response error: ${this.getErrorMessage(error)}`
1081
+ );
1082
+ throw error;
1083
+ }
1084
+ }
1085
+ }
1086
+ }
1087
+ if (chain.providerTransformers.length > 0) {
1088
+ const reversedProviderTransformers = [...chain.providerTransformers].reverse();
1089
+ this.logger.debug(
1090
+ `Executing ${reversedProviderTransformers.length} provider response transformers (reversed)`
1091
+ );
1092
+ for (const transformer of reversedProviderTransformers) {
1093
+ if (transformer.transformResponseOut) {
1094
+ try {
1095
+ finalResponse = await transformer.transformResponseOut(finalResponse, context);
1096
+ } catch (error) {
1097
+ this.logger.error(
1098
+ `Provider transformer ${transformer.name} response error: ${this.getErrorMessage(error)}`
1099
+ );
1100
+ throw error;
1101
+ }
1102
+ }
1103
+ }
1104
+ }
1105
+ if (endpointTransformer?.transformResponseIn) {
1106
+ this.logger.debug("Executing transformResponseIn");
1107
+ try {
1108
+ finalResponse = await endpointTransformer.transformResponseIn(finalResponse, context);
1109
+ } catch (error) {
1110
+ this.logger.error(`transformResponseIn error: ${this.getErrorMessage(error)}`);
1111
+ throw error;
1112
+ }
1113
+ }
1114
+ return finalResponse;
1115
+ }
1116
+ /**
1117
+ * Execute authentication handler if available
1118
+ *
1119
+ * @param request - Request body
1120
+ * @param provider - LLM provider
1121
+ * @param endpointTransformer - Endpoint transformer with auth handler
1122
+ * @param context - Transformer context
1123
+ * @returns Auth result with potentially modified request and config
1124
+ */
1125
+ async executeAuth(request, provider, endpointTransformer, context) {
1126
+ let requestBody = request;
1127
+ let config = {};
1128
+ if (endpointTransformer?.auth) {
1129
+ this.logger.debug("Executing auth handler");
1130
+ try {
1131
+ const auth = await endpointTransformer.auth(requestBody, provider, context);
1132
+ if (auth && typeof auth === "object") {
1133
+ if ("body" in auth) {
1134
+ requestBody = auth.body;
1135
+ const authConfig = auth.config;
1136
+ if (authConfig) {
1137
+ const headers = { ...config.headers ?? {}, ...authConfig.headers ?? {} };
1138
+ delete headers["host"];
1139
+ config = { ...config, ...authConfig, headers };
1140
+ }
1141
+ } else {
1142
+ requestBody = auth;
1143
+ }
1144
+ }
1145
+ } catch (error) {
1146
+ this.logger.error(`Auth handler error: ${this.getErrorMessage(error)}`);
1147
+ throw error;
1148
+ }
1149
+ }
1150
+ return { requestBody, config };
1151
+ }
1152
+ /**
1153
+ * Check if transformers should be bypassed (optimization)
1154
+ *
1155
+ * Bypass is enabled when:
1156
+ * - Provider has only one transformer that matches the endpoint transformer
1157
+ * - Model has no specific transformers or only the same endpoint transformer
1158
+ */
1159
+ shouldBypassTransformers(chain, endpointTransformer, _request) {
1160
+ if (!endpointTransformer?.name) {
1161
+ return false;
1162
+ }
1163
+ const providerHasOnlyEndpoint = chain.providerTransformers.length === 1 && chain.providerTransformers[0]?.name === endpointTransformer.name;
1164
+ const modelHasNoTransformers = chain.modelTransformers.length === 0;
1165
+ const modelHasOnlyEndpoint = chain.modelTransformers.length === 1 && chain.modelTransformers[0]?.name === endpointTransformer.name;
1166
+ return providerHasOnlyEndpoint && (modelHasNoTransformers || modelHasOnlyEndpoint);
1167
+ }
1168
+ /**
1169
+ * Clean headers for pass-through
1170
+ */
1171
+ cleanHeaders(headers) {
1172
+ const result = {};
1173
+ if (headers instanceof Headers) {
1174
+ headers.forEach((value, key) => {
1175
+ if (key.toLowerCase() !== "content-length") {
1176
+ result[key] = value;
1177
+ }
1178
+ });
1179
+ } else {
1180
+ for (const [key, value] of Object.entries(headers)) {
1181
+ if (key.toLowerCase() !== "content-length") {
1182
+ result[key] = value;
1183
+ }
1184
+ }
1185
+ }
1186
+ return result;
1187
+ }
1188
+ /**
1189
+ * Get error message from unknown error
1190
+ */
1191
+ getErrorMessage(error) {
1192
+ if (error instanceof Error) {
1193
+ return error.message;
1194
+ }
1195
+ return String(error);
1196
+ }
1197
+ };
1198
+
1199
+ // src/transformer/transformers/ReasoningTransformer.ts
1200
+ var import_thinking_config = require("@omnicross/contracts/thinking-config");
1201
+
1202
+ // src/outbound-api/OutboundApiServer.ts
1203
+ var import_node_http2 = __toESM(require("http"), 1);
1204
+ var import_node_os = require("os");
1205
+
1206
+ // src/outbound-api/outboundApiRouter.ts
1207
+ var import_node_stream = require("stream");
1208
+
1209
+ // src/outbound-api/outboundApiKeyAuth.ts
1210
+ var import_node_crypto2 = require("crypto");
1211
+
1212
+ // src/outbound-api/roleDetection.ts
1213
+ var import_canonical_models = require("@omnicross/contracts/canonical-models");
1214
+
1215
+ // src/outbound-api/subscriptionSupport.ts
1216
+ var SUBSCRIPTION_PROVIDER_IDS = [
1217
+ "claude",
1218
+ "codex",
1219
+ "gemini",
1220
+ "opencodego"
1221
+ ];
1222
+ var SUBSCRIPTION_ID_SET = new Set(SUBSCRIPTION_PROVIDER_IDS);
1223
+
1224
+ // src/api-converter/shared.ts
1225
+ function convertOpenAITool(tool) {
1226
+ return {
1227
+ name: tool.function.name,
1228
+ description: tool.function.description,
1229
+ input_schema: tool.function.parameters
1230
+ };
1231
+ }
1232
+ function mapAnthropicStopReason(stopReason) {
1233
+ switch (stopReason) {
1234
+ case "end_turn":
1235
+ return "stop";
1236
+ case "max_tokens":
1237
+ return "length";
1238
+ case "tool_use":
1239
+ return "tool_calls";
1240
+ case "stop_sequence":
1241
+ return "stop";
1242
+ default:
1243
+ return "stop";
1244
+ }
1245
+ }
1246
+
1247
+ // src/api-converter/anthropic-to-openai.ts
1248
+ function convertAnthropicToOpenAI(response) {
1249
+ const content = [];
1250
+ const toolCalls = [];
1251
+ let reasoningContent = "";
1252
+ for (const block of response.content) {
1253
+ if (block.type === "text") {
1254
+ content.push(block.text);
1255
+ } else if (block.type === "tool_use") {
1256
+ toolCalls.push({
1257
+ id: block.id,
1258
+ type: "function",
1259
+ function: {
1260
+ name: block.name,
1261
+ arguments: JSON.stringify(block.input)
1262
+ }
1263
+ });
1264
+ } else if (block.type === "thinking") {
1265
+ reasoningContent += block.thinking;
1266
+ }
1267
+ }
1268
+ const finishReason = mapAnthropicStopReason(response.stop_reason);
1269
+ const result = {
1270
+ id: response.id,
1271
+ object: "chat.completion",
1272
+ created: Math.floor(Date.now() / 1e3),
1273
+ model: response.model,
1274
+ choices: [
1275
+ {
1276
+ index: 0,
1277
+ message: {
1278
+ role: "assistant",
1279
+ content: content.join("\n") || null,
1280
+ ...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
1281
+ },
1282
+ finish_reason: finishReason
1283
+ }
1284
+ ],
1285
+ usage: {
1286
+ prompt_tokens: response.usage.input_tokens,
1287
+ completion_tokens: response.usage.output_tokens,
1288
+ total_tokens: response.usage.input_tokens + response.usage.output_tokens
1289
+ }
1290
+ };
1291
+ if (reasoningContent) {
1292
+ result.reasoning_content = reasoningContent;
1293
+ }
1294
+ return result;
1295
+ }
1296
+
1297
+ // src/api-converter/openai-to-anthropic.ts
1298
+ function convertOpenAIToAnthropic(request, config) {
1299
+ const messages = [];
1300
+ let system;
1301
+ for (const msg of request.messages) {
1302
+ if (msg.role === "system") {
1303
+ if (typeof msg.content === "string") {
1304
+ system = msg.content;
1305
+ } else if (Array.isArray(msg.content)) {
1306
+ system = msg.content.filter((p) => p.type === "text").map((p) => ({ type: "text", text: p.text || "" }));
1307
+ }
1308
+ continue;
1309
+ }
1310
+ if (msg.role === "tool" && msg.tool_call_id) {
1311
+ const toolResult = {
1312
+ type: "tool_result",
1313
+ tool_use_id: msg.tool_call_id,
1314
+ content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
1315
+ };
1316
+ const lastMsg = messages[messages.length - 1];
1317
+ if (lastMsg && lastMsg.role === "user" && Array.isArray(lastMsg.content)) {
1318
+ lastMsg.content.push(toolResult);
1319
+ } else {
1320
+ messages.push({ role: "user", content: [toolResult] });
1321
+ }
1322
+ continue;
1323
+ }
1324
+ if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
1325
+ const content = [];
1326
+ if (msg.content) {
1327
+ if (typeof msg.content === "string") {
1328
+ content.push({ type: "text", text: msg.content });
1329
+ } else if (Array.isArray(msg.content)) {
1330
+ for (const part of msg.content) {
1331
+ if (part.type === "text" && part.text) {
1332
+ content.push({ type: "text", text: part.text });
1333
+ }
1334
+ }
1335
+ }
1336
+ }
1337
+ for (const tc of msg.tool_calls) {
1338
+ content.push({
1339
+ type: "tool_use",
1340
+ id: tc.id,
1341
+ name: tc.function.name,
1342
+ input: JSON.parse(tc.function.arguments || "{}")
1343
+ });
1344
+ }
1345
+ messages.push({ role: "assistant", content });
1346
+ continue;
1347
+ }
1348
+ if (msg.role === "user" || msg.role === "assistant") {
1349
+ const content = convertOpenAIMessageContent(msg);
1350
+ messages.push({ role: msg.role, content });
1351
+ }
1352
+ }
1353
+ let model = request.model;
1354
+ if (config.modelMapping && config.modelMapping[model]) {
1355
+ model = config.modelMapping[model];
1356
+ } else if (!model.startsWith("claude")) {
1357
+ model = config.defaultModel;
1358
+ }
1359
+ const result = {
1360
+ model,
1361
+ // Anthropic requires max_tokens; use high default (16384) if not explicitly set
1362
+ // This prevents output truncation for long responses
1363
+ max_tokens: request.max_tokens || 16384,
1364
+ messages,
1365
+ stream: request.stream
1366
+ };
1367
+ if (system) {
1368
+ result.system = system;
1369
+ }
1370
+ if (request.tools && request.tools.length > 0) {
1371
+ result.tools = request.tools.map(convertOpenAITool);
1372
+ if (request.tool_choice) {
1373
+ if (request.tool_choice === "auto") {
1374
+ result.tool_choice = { type: "auto" };
1375
+ } else if (request.tool_choice === "required") {
1376
+ result.tool_choice = { type: "any" };
1377
+ } else if (typeof request.tool_choice === "object") {
1378
+ result.tool_choice = {
1379
+ type: "tool",
1380
+ name: request.tool_choice.function.name
1381
+ };
1382
+ }
1383
+ }
1384
+ }
1385
+ return result;
1386
+ }
1387
+ function convertOpenAIMessageContent(msg) {
1388
+ if (typeof msg.content === "string") {
1389
+ return msg.content;
1390
+ }
1391
+ if (!msg.content || !Array.isArray(msg.content)) {
1392
+ return "";
1393
+ }
1394
+ const parts = [];
1395
+ for (const part of msg.content) {
1396
+ if (part.type === "text" && part.text) {
1397
+ parts.push({ type: "text", text: part.text });
1398
+ } else if (part.type === "image_url" && part.image_url) {
1399
+ const url = part.image_url.url;
1400
+ if (url.startsWith("data:")) {
1401
+ const match = url.match(/^data:([^;]+);base64,(.+)$/);
1402
+ if (match) {
1403
+ parts.push({
1404
+ type: "image",
1405
+ source: {
1406
+ type: "base64",
1407
+ media_type: match[1],
1408
+ data: match[2]
1409
+ }
1410
+ });
1411
+ }
1412
+ } else {
1413
+ parts.push({
1414
+ type: "image",
1415
+ source: { type: "url", url }
1416
+ });
1417
+ }
1418
+ }
1419
+ }
1420
+ return parts.length > 0 ? parts : "";
1421
+ }
1422
+
1423
+ // src/completion/DirectApiHandler.ts
1424
+ async function callOpenAICompletion(provider, apiKey, options, logger) {
1425
+ const request = {
1426
+ model: options.model,
1427
+ messages: options.messages.map((m) => convertMessageToOpenAI(m)),
1428
+ // Only set max_tokens if explicitly provided, otherwise let API use its default
1429
+ ...options.maxTokens ? { max_tokens: options.maxTokens } : {},
1430
+ temperature: options.temperature,
1431
+ stream: false
1432
+ // For now, non-streaming only
1433
+ };
1434
+ if (options.thinkLevel && options.thinkLevel !== "none") {
1435
+ const effort = (0, import_thinking_config2.getOpenAIReasoningEffort)(options.thinkLevel);
1436
+ if (effort) {
1437
+ request.reasoning_effort = effort;
1438
+ }
1439
+ }
1440
+ const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: false });
1441
+ const headers = getProviderHeaders(provider, apiKey);
1442
+ logger.info("Calling OpenAI completion API", { url: apiUrl, model: options.model });
1443
+ const response = await fetch(apiUrl, {
1444
+ method: "POST",
1445
+ headers,
1446
+ body: JSON.stringify(request)
1447
+ });
1448
+ if (!response.ok) {
1449
+ const errorText = await response.text();
1450
+ return {
1451
+ success: false,
1452
+ error: `API error (${response.status}): ${errorText}`
1453
+ };
1454
+ }
1455
+ const data = await response.json();
1456
+ const choice = data.choices[0];
1457
+ return {
1458
+ success: true,
1459
+ message: {
1460
+ id: `msg_${Date.now()}`,
1461
+ role: "assistant",
1462
+ content: choice.message.content || "",
1463
+ timestamp: Date.now(),
1464
+ thinking: data.reasoning_content ? { content: data.reasoning_content } : void 0
1465
+ },
1466
+ usage: {
1467
+ promptTokens: data.usage.prompt_tokens,
1468
+ completionTokens: data.usage.completion_tokens,
1469
+ totalTokens: data.usage.total_tokens
1470
+ }
1471
+ };
1472
+ }
1473
+ async function callAnthropicCompletion(provider, apiKey, options, logger) {
1474
+ const hasImages = options.messages.some((m) => m.images && m.images.length > 0);
1475
+ if (hasImages) {
1476
+ const systemMessages = options.messages.filter((m) => m.role === "system");
1477
+ const nonSystemMessages = options.messages.filter((m) => m.role !== "system");
1478
+ const anthropicRequest2 = {
1479
+ model: options.model,
1480
+ max_tokens: options.maxTokens || 16384,
1481
+ temperature: options.temperature,
1482
+ ...systemMessages.length > 0 ? { system: systemMessages.map((m) => m.content).join("\n\n") } : {},
1483
+ messages: nonSystemMessages.map((m) => convertMessageToAnthropic(m)),
1484
+ stream: false
1485
+ };
1486
+ const apiUrl2 = buildProviderApiUrl(provider, { model: options.model, stream: false });
1487
+ const headers2 = getProviderHeaders(provider, apiKey);
1488
+ logger.info("Calling Anthropic completion API with images", { url: apiUrl2, model: options.model });
1489
+ const response2 = await fetch(apiUrl2, {
1490
+ method: "POST",
1491
+ headers: headers2,
1492
+ body: JSON.stringify(anthropicRequest2)
1493
+ });
1494
+ if (!response2.ok) {
1495
+ const errorText = await response2.text();
1496
+ return {
1497
+ success: false,
1498
+ error: `API error (${response2.status}): ${errorText}`
1499
+ };
1500
+ }
1501
+ const anthropicResponse2 = await response2.json();
1502
+ const openaiResponse2 = convertAnthropicToOpenAI(anthropicResponse2);
1503
+ const choice2 = openaiResponse2.choices[0];
1504
+ return {
1505
+ success: true,
1506
+ message: {
1507
+ id: `assistant-${Date.now()}`,
1508
+ role: "assistant",
1509
+ content: choice2.message?.content || "",
1510
+ timestamp: Date.now()
1511
+ },
1512
+ usage: openaiResponse2.usage ? {
1513
+ promptTokens: openaiResponse2.usage.prompt_tokens,
1514
+ completionTokens: openaiResponse2.usage.completion_tokens,
1515
+ totalTokens: openaiResponse2.usage.total_tokens
1516
+ } : void 0
1517
+ };
1518
+ }
1519
+ const openaiRequest = {
1520
+ model: options.model,
1521
+ messages: options.messages.map((m) => ({
1522
+ role: m.role,
1523
+ content: m.content
1524
+ })),
1525
+ // Anthropic requires max_tokens; use 16384 default if not explicitly set
1526
+ max_tokens: options.maxTokens || 16384,
1527
+ temperature: options.temperature,
1528
+ stream: false
1529
+ };
1530
+ const config = {
1531
+ defaultModel: options.model
1532
+ };
1533
+ const anthropicRequest = convertOpenAIToAnthropic(openaiRequest, config);
1534
+ const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: false });
1535
+ const headers = getProviderHeaders(provider, apiKey);
1536
+ logger.info("Calling Anthropic completion API", { url: apiUrl, model: options.model });
1537
+ const response = await fetch(apiUrl, {
1538
+ method: "POST",
1539
+ headers,
1540
+ body: JSON.stringify(anthropicRequest)
1541
+ });
1542
+ if (!response.ok) {
1543
+ const errorText = await response.text();
1544
+ return {
1545
+ success: false,
1546
+ error: `API error (${response.status}): ${errorText}`
1547
+ };
1548
+ }
1549
+ const anthropicResponse = await response.json();
1550
+ const openaiResponse = convertAnthropicToOpenAI(anthropicResponse);
1551
+ const choice = openaiResponse.choices[0];
1552
+ return {
1553
+ success: true,
1554
+ message: {
1555
+ id: `msg_${Date.now()}`,
1556
+ role: "assistant",
1557
+ content: choice.message.content || "",
1558
+ timestamp: Date.now(),
1559
+ thinking: openaiResponse.reasoning_content ? { content: openaiResponse.reasoning_content } : void 0
1560
+ },
1561
+ usage: {
1562
+ promptTokens: openaiResponse.usage.prompt_tokens,
1563
+ completionTokens: openaiResponse.usage.completion_tokens,
1564
+ totalTokens: openaiResponse.usage.total_tokens
1565
+ }
1566
+ };
1567
+ }
1568
+ async function callGeminiCompletion(provider, apiKey, options, logger) {
1569
+ const contents = [];
1570
+ let systemInstruction;
1571
+ for (const msg of options.messages) {
1572
+ if (msg.role === "system") {
1573
+ systemInstruction = { parts: [{ text: msg.content }] };
1574
+ } else {
1575
+ contents.push(convertMessageToGemini(msg));
1576
+ }
1577
+ }
1578
+ const request = {
1579
+ contents,
1580
+ generationConfig: {
1581
+ ...options.maxTokens ? { maxOutputTokens: options.maxTokens } : {},
1582
+ ...options.temperature !== void 0 ? { temperature: options.temperature } : {}
1583
+ }
1584
+ };
1585
+ if (systemInstruction) {
1586
+ request.systemInstruction = systemInstruction;
1587
+ }
1588
+ const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: false });
1589
+ const headers = getProviderHeaders(provider, apiKey);
1590
+ logger.info("Calling Gemini completion API", { url: apiUrl, model: options.model });
1591
+ const response = await fetch(apiUrl, {
1592
+ method: "POST",
1593
+ headers,
1594
+ body: JSON.stringify(request)
1595
+ });
1596
+ if (!response.ok) {
1597
+ const errorText = await response.text();
1598
+ return {
1599
+ success: false,
1600
+ error: `API error (${response.status}): ${errorText}`
1601
+ };
1602
+ }
1603
+ const data = await response.json();
1604
+ const candidates = data.candidates;
1605
+ if (!candidates || candidates.length === 0) {
1606
+ return {
1607
+ success: false,
1608
+ error: "No candidates in response"
1609
+ };
1610
+ }
1611
+ const candidate = candidates[0];
1612
+ const content = candidate.content;
1613
+ let textContent = "";
1614
+ if (content?.parts) {
1615
+ for (const part of content.parts) {
1616
+ if (part.text) {
1617
+ textContent += part.text;
1618
+ }
1619
+ }
1620
+ }
1621
+ const usage = data.usageMetadata;
1622
+ return {
1623
+ success: true,
1624
+ message: {
1625
+ id: `msg_${Date.now()}`,
1626
+ role: "assistant",
1627
+ content: textContent,
1628
+ timestamp: Date.now()
1629
+ },
1630
+ usage: usage ? {
1631
+ promptTokens: usage.promptTokenCount || 0,
1632
+ completionTokens: usage.candidatesTokenCount || 0,
1633
+ totalTokens: usage.totalTokenCount || 0
1634
+ } : void 0
1635
+ };
1636
+ }
1637
+ async function callOpenAIResponseCompletion(provider, apiKey, options, logger) {
1638
+ const input = [];
1639
+ for (const msg of options.messages) {
1640
+ if (msg.role === "system") {
1641
+ input.push({ role: "developer", content: msg.content });
1642
+ } else {
1643
+ input.push({ role: msg.role, content: msg.content });
1644
+ }
1645
+ }
1646
+ const request = {
1647
+ model: options.model,
1648
+ input,
1649
+ stream: false,
1650
+ ...options.maxTokens ? { max_output_tokens: options.maxTokens } : {},
1651
+ ...options.temperature !== void 0 ? { temperature: options.temperature } : {}
1652
+ };
1653
+ if (options.thinkLevel && options.thinkLevel !== "none") {
1654
+ const effort = (0, import_thinking_config2.getOpenAIReasoningEffort)(options.thinkLevel);
1655
+ if (effort) {
1656
+ request.reasoning = { effort, summary: "auto" };
1657
+ }
1658
+ }
1659
+ const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: false });
1660
+ const headers = getProviderHeaders(provider, apiKey);
1661
+ logger.info("Calling OpenAI Response API", { url: apiUrl, model: options.model });
1662
+ const response = await fetch(apiUrl, {
1663
+ method: "POST",
1664
+ headers,
1665
+ body: JSON.stringify(request)
1666
+ });
1667
+ if (!response.ok) {
1668
+ const errorText = await response.text();
1669
+ return {
1670
+ success: false,
1671
+ error: `API error (${response.status}): ${errorText}`
1672
+ };
1673
+ }
1674
+ const data = await response.json();
1675
+ let textContent = "";
1676
+ const output = data.output;
1677
+ if (output) {
1678
+ for (const item of output) {
1679
+ if (item.type === "message") {
1680
+ const content = item.content;
1681
+ if (content) {
1682
+ for (const part of content) {
1683
+ if (part.type === "output_text" && typeof part.text === "string") {
1684
+ textContent += part.text;
1685
+ }
1686
+ }
1687
+ }
1688
+ }
1689
+ }
1690
+ }
1691
+ const usage = data.usage;
1692
+ return {
1693
+ success: true,
1694
+ message: {
1695
+ id: `msg_${Date.now()}`,
1696
+ role: "assistant",
1697
+ content: textContent,
1698
+ timestamp: Date.now()
1699
+ },
1700
+ usage: usage ? {
1701
+ promptTokens: usage.input_tokens || 0,
1702
+ completionTokens: usage.output_tokens || 0,
1703
+ totalTokens: (usage.input_tokens || 0) + (usage.output_tokens || 0)
1704
+ } : void 0
1705
+ };
1706
+ }
1707
+
1708
+ // src/completion/StreamHandler.ts
1709
+ var import_thinking_config3 = require("@omnicross/contracts/thinking-config");
1710
+ async function streamOpenAICompletion(provider, apiKey, options, messageId, callbacks, logger) {
1711
+ const request = {
1712
+ model: options.model,
1713
+ messages: options.messages.map((m) => convertMessageToOpenAI(m)),
1714
+ // Only set max_tokens if explicitly provided, otherwise let API use its default
1715
+ ...options.maxTokens ? { max_tokens: options.maxTokens } : {},
1716
+ temperature: options.temperature,
1717
+ stream: true
1718
+ };
1719
+ if (options.thinkLevel && options.thinkLevel !== "none") {
1720
+ const effort = (0, import_thinking_config3.getOpenAIReasoningEffort)(options.thinkLevel);
1721
+ if (effort) {
1722
+ request.reasoning_effort = effort;
1723
+ }
1724
+ }
1725
+ const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: true });
1726
+ const headers = getProviderHeaders(provider, apiKey);
1727
+ if (options.nativeSearchAugmentation) {
1728
+ applyAugmentation(request, options.nativeSearchAugmentation);
1729
+ }
1730
+ logger.info("Streaming OpenAI completion request", {
1731
+ url: apiUrl,
1732
+ providerId: options.providerId,
1733
+ model: options.model,
1734
+ maxTokens: options.maxTokens,
1735
+ max_tokens_in_request: request.max_tokens,
1736
+ temperature: options.temperature,
1737
+ messagesCount: options.messages.length,
1738
+ hasNativeSearchAugmentation: !!options.nativeSearchAugmentation
1739
+ });
1740
+ const response = await fetch(apiUrl, {
1741
+ method: "POST",
1742
+ headers,
1743
+ body: JSON.stringify(request)
1744
+ });
1745
+ if (!response.ok) {
1746
+ const errorText = await response.text();
1747
+ callbacks.onError?.(`API error (${response.status}): ${errorText}`);
1748
+ return;
1749
+ }
1750
+ const result = await streamSSEResponse(response, "openai", {
1751
+ onDelta: callbacks.onDelta,
1752
+ onReasoning: callbacks.onReasoning,
1753
+ onAudio: callbacks.onAudio,
1754
+ onVideo: callbacks.onVideo,
1755
+ onError: callbacks.onError
1756
+ });
1757
+ callbacks.onDone?.(
1758
+ {
1759
+ id: messageId,
1760
+ role: "assistant",
1761
+ content: result.content,
1762
+ timestamp: Date.now(),
1763
+ thinking: result.reasoning ? { content: result.reasoning } : void 0,
1764
+ audios: result.audios.length > 0 ? result.audios : void 0,
1765
+ videos: result.videos.length > 0 ? result.videos : void 0
1766
+ },
1767
+ result.usage,
1768
+ result.metrics
1769
+ );
1770
+ }
1771
+ async function streamAnthropicCompletion(provider, apiKey, options, messageId, callbacks, logger) {
1772
+ const hasImages = options.messages.some((m) => m.images && m.images.length > 0);
1773
+ const MAX_TOKENS_FOR_THINKING = 16384;
1774
+ let effectiveMaxTokens = options.maxTokens || 16384;
1775
+ const thinkingMaxTokens = options.thinkLevel && options.thinkLevel !== "none" ? Math.min(effectiveMaxTokens, MAX_TOKENS_FOR_THINKING) : effectiveMaxTokens;
1776
+ const thinkingConfig = options.thinkLevel && options.thinkLevel !== "none" ? (0, import_thinking_config3.buildAnthropicThinking)(options.model, options.thinkLevel, thinkingMaxTokens) : void 0;
1777
+ if (thinkingConfig) {
1778
+ effectiveMaxTokens = thinkingMaxTokens;
1779
+ }
1780
+ logger.debug("Anthropic thinking configuration", {
1781
+ thinkLevel: options.thinkLevel,
1782
+ thinkingConfig,
1783
+ effectiveMaxTokens
1784
+ });
1785
+ let anthropicRequest;
1786
+ if (hasImages) {
1787
+ const systemMessages = options.messages.filter((m) => m.role === "system");
1788
+ const nonSystemMessages = options.messages.filter((m) => m.role !== "system");
1789
+ anthropicRequest = {
1790
+ model: options.model,
1791
+ max_tokens: effectiveMaxTokens,
1792
+ // Omit temperature when thinking is enabled (Anthropic will use default temperature=1)
1793
+ ...thinkingConfig ? {} : { temperature: options.temperature },
1794
+ ...systemMessages.length > 0 ? { system: systemMessages.map((m) => m.content).join("\n\n") } : {},
1795
+ messages: nonSystemMessages.map((m) => convertMessageToAnthropic(m)),
1796
+ stream: true,
1797
+ ...thinkingConfig ? { thinking: thinkingConfig } : {}
1798
+ };
1799
+ } else {
1800
+ const config = {
1801
+ defaultModel: options.model
1802
+ };
1803
+ const openaiRequest = {
1804
+ model: options.model,
1805
+ messages: options.messages.map((m) => ({
1806
+ role: m.role,
1807
+ content: m.content
1808
+ })),
1809
+ // Anthropic requires max_tokens; use adjusted value
1810
+ max_tokens: effectiveMaxTokens,
1811
+ // Omit temperature when thinking is enabled (Anthropic will use default temperature=1)
1812
+ temperature: thinkingConfig ? void 0 : options.temperature,
1813
+ stream: true
1814
+ };
1815
+ anthropicRequest = convertOpenAIToAnthropic(openaiRequest, config);
1816
+ if (thinkingConfig) {
1817
+ anthropicRequest.thinking = thinkingConfig;
1818
+ delete anthropicRequest.temperature;
1819
+ }
1820
+ }
1821
+ const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: true });
1822
+ const headers = getProviderHeaders(provider, apiKey);
1823
+ if (options.nativeSearchAugmentation) {
1824
+ applyAugmentation(anthropicRequest, options.nativeSearchAugmentation);
1825
+ }
1826
+ logger.info("Streaming Anthropic completion request", {
1827
+ url: apiUrl,
1828
+ model: anthropicRequest.model,
1829
+ messagesCount: anthropicRequest.messages?.length,
1830
+ max_tokens: anthropicRequest.max_tokens,
1831
+ temperature: anthropicRequest.temperature,
1832
+ stream: anthropicRequest.stream,
1833
+ hasImages,
1834
+ hasThinking: !!anthropicRequest.thinking,
1835
+ hasNativeSearchAugmentation: !!options.nativeSearchAugmentation
1836
+ });
1837
+ const response = await fetch(apiUrl, {
1838
+ method: "POST",
1839
+ headers,
1840
+ body: JSON.stringify(anthropicRequest)
1841
+ });
1842
+ logger.debug("Anthropic response status", { status: response.status });
1843
+ if (!response.ok) {
1844
+ const errorText = await response.text();
1845
+ logger.error("Anthropic API error", void 0, { status: response.status, errorText });
1846
+ callbacks.onError?.(`API error (${response.status}): ${errorText}`);
1847
+ return;
1848
+ }
1849
+ const result = await streamSSEResponse(response, "anthropic", {
1850
+ onDelta: callbacks.onDelta,
1851
+ onReasoning: callbacks.onReasoning,
1852
+ onError: callbacks.onError,
1853
+ onBlock: callbacks.onBlock
1854
+ });
1855
+ logger.info("Anthropic stream complete", {
1856
+ contentLength: result.content.length,
1857
+ reasoningLength: result.reasoning?.length || 0,
1858
+ blocksCount: result.blocks.length
1859
+ });
1860
+ callbacks.onDone?.(
1861
+ {
1862
+ id: messageId,
1863
+ role: "assistant",
1864
+ content: result.content,
1865
+ timestamp: Date.now(),
1866
+ thinking: result.reasoning ? { content: result.reasoning } : void 0,
1867
+ blocks: result.blocks.length > 0 ? result.blocks : void 0
1868
+ },
1869
+ result.usage,
1870
+ result.metrics
1871
+ );
1872
+ }
1873
+ async function streamGeminiCompletion(provider, apiKey, options, messageId, callbacks, logger) {
1874
+ const contents = [];
1875
+ let systemInstruction;
1876
+ for (const msg of options.messages) {
1877
+ if (msg.role === "system") {
1878
+ systemInstruction = { parts: [{ text: msg.content }] };
1879
+ } else {
1880
+ contents.push(convertMessageToGemini(msg));
1881
+ }
1882
+ }
1883
+ const request = {
1884
+ contents,
1885
+ generationConfig: {
1886
+ ...options.maxTokens ? { maxOutputTokens: options.maxTokens } : {},
1887
+ ...options.temperature !== void 0 ? { temperature: options.temperature } : {}
1888
+ }
1889
+ };
1890
+ if (systemInstruction) {
1891
+ request.systemInstruction = systemInstruction;
1892
+ }
1893
+ const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: true });
1894
+ const headers = getProviderHeaders(provider, apiKey);
1895
+ if (options.nativeSearchAugmentation) {
1896
+ applyAugmentation(request, options.nativeSearchAugmentation);
1897
+ }
1898
+ logger.info("Streaming Gemini completion request", {
1899
+ url: apiUrl,
1900
+ model: options.model,
1901
+ contentsCount: contents.length,
1902
+ hasSystemInstruction: !!systemInstruction,
1903
+ maxOutputTokens: options.maxTokens,
1904
+ temperature: options.temperature,
1905
+ hasNativeSearchAugmentation: !!options.nativeSearchAugmentation
1906
+ });
1907
+ const response = await fetch(apiUrl, {
1908
+ method: "POST",
1909
+ headers,
1910
+ body: JSON.stringify(request)
1911
+ });
1912
+ logger.debug("Gemini response status", { status: response.status });
1913
+ if (!response.ok) {
1914
+ const errorText = await response.text();
1915
+ logger.error("Gemini API error", void 0, { status: response.status, errorText });
1916
+ callbacks.onError?.(`API error (${response.status}): ${errorText}`);
1917
+ return;
1918
+ }
1919
+ const result = await streamSSEResponse(response, "gemini", {
1920
+ onDelta: callbacks.onDelta,
1921
+ onReasoning: callbacks.onReasoning,
1922
+ onError: callbacks.onError
1923
+ });
1924
+ logger.info("Gemini stream complete", { contentLength: result.content.length });
1925
+ callbacks.onDone?.(
1926
+ {
1927
+ id: messageId,
1928
+ role: "assistant",
1929
+ content: result.content,
1930
+ timestamp: Date.now(),
1931
+ thinking: result.reasoning ? { content: result.reasoning } : void 0
1932
+ },
1933
+ result.usage,
1934
+ result.metrics
1935
+ );
1936
+ }
1937
+ async function streamOpenAIResponseCompletion(provider, apiKey, options, messageId, callbacks, logger) {
1938
+ const input = [];
1939
+ for (const msg of options.messages) {
1940
+ if (msg.role === "system") {
1941
+ input.push({ role: "developer", content: msg.content });
1942
+ } else {
1943
+ input.push({ role: msg.role, content: msg.content });
1944
+ }
1945
+ }
1946
+ const request = {
1947
+ model: options.model,
1948
+ input,
1949
+ stream: true,
1950
+ ...options.maxTokens ? { max_output_tokens: options.maxTokens } : {},
1951
+ ...options.temperature !== void 0 ? { temperature: options.temperature } : {}
1952
+ };
1953
+ if (options.thinkLevel && options.thinkLevel !== "none") {
1954
+ const effort = (0, import_thinking_config3.getOpenAIReasoningEffort)(options.thinkLevel);
1955
+ if (effort) {
1956
+ request.reasoning = { effort, summary: "auto" };
1957
+ }
1958
+ }
1959
+ const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: true });
1960
+ const headers = getProviderHeaders(provider, apiKey);
1961
+ logger.info("Streaming OpenAI Response API request", {
1962
+ url: apiUrl,
1963
+ providerId: options.providerId,
1964
+ model: options.model,
1965
+ maxOutputTokens: options.maxTokens,
1966
+ temperature: options.temperature,
1967
+ inputCount: input.length,
1968
+ reasoning: request.reasoning
1969
+ });
1970
+ const response = await fetch(apiUrl, {
1971
+ method: "POST",
1972
+ headers,
1973
+ body: JSON.stringify(request)
1974
+ });
1975
+ if (!response.ok) {
1976
+ const errorText = await response.text();
1977
+ callbacks.onError?.(`API error (${response.status}): ${errorText}`);
1978
+ return;
1979
+ }
1980
+ const result = await streamSSEResponse(response, "openai-response", {
1981
+ onDelta: callbacks.onDelta,
1982
+ onReasoning: callbacks.onReasoning,
1983
+ onError: callbacks.onError
1984
+ });
1985
+ logger.info("OpenAI Response API stream complete", { contentLength: result.content.length });
1986
+ callbacks.onDone?.(
1987
+ {
1988
+ id: messageId,
1989
+ role: "assistant",
1990
+ content: result.content,
1991
+ timestamp: Date.now(),
1992
+ thinking: result.reasoning ? { content: result.reasoning } : void 0
1993
+ },
1994
+ result.usage,
1995
+ result.metrics
1996
+ );
1997
+ }
1998
+
1999
+ // src/completion/ThinkingResolver.ts
2000
+ var import_thinking_config4 = require("@omnicross/contracts/thinking-config");
2001
+ async function resolveEffectiveMaxTokens(llmConfig, getProvider, logger, providerId, modelId, sessionMaxTokens) {
2002
+ if (sessionMaxTokens !== void 0 && sessionMaxTokens > 0) {
2003
+ logger.debug("Using session maxTokens", { sessionMaxTokens });
2004
+ return sessionMaxTokens;
2005
+ }
2006
+ try {
2007
+ const globalParams = await llmConfig.getGlobalModelParameters();
2008
+ if (globalParams?.maxTokens?.enabled && globalParams.maxTokens.value > 0) {
2009
+ logger.debug("Using global maxTokens", { maxTokens: globalParams.maxTokens.value });
2010
+ return globalParams.maxTokens.value;
2011
+ }
2012
+ } catch (err) {
2013
+ logger.warn("Failed to get global params", err instanceof Error ? err : void 0);
2014
+ }
2015
+ const MAX_TOKENS_CAP = 131072;
2016
+ const provider = await getProvider(providerId);
2017
+ if (provider) {
2018
+ const modelConfig = provider.modelConfigs?.find((m) => m.id === modelId);
2019
+ if (modelConfig?.maxTokens && modelConfig.maxTokens > 0) {
2020
+ const cappedMaxTokens = Math.min(modelConfig.maxTokens, MAX_TOKENS_CAP);
2021
+ logger.debug("Using model config maxTokens", {
2022
+ maxTokens: modelConfig.maxTokens,
2023
+ cappedMaxTokens
2024
+ });
2025
+ return cappedMaxTokens;
2026
+ }
2027
+ if (provider.modelGroups) {
2028
+ for (const group of provider.modelGroups) {
2029
+ const model = group.models?.find((m) => m.id === modelId);
2030
+ if (model?.maxTokens && model.maxTokens > 0) {
2031
+ const cappedMaxTokens = Math.min(model.maxTokens, MAX_TOKENS_CAP);
2032
+ logger.debug("Using modelGroup model maxTokens", {
2033
+ maxTokens: model.maxTokens,
2034
+ cappedMaxTokens
2035
+ });
2036
+ return cappedMaxTokens;
2037
+ }
2038
+ }
2039
+ }
2040
+ }
2041
+ try {
2042
+ const discoveredMaxTokens = await llmConfig.getDiscoveredModelMaxTokens(providerId, modelId);
2043
+ if (discoveredMaxTokens && discoveredMaxTokens > 0) {
2044
+ const cappedMaxTokens = Math.min(discoveredMaxTokens, MAX_TOKENS_CAP);
2045
+ logger.debug("Using discovered model maxTokens", {
2046
+ discoveredMaxTokens,
2047
+ cappedMaxTokens
2048
+ });
2049
+ return cappedMaxTokens;
2050
+ }
2051
+ } catch (err) {
2052
+ logger.warn("Failed to get discovered model maxTokens", err instanceof Error ? err : void 0);
2053
+ }
2054
+ logger.debug("No maxTokens configured, returning undefined");
2055
+ return void 0;
2056
+ }
2057
+ async function getRequiredMaxTokens(llmConfig, getProvider, logger, providerId, modelId, sessionMaxTokens) {
2058
+ const resolved = await resolveEffectiveMaxTokens(llmConfig, getProvider, logger, providerId, modelId, sessionMaxTokens);
2059
+ if (resolved !== void 0) {
2060
+ return resolved;
2061
+ }
2062
+ logger.debug("Using default maxTokens", { defaultMaxTokens: import_thinking_config4.DEFAULT_MAX_TOKENS });
2063
+ return import_thinking_config4.DEFAULT_MAX_TOKENS;
2064
+ }
2065
+ async function resolveThinkingBudget(getProvider, logger, providerId, modelId, maxTokens, thinkLevel) {
2066
+ if (thinkLevel === "none" || !(0, import_thinking_config4.isReasoningModel)(modelId)) {
2067
+ return {
2068
+ adjustedMaxTokens: maxTokens,
2069
+ thinkingBudget: void 0,
2070
+ thinkingConfig: void 0
2071
+ };
2072
+ }
2073
+ const thinkingBudget = (0, import_thinking_config4.calculateThinkingBudget)(modelId, thinkLevel, maxTokens);
2074
+ const provider = await getProvider(providerId);
2075
+ const providerName = provider?.name?.toLowerCase() || "";
2076
+ const apiFormat = provider ? resolveApiFormat(provider) : "openai";
2077
+ if (apiFormat === "anthropic" || providerName === "anthropic" || providerName.includes("claude")) {
2078
+ const thinkingConfig = (0, import_thinking_config4.buildAnthropicThinking)(modelId, thinkLevel, maxTokens);
2079
+ const adjustedMaxTokens = (0, import_thinking_config4.getClaudeMaxTokens)(maxTokens, thinkingBudget) || maxTokens;
2080
+ logger.debug("Claude model thinking budget", {
2081
+ thinkingBudget,
2082
+ adjustedMaxTokens
2083
+ });
2084
+ return {
2085
+ adjustedMaxTokens,
2086
+ thinkingBudget,
2087
+ thinkingConfig
2088
+ };
2089
+ }
2090
+ logger.debug("Non-Claude model thinking budget", { thinkingBudget });
2091
+ return {
2092
+ adjustedMaxTokens: maxTokens,
2093
+ thinkingBudget,
2094
+ thinkingConfig: thinkingBudget ? { type: "enabled", budget_tokens: thinkingBudget } : void 0
2095
+ };
2096
+ }
2097
+
2098
+ // src/completion/ToolExecutor.ts
2099
+ function logToolFormat(tools, logger) {
2100
+ if (!Array.isArray(tools) || tools.length === 0) {
2101
+ logger.warn("No tools provided");
2102
+ return;
2103
+ }
2104
+ const firstTool = tools[0];
2105
+ if ("function" in firstTool) {
2106
+ const openaiTools = tools;
2107
+ logger.info("Tools configured (OpenAI format)", {
2108
+ count: openaiTools.length,
2109
+ tools: openaiTools.map((t) => t.function.name),
2110
+ firstTool: {
2111
+ name: firstTool.function.name,
2112
+ description: firstTool.function.description?.slice(0, 100),
2113
+ parameters: firstTool.function.parameters
2114
+ }
2115
+ });
2116
+ } else if ("input_schema" in firstTool) {
2117
+ const anthropicTools = tools;
2118
+ logger.info("Tools configured (Anthropic format)", {
2119
+ count: anthropicTools.length,
2120
+ tools: anthropicTools.map((t) => t.name),
2121
+ firstTool: {
2122
+ name: firstTool.name,
2123
+ description: firstTool.description?.slice(0, 100),
2124
+ input_schema: firstTool.input_schema
2125
+ }
2126
+ });
2127
+ } else if ("functionDeclarations" in firstTool) {
2128
+ const geminiTools = tools;
2129
+ const allDeclarations = geminiTools.flatMap((t) => t.functionDeclarations);
2130
+ logger.info("Tools configured (Gemini format)", {
2131
+ count: allDeclarations.length,
2132
+ tools: allDeclarations.map((t) => t.name),
2133
+ firstTool: {
2134
+ name: allDeclarations[0]?.name,
2135
+ description: allDeclarations[0]?.description?.slice(0, 100),
2136
+ parameters: allDeclarations[0]?.parameters
2137
+ }
2138
+ });
2139
+ }
2140
+ }
2141
+ function buildToolRequest(apiFormat, conversationMessages, actualModel, options, provider) {
2142
+ let requestBody;
2143
+ let url;
2144
+ if (apiFormat === "google") {
2145
+ const contents = [];
2146
+ for (const msg of conversationMessages) {
2147
+ if (msg.role !== "system") {
2148
+ contents.push({
2149
+ role: msg.role === "assistant" ? "model" : "user",
2150
+ parts: [{ text: msg.content }]
2151
+ });
2152
+ }
2153
+ }
2154
+ requestBody = {
2155
+ contents,
2156
+ tools: options.tools
2157
+ };
2158
+ url = buildProviderApiUrl(provider, { model: actualModel, stream: true });
2159
+ } else if (apiFormat === "anthropic") {
2160
+ requestBody = {
2161
+ model: actualModel,
2162
+ messages: conversationMessages.filter((m) => m.role !== "system").map((m) => ({
2163
+ role: m.role,
2164
+ content: m.content
2165
+ })),
2166
+ max_tokens: options.maxTokens || 4096,
2167
+ temperature: options.temperature ?? 0.7,
2168
+ stream: true,
2169
+ tools: options.tools
2170
+ };
2171
+ const systemMsg = conversationMessages.find((m) => m.role === "system");
2172
+ if (systemMsg) {
2173
+ requestBody.system = systemMsg.content;
2174
+ }
2175
+ url = buildProviderApiUrl(provider, { model: actualModel, stream: true });
2176
+ } else {
2177
+ requestBody = {
2178
+ model: actualModel,
2179
+ messages: conversationMessages.map((m) => ({
2180
+ role: m.role,
2181
+ content: m.content
2182
+ })),
2183
+ max_tokens: options.maxTokens || 4096,
2184
+ temperature: options.temperature ?? 0.7,
2185
+ stream: true,
2186
+ tools: options.tools
2187
+ };
2188
+ url = buildProviderApiUrl(provider, { model: actualModel, stream: true });
2189
+ }
2190
+ return { requestBody, url };
2191
+ }
2192
+ function extractDeltaContent(rawJson, apiFormat) {
2193
+ const json = rawJson;
2194
+ if (apiFormat === "google") {
2195
+ const parts = json.candidates?.[0]?.content?.parts || [];
2196
+ let text = "";
2197
+ for (const part of parts) {
2198
+ if (part.thought === true) continue;
2199
+ if (part.text) text += part.text;
2200
+ }
2201
+ return text;
2202
+ } else if (apiFormat === "anthropic") {
2203
+ return json.delta?.text || "";
2204
+ } else {
2205
+ return json.choices?.[0]?.delta?.content || "";
2206
+ }
2207
+ }
2208
+ function extractDeltaReasoning(rawJson, apiFormat) {
2209
+ const json = rawJson;
2210
+ if (apiFormat === "google") {
2211
+ const parts = json.candidates?.[0]?.content?.parts || [];
2212
+ let reasoning = "";
2213
+ for (const part of parts) {
2214
+ if (part.thought === true && part.text) reasoning += part.text;
2215
+ }
2216
+ return reasoning;
2217
+ } else if (apiFormat === "anthropic") {
2218
+ return "";
2219
+ } else {
2220
+ const delta = json.choices?.[0]?.delta;
2221
+ return (delta?.thinking?.content || "") + (delta?.reasoning_content || "");
2222
+ }
2223
+ }
2224
+ function extractToolCalls(rawJson, apiFormat, toolCalls, callbacks, logger, pendingOpenAIToolCalls) {
2225
+ const json = rawJson;
2226
+ if (apiFormat === "google") {
2227
+ const parts = json.candidates?.[0]?.content?.parts || [];
2228
+ for (const part of parts) {
2229
+ if (part.functionCall) {
2230
+ logger.info("Function call detected", { functionCall: part.functionCall });
2231
+ const toolCall = {
2232
+ id: `call_${Date.now()}_${Math.random().toString(36).slice(2)}`,
2233
+ name: part.functionCall.name,
2234
+ args: part.functionCall.args || {}
2235
+ };
2236
+ toolCalls.push(toolCall);
2237
+ callbacks.onToolCall?.(toolCall);
2238
+ logger.info("Tool called", { toolName: toolCall.name });
2239
+ }
2240
+ }
2241
+ } else if (apiFormat !== "anthropic") {
2242
+ const delta = json.choices?.[0]?.delta;
2243
+ if (delta?.tool_calls && pendingOpenAIToolCalls) {
2244
+ for (const tc of delta.tool_calls) {
2245
+ const index = tc.index ?? 0;
2246
+ let pending = pendingOpenAIToolCalls.get(index);
2247
+ if (!pending) {
2248
+ pending = { id: "", name: "", arguments: "" };
2249
+ pendingOpenAIToolCalls.set(index, pending);
2250
+ }
2251
+ if (tc.id) pending.id = tc.id;
2252
+ if (tc.function?.name) pending.name += tc.function.name;
2253
+ if (tc.function?.arguments) pending.arguments += tc.function.arguments;
2254
+ }
2255
+ }
2256
+ }
2257
+ }
2258
+ function finalizeOpenAIToolCalls(pendingOpenAIToolCalls, toolCalls, callbacks, logger) {
2259
+ for (const [, pending] of pendingOpenAIToolCalls) {
2260
+ if (pending.name) {
2261
+ try {
2262
+ const toolCall = {
2263
+ id: pending.id || `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`,
2264
+ name: pending.name,
2265
+ args: JSON.parse(pending.arguments || "{}")
2266
+ };
2267
+ toolCalls.push(toolCall);
2268
+ callbacks.onToolCall?.(toolCall);
2269
+ logger.info("Tool called (OpenAI)", { toolName: toolCall.name, args: toolCall.args });
2270
+ } catch (e) {
2271
+ logger.error("Failed to parse OpenAI tool call arguments", e instanceof Error ? e : void 0, {
2272
+ name: pending.name,
2273
+ arguments: pending.arguments
2274
+ });
2275
+ }
2276
+ }
2277
+ }
2278
+ }
2279
+ function parseStreamChunk(rawJson, apiFormat, callbacks, logger) {
2280
+ const json = rawJson;
2281
+ if (apiFormat === "google") {
2282
+ const candidate = json.candidates?.[0];
2283
+ if (!candidate) {
2284
+ logger.warn("No candidate found in Gemini response");
2285
+ return;
2286
+ }
2287
+ const parts = candidate.content?.parts || [];
2288
+ for (const part of parts) {
2289
+ if (part.thought === true && part.text) {
2290
+ callbacks.onReasoning?.(part.text);
2291
+ continue;
2292
+ } else if (part.thoughtSignature && !part.thought) {
2293
+ logger.debug("Gemini returned thoughtSignature without thought content");
2294
+ }
2295
+ if (part.text) {
2296
+ callbacks.onDelta?.(part.text);
2297
+ }
2298
+ }
2299
+ } else if (apiFormat === "anthropic") {
2300
+ const delta = json.delta;
2301
+ if (delta?.text) {
2302
+ callbacks.onDelta?.(delta.text);
2303
+ }
2304
+ } else {
2305
+ const delta = json.choices?.[0]?.delta;
2306
+ if (delta?.content) {
2307
+ callbacks.onDelta?.(delta.content);
2308
+ }
2309
+ if (delta?.thinking?.content) {
2310
+ callbacks.onReasoning?.(delta.thinking.content);
2311
+ }
2312
+ if (delta?.reasoning_content) {
2313
+ callbacks.onReasoning?.(delta.reasoning_content);
2314
+ }
2315
+ }
2316
+ }
2317
+ function buildIterationBlocks(iteration, content, reasoning, blocks, callbacks) {
2318
+ const blockIdPrefix = `block_${iteration}_${Date.now()}`;
2319
+ if (reasoning && reasoning.trim()) {
2320
+ const thinkingBlock = {
2321
+ id: `${blockIdPrefix}_thinking`,
2322
+ type: "thinking",
2323
+ content: reasoning
2324
+ };
2325
+ blocks.push(thinkingBlock);
2326
+ callbacks.onBlock?.(thinkingBlock);
2327
+ }
2328
+ if (content && content.trim()) {
2329
+ const textBlock = {
2330
+ id: `${blockIdPrefix}_text`,
2331
+ type: "text",
2332
+ content
2333
+ };
2334
+ blocks.push(textBlock);
2335
+ callbacks.onBlock?.(textBlock);
2336
+ }
2337
+ }
2338
+ async function executeToolCalls(toolCalls, mcpTools, mcpService, blocks, callbacks, logger, builtinExecutor) {
2339
+ const toolResults = [];
2340
+ for (const toolCall of toolCalls) {
2341
+ const toolUseBlock = {
2342
+ id: `${toolCall.id}_use`,
2343
+ type: "tool_use",
2344
+ toolId: toolCall.id,
2345
+ toolName: toolCall.name,
2346
+ input: toolCall.args,
2347
+ status: "running"
2348
+ };
2349
+ blocks.push(toolUseBlock);
2350
+ callbacks.onBlock?.(toolUseBlock);
2351
+ try {
2352
+ logger.info("Executing tool", { toolName: toolCall.name });
2353
+ const mcpTool = mcpTools.find((t) => t.id === toolCall.name);
2354
+ if (!mcpTool) {
2355
+ logger.error("Tool not found", void 0, { toolName: toolCall.name });
2356
+ continue;
2357
+ }
2358
+ logger.debug("Tool info", {
2359
+ serverId: mcpTool.serverId,
2360
+ toolName: mcpTool.name,
2361
+ args: toolCall.args
2362
+ });
2363
+ const mappedArgs = mapToolArguments(toolCall.args, mcpTool, logger);
2364
+ let result;
2365
+ if (mcpTool.serverId === "builtin" && builtinExecutor) {
2366
+ result = await builtinExecutor.execute(mcpTool.name, mappedArgs);
2367
+ } else if (mcpService) {
2368
+ result = await mcpService.callTool(mcpTool.serverId, mcpTool.name, mappedArgs, toolCall.id);
2369
+ } else {
2370
+ result = { isError: true, content: [{ type: "text", text: "MCP service not available" }] };
2371
+ }
2372
+ logger.info("Tool result received", { result });
2373
+ callbacks.onToolResult?.(toolCall.id, result);
2374
+ const resultText = result.content?.[0]?.text || JSON.stringify(result);
2375
+ const toolResultBlock = {
2376
+ id: `${toolCall.id}_result`,
2377
+ type: "tool_result",
2378
+ toolId: toolCall.id,
2379
+ toolName: toolCall.name,
2380
+ output: resultText,
2381
+ isError: result.isError
2382
+ };
2383
+ blocks.push(toolResultBlock);
2384
+ callbacks.onBlock?.(toolResultBlock);
2385
+ const toolUseBlockRef = blocks.find((b) => b.id === `${toolCall.id}_use`);
2386
+ if (toolUseBlockRef && toolUseBlockRef.type === "tool_use") {
2387
+ toolUseBlockRef.status = result.isError ? "error" : "completed";
2388
+ callbacks.onBlock?.({ ...toolUseBlockRef });
2389
+ }
2390
+ toolResults.push({ toolCall, result });
2391
+ } catch (error) {
2392
+ logger.error("Tool execution error", error instanceof Error ? error : void 0);
2393
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
2394
+ const errorResult = {
2395
+ isError: true,
2396
+ content: [{ type: "text", text: `Error: ${errorMessage}` }]
2397
+ };
2398
+ callbacks.onToolResult?.(toolCall.id, errorResult);
2399
+ const errorToolResultBlock = {
2400
+ id: `${toolCall.id}_result`,
2401
+ type: "tool_result",
2402
+ toolId: toolCall.id,
2403
+ toolName: toolCall.name,
2404
+ error: errorMessage,
2405
+ isError: true
2406
+ };
2407
+ blocks.push(errorToolResultBlock);
2408
+ callbacks.onBlock?.(errorToolResultBlock);
2409
+ const errorToolUseBlockRef = blocks.find((b) => b.id === `${toolCall.id}_use`);
2410
+ if (errorToolUseBlockRef && errorToolUseBlockRef.type === "tool_use") {
2411
+ errorToolUseBlockRef.status = "error";
2412
+ callbacks.onBlock?.({ ...errorToolUseBlockRef });
2413
+ }
2414
+ toolResults.push({ toolCall, result: errorResult });
2415
+ }
2416
+ }
2417
+ return toolResults;
2418
+ }
2419
+ function mapToolArguments(args, mcpTool, logger) {
2420
+ const mappedArgs = { ...args };
2421
+ const toolSchema = mcpTool.inputSchema;
2422
+ if (toolSchema?.properties) {
2423
+ const expectedParams = Object.keys(toolSchema.properties);
2424
+ const paramMappings = {
2425
+ "search_query": ["query", "q", "searchQuery", "search"],
2426
+ "content": ["text", "body", "message"],
2427
+ "file_path": ["path", "filePath", "file"],
2428
+ "url": ["link", "uri"]
2429
+ };
2430
+ for (const expectedParam of expectedParams) {
2431
+ if (!(expectedParam in mappedArgs)) {
2432
+ const aliases = paramMappings[expectedParam];
2433
+ if (aliases) {
2434
+ for (const alias of aliases) {
2435
+ if (alias in mappedArgs) {
2436
+ logger.info("Mapping parameter", { from: alias, to: expectedParam });
2437
+ mappedArgs[expectedParam] = mappedArgs[alias];
2438
+ delete mappedArgs[alias];
2439
+ break;
2440
+ }
2441
+ }
2442
+ }
2443
+ }
2444
+ }
2445
+ }
2446
+ return mappedArgs;
2447
+ }
2448
+
2449
+ // src/completion/ToolHandler.ts
2450
+ async function streamWithTools(options, callbacks, mcpService, llmConfig, getProvider, resolveApiKey, logger, builtinExecutor) {
2451
+ try {
2452
+ const routedInfo = await llmConfig.resolveRoutedModel(
2453
+ options.providerId,
2454
+ options.model
2455
+ );
2456
+ const actualProviderId = routedInfo?.actualProviderId || options.providerId;
2457
+ const actualModel = routedInfo?.actualModelId || options.model;
2458
+ const provider = await getProvider(actualProviderId);
2459
+ if (!provider) {
2460
+ callbacks.onError?.(`Provider not found: ${actualProviderId}`);
2461
+ return;
2462
+ }
2463
+ if (!provider.enabled) {
2464
+ callbacks.onError?.(`Provider is disabled: ${provider.name}`);
2465
+ return;
2466
+ }
2467
+ const { apiKey: effectiveKey } = resolveProviderEndpoint(provider);
2468
+ const apiKey = resolveApiKey(effectiveKey);
2469
+ if (!apiKey) {
2470
+ callbacks.onError?.("API key not configured");
2471
+ return;
2472
+ }
2473
+ const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
2474
+ callbacks.onStart?.(messageId);
2475
+ const globalParams = await llmConfig.getGlobalModelParameters();
2476
+ const MAX_ITERATIONS = globalParams.toolMaxTurns ?? 100;
2477
+ let iteration = 0;
2478
+ const conversationMessages = [...options.messages];
2479
+ let finalContent = "";
2480
+ let finalReasoning = "";
2481
+ let finalUsageTokens;
2482
+ const blocks = [];
2483
+ logger.info("Sending request to LLM with tools", {
2484
+ model: actualModel,
2485
+ providerId: actualProviderId,
2486
+ messagesCount: options.messages.length
2487
+ });
2488
+ logToolFormat(options.tools, logger);
2489
+ const apiFormat = resolveApiFormat(provider);
2490
+ logger.info("API format determined", { apiFormat });
2491
+ while (iteration < MAX_ITERATIONS) {
2492
+ iteration++;
2493
+ logger.info("Agentic loop iteration", {
2494
+ iteration,
2495
+ maxIterations: MAX_ITERATIONS,
2496
+ messagesCount: conversationMessages.length
2497
+ });
2498
+ const { requestBody, url } = buildToolRequest(
2499
+ apiFormat,
2500
+ conversationMessages,
2501
+ actualModel,
2502
+ options,
2503
+ provider
2504
+ );
2505
+ if (options.nativeSearchAugmentation) {
2506
+ applyAugmentation(requestBody, options.nativeSearchAugmentation);
2507
+ }
2508
+ const headers = getProviderHeaders(provider, apiKey);
2509
+ logger.info("Sending tool call request", {
2510
+ url,
2511
+ headers,
2512
+ requestBody
2513
+ });
2514
+ const response = await fetch(url, {
2515
+ method: "POST",
2516
+ headers,
2517
+ body: JSON.stringify(requestBody)
2518
+ });
2519
+ if (!response.ok) {
2520
+ const errorText = await response.text();
2521
+ callbacks.onError?.(`API error (${response.status}): ${errorText}`);
2522
+ return;
2523
+ }
2524
+ const reader = response.body?.getReader();
2525
+ if (!reader) {
2526
+ callbacks.onError?.("No response body");
2527
+ return;
2528
+ }
2529
+ const decoder = new TextDecoder();
2530
+ let content = "";
2531
+ let reasoning = "";
2532
+ const toolCalls = [];
2533
+ const pendingOpenAIToolCalls = /* @__PURE__ */ new Map();
2534
+ let buffer = "";
2535
+ try {
2536
+ while (true) {
2537
+ const { done, value } = await reader.read();
2538
+ if (done) break;
2539
+ const chunk = decoder.decode(value, { stream: true });
2540
+ logger.debug("Received raw chunk", { chunkLength: chunk.length });
2541
+ buffer += chunk;
2542
+ const lines = buffer.split("\n");
2543
+ buffer = lines.pop() || "";
2544
+ for (const line of lines) {
2545
+ const trimmedLine = line.trim();
2546
+ if (!trimmedLine) continue;
2547
+ logger.debug("Processing line", { linePreview: trimmedLine.substring(0, 200) });
2548
+ if (trimmedLine.startsWith("data: ")) {
2549
+ const data = trimmedLine.slice(6);
2550
+ if (data === "[DONE]") {
2551
+ logger.debug("Stream done");
2552
+ continue;
2553
+ }
2554
+ try {
2555
+ const json = JSON.parse(data);
2556
+ logger.debug("Parsed JSON", { json });
2557
+ if (json.error) {
2558
+ logger.error("API Error", void 0, { error: json.error });
2559
+ callbacks.onError?.(`API Error: ${json.error.message || JSON.stringify(json.error)}`);
2560
+ return;
2561
+ }
2562
+ parseStreamChunk(json, apiFormat, callbacks, logger);
2563
+ content = content + extractDeltaContent(json, apiFormat);
2564
+ reasoning = reasoning + extractDeltaReasoning(json, apiFormat);
2565
+ extractToolCalls(json, apiFormat, toolCalls, callbacks, logger, pendingOpenAIToolCalls);
2566
+ if (apiFormat !== "google" && apiFormat !== "anthropic" && json.usage) {
2567
+ finalUsageTokens = {
2568
+ promptTokens: json.usage.prompt_tokens || 0,
2569
+ completionTokens: json.usage.completion_tokens || 0,
2570
+ totalTokens: json.usage.total_tokens || 0
2571
+ };
2572
+ }
2573
+ } catch (_e) {
2574
+ }
2575
+ }
2576
+ }
2577
+ }
2578
+ if (apiFormat !== "google" && apiFormat !== "anthropic") {
2579
+ finalizeOpenAIToolCalls(pendingOpenAIToolCalls, toolCalls, callbacks, logger);
2580
+ pendingOpenAIToolCalls.clear();
2581
+ }
2582
+ finalContent = content;
2583
+ finalReasoning = reasoning;
2584
+ buildIterationBlocks(iteration, content, reasoning, blocks, callbacks);
2585
+ const executableToolCalls = toolCalls.filter(
2586
+ (tc) => !NATIVE_SEARCH_TOOL_NAMES.includes(tc.name)
2587
+ );
2588
+ logger.info("Tool execution check", {
2589
+ toolCallsLength: toolCalls.length,
2590
+ executableToolCallsLength: executableToolCalls.length,
2591
+ hasMcpTools: !!options.mcpTools,
2592
+ mcpToolsLength: options.mcpTools?.length,
2593
+ hasMcpService: !!mcpService
2594
+ });
2595
+ if (executableToolCalls.length > 0 && options.mcpTools && (mcpService || builtinExecutor)) {
2596
+ logger.info("Executing tool calls", { count: executableToolCalls.length });
2597
+ conversationMessages.push({
2598
+ id: `assistant_${iteration}`,
2599
+ role: "assistant",
2600
+ content: content || "Calling tools...",
2601
+ timestamp: Date.now()
2602
+ });
2603
+ const toolResults = await executeToolCalls(
2604
+ executableToolCalls,
2605
+ options.mcpTools,
2606
+ mcpService,
2607
+ blocks,
2608
+ callbacks,
2609
+ logger,
2610
+ builtinExecutor
2611
+ );
2612
+ const toolResultsText = toolResults.map(({ toolCall, result }) => {
2613
+ const resultText = typeof result === "string" ? result : result.content?.[0]?.text || JSON.stringify(result);
2614
+ return `Tool ${toolCall.name} result:
2615
+ ${resultText}`;
2616
+ }).join("\n\n");
2617
+ conversationMessages.push({
2618
+ id: `tool_results_${iteration}`,
2619
+ role: "user",
2620
+ content: toolResultsText,
2621
+ timestamp: Date.now()
2622
+ });
2623
+ logger.info("Continuing to next iteration with tool results");
2624
+ continue;
2625
+ } else {
2626
+ logger.info("No tool calls, finishing");
2627
+ break;
2628
+ }
2629
+ } catch (streamError) {
2630
+ logger.error("Stream processing error", streamError instanceof Error ? streamError : void 0);
2631
+ callbacks.onError?.(`Stream error: ${streamError instanceof Error ? streamError.message : "Unknown error"}`);
2632
+ return;
2633
+ }
2634
+ }
2635
+ logger.info("Sending final callback", { blocksCount: blocks.length });
2636
+ callbacks.onDone?.({
2637
+ id: messageId,
2638
+ role: "assistant",
2639
+ content: finalContent,
2640
+ timestamp: Date.now(),
2641
+ thinking: finalReasoning ? { content: finalReasoning } : void 0,
2642
+ blocks: blocks.length > 0 ? blocks : void 0
2643
+ }, finalUsageTokens);
2644
+ } catch (error) {
2645
+ logger.error("Error in streamWithTools", error instanceof Error ? error : void 0);
2646
+ const message = error instanceof Error ? error.message : "Unknown error";
2647
+ callbacks.onError?.(message);
2648
+ }
2649
+ }
2650
+
2651
+ // src/completion/TransformerHandler.ts
2652
+ function readUsageFromOpenAIResponse(usage) {
2653
+ if (!usage) return null;
2654
+ const promptTokens = Number(usage.prompt_tokens) || 0;
2655
+ const completionTokens = Number(usage.completion_tokens) || 0;
2656
+ const promptDetails = usage.prompt_tokens_details ?? {};
2657
+ const cachedTokens = Number(promptDetails.cached_tokens) || Number(usage.cache_read_input_tokens) || 0;
2658
+ const cacheCreation = Number(usage.cache_creation_input_tokens) || 0;
2659
+ const inputTokens = Math.max(0, promptTokens - cachedTokens - cacheCreation);
2660
+ const reasoningDetails = usage.completion_tokens_details ?? usage.output_tokens_details ?? {};
2661
+ const reasoningTokens = Number(reasoningDetails.reasoning_tokens) || 0;
2662
+ return {
2663
+ inputTokens,
2664
+ outputTokens: completionTokens,
2665
+ cacheReadTokens: cachedTokens,
2666
+ cacheCreationTokens: cacheCreation,
2667
+ reasoningTokens
2668
+ };
2669
+ }
2670
+ var sharedExecutor = null;
2671
+ function getSharedExecutor2() {
2672
+ if (!sharedExecutor) {
2673
+ sharedExecutor = new TransformerChainExecutor();
2674
+ }
2675
+ return sharedExecutor;
2676
+ }
2677
+ async function resolveChainWithMain(llmConfig, providerId, model) {
2678
+ return resolveProviderChain(llmConfig, providerId, model);
2679
+ }
2680
+ async function completeWithTransformers(options, llmConfig, getProvider, resolveApiKey, completeFallback, logger, recording) {
2681
+ try {
2682
+ const routedInfo = await llmConfig.resolveRoutedModel(
2683
+ options.providerId,
2684
+ options.model
2685
+ );
2686
+ const actualProviderId = routedInfo?.actualProviderId || options.providerId;
2687
+ const actualModel = routedInfo?.actualModelId || options.model;
2688
+ const provider = await getProvider(actualProviderId);
2689
+ if (!provider) {
2690
+ return { success: false, error: `Provider not found: ${actualProviderId}` };
2691
+ }
2692
+ if (!provider.enabled) {
2693
+ return { success: false, error: `Provider is disabled: ${provider.name}` };
2694
+ }
2695
+ const { apiKey: effectiveKey } = resolveProviderEndpoint(provider);
2696
+ const apiKey = resolveApiKey(effectiveKey);
2697
+ if (!apiKey) {
2698
+ return { success: false, error: "API key not configured" };
2699
+ }
2700
+ const { chain, hasTransformers } = await resolveChainWithMain(
2701
+ llmConfig,
2702
+ actualProviderId,
2703
+ actualModel
2704
+ );
2705
+ if (!hasTransformers) {
2706
+ return completeFallback(options);
2707
+ }
2708
+ const unifiedRequest = {
2709
+ model: actualModel,
2710
+ messages: options.messages.map((m) => ({
2711
+ role: m.role,
2712
+ content: m.content
2713
+ })),
2714
+ max_tokens: options.maxTokens || 4096,
2715
+ temperature: options.temperature,
2716
+ stream: options.stream ?? false
2717
+ };
2718
+ const transformerProvider = {
2719
+ name: provider.name,
2720
+ baseUrl: provider.api_base_url,
2721
+ apiKey,
2722
+ models: provider.models || []
2723
+ };
2724
+ const executor = getSharedExecutor2();
2725
+ const { response } = await executeProviderCall({
2726
+ executor,
2727
+ request: unifiedRequest,
2728
+ provider: transformerProvider,
2729
+ chain,
2730
+ endpointTransformer: void 0,
2731
+ extendedContext: options.useExtendedContext ? { enabled: true, model: actualModel } : void 0,
2732
+ resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : buildProviderApiUrl(provider, { model: actualModel, stream: false }),
2733
+ buildHeaders: (config) => ({
2734
+ ...getProviderHeaders(provider, apiKey),
2735
+ ...config.headers,
2736
+ ...isOpenRouterProvider(provider) ? OPENROUTER_APP_HEADERS : {}
2737
+ }),
2738
+ // Add OpenRouter provider routing config if applicable
2739
+ prepareBody: (requestBody) => addOpenRouterProviderToRequest(
2740
+ requestBody,
2741
+ provider,
2742
+ actualModel
2743
+ ),
2744
+ fetchFn: (url, headers, body) => {
2745
+ logger.info("Calling completion with transformers", { url });
2746
+ return fetch(url, {
2747
+ method: "POST",
2748
+ headers,
2749
+ body: JSON.stringify(body)
2750
+ });
2751
+ }
2752
+ });
2753
+ if (!response.ok) {
2754
+ const errorText = await response.text();
2755
+ return {
2756
+ success: false,
2757
+ error: `API error (${response.status}): ${errorText}`
2758
+ };
2759
+ }
2760
+ const transformedResponse = await executor.executeResponseChain(
2761
+ unifiedRequest,
2762
+ response,
2763
+ transformerProvider,
2764
+ chain,
2765
+ { endpointTransformer: void 0 }
2766
+ );
2767
+ const data = await transformedResponse.json();
2768
+ const choice = data.choices?.[0];
2769
+ if (!choice) {
2770
+ return { success: false, error: "No choices in response" };
2771
+ }
2772
+ const rawToolCalls = choice.message?.tool_calls;
2773
+ const toolCalls = rawToolCalls?.map((tc) => ({
2774
+ id: tc.id,
2775
+ name: tc.function.name,
2776
+ args: typeof tc.function.arguments === "string" ? JSON.parse(tc.function.arguments) : tc.function.arguments
2777
+ }));
2778
+ const tapped = readUsageFromOpenAIResponse(data.usage);
2779
+ if (recording?.recorder && tapped) {
2780
+ recording.recorder.record({
2781
+ messageId: options.messageId ?? null,
2782
+ parentMessageId: options.parentMessageId ?? null,
2783
+ sessionId: options.sessionId ?? null,
2784
+ providerId: actualProviderId,
2785
+ model: actualModel,
2786
+ apiKeyId: recording.apiKeyId ?? null,
2787
+ engineOrigin: "completion",
2788
+ usage: tapped,
2789
+ rawUsage: data.usage
2790
+ });
2791
+ }
2792
+ return {
2793
+ success: true,
2794
+ message: {
2795
+ id: `msg_${Date.now()}`,
2796
+ role: "assistant",
2797
+ content: choice.message?.content || "",
2798
+ timestamp: Date.now(),
2799
+ thinking: data.reasoning_content || choice.message?.thinking?.content ? { content: data.reasoning_content || choice.message?.thinking?.content || "", signature: choice.message?.thinking?.signature } : void 0,
2800
+ toolCalls
2801
+ },
2802
+ usage: data.usage ? {
2803
+ promptTokens: data.usage.prompt_tokens || 0,
2804
+ completionTokens: data.usage.completion_tokens || 0,
2805
+ totalTokens: data.usage.total_tokens || 0
2806
+ } : void 0,
2807
+ finishReason: choice.finish_reason
2808
+ };
2809
+ } catch (error) {
2810
+ const message = error instanceof Error ? error.message : "Unknown error";
2811
+ return { success: false, error: message };
2812
+ }
2813
+ }
2814
+ async function completeStreamWithTransformers(options, callbacks, llmConfig, getProvider, resolveApiKey, completeStreamFallback, logger, recording) {
2815
+ try {
2816
+ const routedInfo = await llmConfig.resolveRoutedModel(
2817
+ options.providerId,
2818
+ options.model
2819
+ );
2820
+ const actualProviderId = routedInfo?.actualProviderId || options.providerId;
2821
+ const actualModel = routedInfo?.actualModelId || options.model;
2822
+ const provider = await getProvider(actualProviderId);
2823
+ if (!provider) {
2824
+ callbacks.onError?.(`Provider not found: ${actualProviderId}`);
2825
+ return;
2826
+ }
2827
+ if (!provider.enabled) {
2828
+ callbacks.onError?.(`Provider is disabled: ${provider.name}`);
2829
+ return;
2830
+ }
2831
+ const { apiKey: effectiveKey } = resolveProviderEndpoint(provider);
2832
+ const apiKey = resolveApiKey(effectiveKey);
2833
+ if (!apiKey) {
2834
+ callbacks.onError?.("API key not configured");
2835
+ return;
2836
+ }
2837
+ const { chain, hasTransformers } = await resolveChainWithMain(
2838
+ llmConfig,
2839
+ actualProviderId,
2840
+ actualModel
2841
+ );
2842
+ if (!hasTransformers) {
2843
+ return completeStreamFallback(options, callbacks);
2844
+ }
2845
+ const messageId = `msg_${Date.now()}`;
2846
+ callbacks.onStart?.(messageId);
2847
+ const unifiedRequest = {
2848
+ model: actualModel,
2849
+ messages: options.messages.map((m) => ({
2850
+ role: m.role,
2851
+ content: m.content
2852
+ })),
2853
+ max_tokens: options.maxTokens || 4096,
2854
+ temperature: options.temperature,
2855
+ stream: true
2856
+ };
2857
+ const transformerProvider = {
2858
+ name: provider.name,
2859
+ baseUrl: provider.api_base_url,
2860
+ apiKey,
2861
+ models: provider.models || []
2862
+ };
2863
+ const executor = getSharedExecutor2();
2864
+ const { response } = await executeProviderCall({
2865
+ executor,
2866
+ request: unifiedRequest,
2867
+ provider: transformerProvider,
2868
+ chain,
2869
+ endpointTransformer: void 0,
2870
+ extendedContext: options.useExtendedContext ? { enabled: true, model: actualModel } : void 0,
2871
+ resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : buildProviderApiUrl(provider, { model: actualModel, stream: true }),
2872
+ buildHeaders: (config) => ({
2873
+ ...getProviderHeaders(provider, apiKey),
2874
+ ...config.headers,
2875
+ ...isOpenRouterProvider(provider) ? OPENROUTER_APP_HEADERS : {}
2876
+ }),
2877
+ // Add OpenRouter provider routing config if applicable
2878
+ prepareBody: (requestBody) => addOpenRouterProviderToRequest(
2879
+ requestBody,
2880
+ provider,
2881
+ actualModel
2882
+ ),
2883
+ fetchFn: (url, headers, body) => {
2884
+ logger.info("Streaming completion with transformers", { url });
2885
+ return fetch(url, {
2886
+ method: "POST",
2887
+ headers,
2888
+ body: JSON.stringify(body)
2889
+ });
2890
+ }
2891
+ });
2892
+ if (!response.ok) {
2893
+ const errorText = await response.text();
2894
+ callbacks.onError?.(`API error (${response.status}): ${errorText}`);
2895
+ return;
2896
+ }
2897
+ const transformedResponse = await executor.executeResponseChain(
2898
+ unifiedRequest,
2899
+ response,
2900
+ transformerProvider,
2901
+ chain,
2902
+ { endpointTransformer: void 0 }
2903
+ );
2904
+ const reader = transformedResponse.body?.getReader();
2905
+ if (!reader) {
2906
+ callbacks.onError?.("No response body");
2907
+ return;
2908
+ }
2909
+ const decoder = new TextDecoder();
2910
+ let content = "";
2911
+ let reasoning = "";
2912
+ try {
2913
+ while (true) {
2914
+ const { done, value } = await reader.read();
2915
+ if (done) break;
2916
+ const chunk = decoder.decode(value, { stream: true });
2917
+ const lines = chunk.split("\n").filter((line) => line.trim() !== "");
2918
+ for (const line of lines) {
2919
+ if (line.startsWith("data: ")) {
2920
+ const data = line.slice(6);
2921
+ if (data === "[DONE]") continue;
2922
+ try {
2923
+ const json = JSON.parse(data);
2924
+ const delta = json.choices?.[0]?.delta;
2925
+ if (delta?.content) {
2926
+ content += delta.content;
2927
+ callbacks.onDelta?.(delta.content);
2928
+ }
2929
+ if (delta?.thinking?.content) {
2930
+ reasoning += delta.thinking.content;
2931
+ callbacks.onReasoning?.(delta.thinking.content);
2932
+ }
2933
+ if (delta?.reasoning_content) {
2934
+ reasoning += delta.reasoning_content;
2935
+ callbacks.onReasoning?.(delta.reasoning_content);
2936
+ }
2937
+ if (json.usage) {
2938
+ const tapped = readUsageFromOpenAIResponse(json.usage);
2939
+ if (recording?.recorder && tapped) {
2940
+ recording.recorder.record({
2941
+ messageId: options.messageId ?? null,
2942
+ parentMessageId: options.parentMessageId ?? null,
2943
+ sessionId: options.sessionId ?? null,
2944
+ providerId: actualProviderId,
2945
+ model: actualModel,
2946
+ apiKeyId: recording.apiKeyId ?? null,
2947
+ engineOrigin: "completion",
2948
+ usage: tapped,
2949
+ rawUsage: json.usage
2950
+ });
2951
+ }
2952
+ callbacks.onDone?.(
2953
+ {
2954
+ id: messageId,
2955
+ role: "assistant",
2956
+ content,
2957
+ timestamp: Date.now(),
2958
+ thinking: reasoning ? { content: reasoning } : void 0
2959
+ },
2960
+ {
2961
+ promptTokens: json.usage.prompt_tokens || 0,
2962
+ completionTokens: json.usage.completion_tokens || 0,
2963
+ totalTokens: json.usage.total_tokens || 0
2964
+ }
2965
+ );
2966
+ return;
2967
+ }
2968
+ } catch (_e) {
2969
+ }
2970
+ }
2971
+ }
2972
+ }
2973
+ callbacks.onDone?.({
2974
+ id: messageId,
2975
+ role: "assistant",
2976
+ content,
2977
+ timestamp: Date.now(),
2978
+ thinking: reasoning ? { content: reasoning } : void 0
2979
+ });
2980
+ } finally {
2981
+ reader.releaseLock();
2982
+ }
2983
+ } catch (error) {
2984
+ const message = error instanceof Error ? error.message : "Unknown error";
2985
+ callbacks.onError?.(message);
2986
+ }
2987
+ }
2988
+
2989
+ // src/completion/CompletionService.ts
2990
+ var CompletionService = class {
2991
+ constructor(paths, llmConfig, logger) {
2992
+ this.paths = paths;
2993
+ this.llmConfig = llmConfig;
2994
+ this.logger = logger;
2995
+ }
2996
+ paths;
2997
+ llmConfig;
2998
+ logger;
2999
+ apiKeyPool = null;
3000
+ usageRecorder = null;
3001
+ usageEventSink = null;
3002
+ visionFallbackProvider = null;
3003
+ /**
3004
+ * Set the API key pool service for multi-key load balancing.
3005
+ * When set, keys are resolved via the pool instead of directly from the provider.
3006
+ */
3007
+ setApiKeyPool(pool) {
3008
+ this.apiKeyPool = pool;
3009
+ }
3010
+ /**
3011
+ * Set the usage recorder so completion paths can persist token/cost stats.
3012
+ * Optional — when unset, all calls succeed but nothing is recorded.
3013
+ */
3014
+ setUsageRecorder(recorder) {
3015
+ this.usageRecorder = recorder;
3016
+ }
3017
+ /**
3018
+ * Set the usage-event sink so completion paths can push live usage events
3019
+ * (context-meter, aggregate recorder) into the in-process hub. Injected DOWN
3020
+ * at bootstrap with `getUsageEventHub()` immediately after construction, so
3021
+ * emission is unconditional in production. Optional — when unset (unit-test
3022
+ * constructors), the emit calls no-op, identical to `usageRecorder`.
3023
+ */
3024
+ setUsageEventSink(sink) {
3025
+ this.usageEventSink = sink;
3026
+ }
3027
+ /**
3028
+ * Set the vision-fallback provider used by `applyVisionFallback` to describe
3029
+ * images for non-vision models. Injected DOWN by the host at bootstrap
3030
+ * (the host's impl is built on top of CompletionService).
3031
+ * Optional — when unset, `applyVisionFallback` strips images instead.
3032
+ */
3033
+ setVisionFallbackProvider(provider) {
3034
+ this.visionFallbackProvider = provider;
3035
+ }
3036
+ /**
3037
+ * Get provider by ID
3038
+ * Delegates to the `ProviderConfigSource` which maintains its own in-memory cache.
3039
+ */
3040
+ async getProvider(providerId) {
3041
+ return this.llmConfig.getProvider(providerId);
3042
+ }
3043
+ /**
3044
+ * Send a completion request
3045
+ */
3046
+ async complete(options) {
3047
+ try {
3048
+ const routedInfo = await this.llmConfig.resolveRoutedModel(
3049
+ options.providerId,
3050
+ options.model
3051
+ );
3052
+ const actualProviderId = routedInfo?.actualProviderId || options.providerId;
3053
+ const actualModel = routedInfo?.actualModelId || options.model;
3054
+ const provider = await this.getProvider(actualProviderId);
3055
+ if (!provider) {
3056
+ return { success: false, error: `Provider not found: ${actualProviderId}` };
3057
+ }
3058
+ if (!provider.enabled) {
3059
+ return { success: false, error: `Provider is disabled: ${provider.name}` };
3060
+ }
3061
+ const apiKey = await this.resolveApiKeyForRequest(provider, actualProviderId, options.sessionId);
3062
+ if (!apiKey) {
3063
+ return { success: false, error: "API key not configured" };
3064
+ }
3065
+ const apiFormat = resolveApiFormat(provider);
3066
+ this.logger.info("Using API format for completion", { apiFormat, providerId: actualProviderId });
3067
+ const resolvedOptions = { ...options, model: actualModel };
3068
+ let result = await this.callDirectHandler(apiFormat, provider, apiKey, resolvedOptions);
3069
+ if (!result.success && result.error && this.apiKeyPool && options.sessionId) {
3070
+ const status = this.extractHttpStatus(result.error);
3071
+ if (status && (status === 429 || status === 529 || status === 401 || status === 403)) {
3072
+ const newKey = await this.apiKeyPool.reportError(actualProviderId, options.sessionId, status);
3073
+ if (newKey) {
3074
+ this.logger.info("Retrying completion with new API key", {
3075
+ providerId: actualProviderId,
3076
+ statusCode: status
3077
+ });
3078
+ result = await this.callDirectHandler(apiFormat, provider, newKey, resolvedOptions);
3079
+ }
3080
+ }
3081
+ }
3082
+ if (result.success && this.apiKeyPool && options.sessionId) {
3083
+ this.apiKeyPool.reportSuccess(options.sessionId);
3084
+ }
3085
+ if (result.success && result.usage && options.sessionId) {
3086
+ this.usageEventSink?.emit({
3087
+ sessionId: options.sessionId,
3088
+ modelId: actualModel,
3089
+ usage: result.usage,
3090
+ engineOrigin: "completion"
3091
+ });
3092
+ }
3093
+ return result;
3094
+ } catch (error) {
3095
+ const message = error instanceof Error ? error.message : "Unknown error";
3096
+ return { success: false, error: message };
3097
+ }
3098
+ }
3099
+ /**
3100
+ * Send a streaming completion request
3101
+ */
3102
+ async completeStream(options, callbacks) {
3103
+ try {
3104
+ await this.applyVisionFallback(options);
3105
+ this.logger.info("Starting stream completion", {
3106
+ providerId: options.providerId,
3107
+ model: options.model,
3108
+ messagesCount: options.messages.length
3109
+ });
3110
+ const routedInfo = await this.llmConfig.resolveRoutedModel(
3111
+ options.providerId,
3112
+ options.model
3113
+ );
3114
+ this.logger.debug("Resolved routed model", { routedInfo });
3115
+ const actualProviderId = routedInfo?.actualProviderId || options.providerId;
3116
+ const actualModel = routedInfo?.actualModelId || options.model;
3117
+ this.logger.debug("Resolved provider and model", { actualProviderId, actualModel });
3118
+ const provider = await this.getProvider(actualProviderId);
3119
+ this.logger.debug("Retrieved provider", provider ? {
3120
+ id: provider.id,
3121
+ name: provider.name,
3122
+ apiType: provider.apiType,
3123
+ apiFormat: provider.apiFormat,
3124
+ api_base_url: provider.api_base_url,
3125
+ enabled: provider.enabled
3126
+ } : { error: "Provider not found" });
3127
+ if (!provider) {
3128
+ callbacks.onError?.(`Provider not found: ${actualProviderId}`);
3129
+ return;
3130
+ }
3131
+ if (!provider.enabled) {
3132
+ callbacks.onError?.(`Provider is disabled: ${provider.name}`);
3133
+ return;
3134
+ }
3135
+ const apiKey = await this.resolveApiKeyForRequest(provider, actualProviderId, options.sessionId);
3136
+ if (!apiKey) {
3137
+ callbacks.onError?.("API key not configured");
3138
+ return;
3139
+ }
3140
+ const apiFormat = resolveApiFormat(provider);
3141
+ this.logger.info("Using API format for stream", { apiFormat });
3142
+ const messageId = `msg_${Date.now()}`;
3143
+ callbacks.onStart?.(messageId);
3144
+ const resolvedOptions = { ...options, model: actualModel };
3145
+ if (options.sessionId) {
3146
+ const sid = options.sessionId;
3147
+ const userOnDone = callbacks.onDone;
3148
+ callbacks = {
3149
+ ...callbacks,
3150
+ onDone: (message, usage, metrics) => {
3151
+ if (usage) {
3152
+ this.usageEventSink?.emit({
3153
+ sessionId: sid,
3154
+ modelId: actualModel,
3155
+ usage,
3156
+ engineOrigin: "completion"
3157
+ });
3158
+ }
3159
+ userOnDone?.(message, usage, metrics);
3160
+ }
3161
+ };
3162
+ }
3163
+ if (this.apiKeyPool && options.sessionId) {
3164
+ const retryState = { error: null };
3165
+ const interceptCallbacks = {
3166
+ ...callbacks,
3167
+ onStart: void 0,
3168
+ // already called above
3169
+ onError: (error) => {
3170
+ const status = this.extractHttpStatus(error);
3171
+ if (status && (status === 429 || status === 529 || status === 401 || status === 403)) {
3172
+ retryState.error = { status, message: error };
3173
+ return;
3174
+ }
3175
+ callbacks.onError?.(error);
3176
+ },
3177
+ onDone: (message, usage, metrics) => {
3178
+ this.apiKeyPool.reportSuccess(options.sessionId);
3179
+ callbacks.onDone?.(message, usage, metrics);
3180
+ }
3181
+ };
3182
+ await this.callStreamHandler(apiFormat, provider, apiKey, resolvedOptions, messageId, interceptCallbacks);
3183
+ if (retryState.error) {
3184
+ const newKey = await this.apiKeyPool.reportError(
3185
+ actualProviderId,
3186
+ options.sessionId,
3187
+ retryState.error.status
3188
+ );
3189
+ if (newKey) {
3190
+ this.logger.info("Retrying stream with new API key", {
3191
+ providerId: actualProviderId,
3192
+ statusCode: retryState.error.status
3193
+ });
3194
+ await this.callStreamHandler(apiFormat, provider, newKey, resolvedOptions, messageId, {
3195
+ ...callbacks,
3196
+ onStart: void 0
3197
+ // don't fire onStart again
3198
+ });
3199
+ } else {
3200
+ callbacks.onError?.(retryState.error.message);
3201
+ }
3202
+ }
3203
+ } else {
3204
+ await this.callStreamHandler(apiFormat, provider, apiKey, resolvedOptions, messageId, {
3205
+ ...callbacks,
3206
+ onStart: void 0
3207
+ // already called above
3208
+ });
3209
+ }
3210
+ } catch (error) {
3211
+ const message = error instanceof Error ? error.message : "Unknown error";
3212
+ this.logger.error("Stream completion error", error instanceof Error ? error : void 0, { message });
3213
+ callbacks.onError?.(message);
3214
+ }
3215
+ }
3216
+ /**
3217
+ * Get available models for a provider
3218
+ */
3219
+ async getAvailableModels(providerId) {
3220
+ const provider = await this.getProvider(providerId);
3221
+ if (!provider) return [];
3222
+ return provider.models || [];
3223
+ }
3224
+ /**
3225
+ * Test provider connection with a specific model.
3226
+ * Sends "Hello" and returns the AI response, duration, etc.
3227
+ */
3228
+ async testModel(providerId, modelId) {
3229
+ try {
3230
+ const provider = await this.getProvider(providerId);
3231
+ if (!provider) {
3232
+ return { success: false, message: "Provider not found", model: modelId };
3233
+ }
3234
+ const apiKey = this.resolveApiKey(provider.api_key);
3235
+ if (!apiKey) {
3236
+ return { success: false, message: "API key not configured", model: modelId };
3237
+ }
3238
+ const testMessages = [
3239
+ { id: "test", role: "user", content: "Hello", timestamp: Date.now() }
3240
+ ];
3241
+ const startTime = Date.now();
3242
+ const result = await this.complete({
3243
+ providerId,
3244
+ model: modelId,
3245
+ messages: testMessages,
3246
+ maxTokens: 100
3247
+ });
3248
+ const durationMs = Date.now() - startTime;
3249
+ if (result.success) {
3250
+ return {
3251
+ success: true,
3252
+ message: "Connection successful",
3253
+ response: result.message?.content || "",
3254
+ model: modelId,
3255
+ durationMs
3256
+ };
3257
+ } else {
3258
+ return {
3259
+ success: false,
3260
+ message: result.error || "Unknown error",
3261
+ model: modelId,
3262
+ durationMs
3263
+ };
3264
+ }
3265
+ } catch (error) {
3266
+ const message = error instanceof Error ? error.message : "Unknown error";
3267
+ return { success: false, message, model: modelId };
3268
+ }
3269
+ }
3270
+ /**
3271
+ * Check if any messages contain images and the model lacks vision capability.
3272
+ * If so, use the auxiliary vision model to describe images as text and remove
3273
+ * the image attachments from the messages.
3274
+ *
3275
+ * Mutates options.messages in-place.
3276
+ */
3277
+ async applyVisionFallback(options) {
3278
+ const hasImages = options.messages.some((m) => m.images && m.images.length > 0);
3279
+ if (!hasImages) return;
3280
+ const hasVision = await this.llmConfig.hasVisionCapability(options.providerId, options.model);
3281
+ if (hasVision) return;
3282
+ const { vision: effectiveVisionModel } = await this.llmConfig.resolveEffectiveModels();
3283
+ if (!effectiveVisionModel || !this.visionFallbackProvider) {
3284
+ this.logger.info("Messages contain images but no vision auxiliary model configured; stripping images");
3285
+ for (const msg of options.messages) {
3286
+ if (msg.images && msg.images.length > 0) {
3287
+ msg.images = void 0;
3288
+ }
3289
+ }
3290
+ return;
3291
+ }
3292
+ this.logger.info("Model lacks vision capability, using auxiliary vision model for image descriptions", {
3293
+ providerId: options.providerId,
3294
+ model: options.model
3295
+ });
3296
+ const visionService = this.visionFallbackProvider;
3297
+ for (const msg of options.messages) {
3298
+ if (msg.images && msg.images.length > 0) {
3299
+ const imageDataUrls = msg.images.map((img) => ({ data: img.url }));
3300
+ const description = await visionService.describeImages(imageDataUrls, msg.content, effectiveVisionModel);
3301
+ if (description && description !== "[Image description unavailable]") {
3302
+ msg.content = `${msg.content}
3303
+
3304
+ [Image Description]
3305
+ ${description}`;
3306
+ }
3307
+ msg.images = void 0;
3308
+ }
3309
+ }
3310
+ }
3311
+ /**
3312
+ * Resolve API key for a request with priority:
3313
+ * 1. Coding Plan override (if enabled)
3314
+ * 2. API key pool (session-affinity weighted round-robin)
3315
+ * 3. Legacy single key from provider config
3316
+ */
3317
+ async resolveApiKeyForRequest(provider, providerId, sessionId) {
3318
+ if (provider.codingPlan?.enabled && provider.codingPlan.apiKey) {
3319
+ return this.resolveApiKey(provider.codingPlan.apiKey);
3320
+ }
3321
+ if (this.apiKeyPool) {
3322
+ const poolKey = sessionId ? await this.apiKeyPool.getKeyForSession(providerId, sessionId) : await this.apiKeyPool.getKey(providerId);
3323
+ if (poolKey) return poolKey;
3324
+ }
3325
+ const { apiKey: effectiveKey } = resolveProviderEndpoint(provider);
3326
+ return this.resolveApiKey(effectiveKey);
3327
+ }
3328
+ // ==========================================================================
3329
+ // Handler dispatch helpers
3330
+ // ==========================================================================
3331
+ /**
3332
+ * Extract HTTP status code from error messages like "API error (429): ..."
3333
+ */
3334
+ extractHttpStatus(error) {
3335
+ const match = error.match(/\((\d{3})\):/);
3336
+ return match ? parseInt(match[1], 10) : null;
3337
+ }
3338
+ /**
3339
+ * Dispatch a non-streaming completion to the appropriate handler.
3340
+ */
3341
+ async callDirectHandler(apiFormat, provider, apiKey, options) {
3342
+ switch (apiFormat) {
3343
+ case "anthropic":
3344
+ return callAnthropicCompletion(provider, apiKey, options, this.logger);
3345
+ case "google":
3346
+ return callGeminiCompletion(provider, apiKey, options, this.logger);
3347
+ case "openai-response":
3348
+ return callOpenAIResponseCompletion(provider, apiKey, options, this.logger);
3349
+ case "azure-openai":
3350
+ case "openai":
3351
+ default:
3352
+ return callOpenAICompletion(provider, apiKey, options, this.logger);
3353
+ }
3354
+ }
3355
+ /**
3356
+ * Dispatch a streaming completion to the appropriate handler.
3357
+ */
3358
+ async callStreamHandler(apiFormat, provider, apiKey, options, messageId, callbacks) {
3359
+ switch (apiFormat) {
3360
+ case "openai-response":
3361
+ await streamOpenAIResponseCompletion(provider, apiKey, options, messageId, callbacks, this.logger);
3362
+ return;
3363
+ case "anthropic":
3364
+ await streamAnthropicCompletion(provider, apiKey, options, messageId, callbacks, this.logger);
3365
+ return;
3366
+ case "google":
3367
+ await streamGeminiCompletion(provider, apiKey, options, messageId, callbacks, this.logger);
3368
+ return;
3369
+ case "azure-openai":
3370
+ case "openai":
3371
+ default:
3372
+ await streamOpenAICompletion(provider, apiKey, options, messageId, callbacks, this.logger);
3373
+ return;
3374
+ }
3375
+ }
3376
+ /**
3377
+ * Resolve API key (handle environment variable references)
3378
+ */
3379
+ resolveApiKey(apiKey) {
3380
+ if (!apiKey) return "";
3381
+ if (apiKey.startsWith("$")) {
3382
+ const envVar = apiKey.slice(1);
3383
+ return process.env[envVar] || "";
3384
+ }
3385
+ return apiKey;
3386
+ }
3387
+ /**
3388
+ * Resolve effective max_tokens value with priority:
3389
+ * 1. Session settings (if provided)
3390
+ * 2. Global model parameters (if enabled)
3391
+ * 3. Model's maxTokens from provider config
3392
+ * 4. Discovered models cache (from API)
3393
+ * 5. undefined - let API use its default
3394
+ */
3395
+ async resolveEffectiveMaxTokens(providerId, modelId, sessionMaxTokens) {
3396
+ return resolveEffectiveMaxTokens(
3397
+ this.llmConfig,
3398
+ this.getProvider.bind(this),
3399
+ this.logger,
3400
+ providerId,
3401
+ modelId,
3402
+ sessionMaxTokens
3403
+ );
3404
+ }
3405
+ /**
3406
+ * Get required max_tokens for providers that need it (e.g., Anthropic)
3407
+ * Falls back to DEFAULT_MAX_TOKENS if no value is configured
3408
+ */
3409
+ async getRequiredMaxTokens(providerId, modelId, sessionMaxTokens) {
3410
+ return getRequiredMaxTokens(
3411
+ this.llmConfig,
3412
+ this.getProvider.bind(this),
3413
+ this.logger,
3414
+ providerId,
3415
+ modelId,
3416
+ sessionMaxTokens
3417
+ );
3418
+ }
3419
+ /**
3420
+ * Calculate thinking budget and adjust max_tokens for the provider
3421
+ */
3422
+ async resolveThinkingBudget(providerId, modelId, maxTokens, thinkLevel) {
3423
+ return resolveThinkingBudget(
3424
+ this.getProvider.bind(this),
3425
+ this.logger,
3426
+ providerId,
3427
+ modelId,
3428
+ maxTokens,
3429
+ thinkLevel
3430
+ );
3431
+ }
3432
+ // --------------------------------------------------------------------------
3433
+ // Transformer Chain Completion
3434
+ // --------------------------------------------------------------------------
3435
+ /**
3436
+ * Send a completion request using transformer chain
3437
+ */
3438
+ async completeWithTransformers(options) {
3439
+ return completeWithTransformers(
3440
+ options,
3441
+ this.llmConfig,
3442
+ this.getProvider.bind(this),
3443
+ this.resolveApiKey.bind(this),
3444
+ this.complete.bind(this),
3445
+ this.logger,
3446
+ this.usageRecorder ? { recorder: this.usageRecorder } : void 0
3447
+ );
3448
+ }
3449
+ /**
3450
+ * Send a streaming completion request using transformer chain
3451
+ */
3452
+ async completeStreamWithTransformers(options, callbacks) {
3453
+ return completeStreamWithTransformers(
3454
+ options,
3455
+ callbacks,
3456
+ this.llmConfig,
3457
+ this.getProvider.bind(this),
3458
+ this.resolveApiKey.bind(this),
3459
+ this.completeStream.bind(this),
3460
+ this.logger,
3461
+ this.usageRecorder ? { recorder: this.usageRecorder } : void 0
3462
+ );
3463
+ }
3464
+ // --------------------------------------------------------------------------
3465
+ // Tool-based Completion
3466
+ // --------------------------------------------------------------------------
3467
+ /**
3468
+ * Stream completion with MCP tools support (direct API call)
3469
+ * Implements agentic loop: calls LLM -> executes tools -> calls LLM again until done
3470
+ */
3471
+ async streamWithTools(options, callbacks, mcpService, builtinExecutor) {
3472
+ return streamWithTools(
3473
+ options,
3474
+ callbacks,
3475
+ mcpService,
3476
+ this.llmConfig,
3477
+ this.getProvider.bind(this),
3478
+ this.resolveApiKey.bind(this),
3479
+ this.logger,
3480
+ builtinExecutor
3481
+ );
3482
+ }
3483
+ };
3484
+ // Annotate the CommonJS export names for ESM import in node:
3485
+ 0 && (module.exports = {
3486
+ CompletionService
3487
+ });