@omnicross/core 0.1.0

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