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