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