@omnicross/core 0.1.1 → 0.1.2

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 (210) hide show
  1. package/dist/ApiConverter.cjs +3 -778
  2. package/dist/ApiConverter.js +12 -751
  3. package/dist/{CompletionService-DtOF_War.d.cts → CompletionService-D3a47vdU.d.cts} +2 -1
  4. package/dist/{CompletionService-7fCmKAP3.d.ts → CompletionService-DdWaAxq0.d.ts} +2 -1
  5. package/dist/auth/GeminiCodeAssistProjectResolver.cjs +27 -60
  6. package/dist/auth/GeminiCodeAssistProjectResolver.js +5 -9
  7. package/dist/builtin-web-fetch-77JZUZGM.cjs +100 -0
  8. package/dist/builtin-web-fetch-IGLBX2XY.js +100 -0
  9. package/dist/chunk-2DCNB7DF.cjs +514 -0
  10. package/dist/chunk-2FEVTJWG.js +4400 -0
  11. package/dist/chunk-3MEACFK3.js +193 -0
  12. package/dist/chunk-3OPYJG76.cjs +266 -0
  13. package/dist/chunk-46RLHI7D.js +0 -0
  14. package/dist/chunk-46ZIFWYG.cjs +277 -0
  15. package/dist/chunk-4NBS6KPV.cjs +43 -0
  16. package/dist/chunk-4VAVYUZS.cjs +991 -0
  17. package/dist/chunk-5ERIZW3D.js +44 -0
  18. package/dist/chunk-5HTVET6E.js +514 -0
  19. package/dist/chunk-5ZQBEOMD.js +764 -0
  20. package/dist/chunk-6VIXXLMX.cjs +14 -0
  21. package/dist/chunk-745DV5FL.js +147 -0
  22. package/dist/chunk-74TMJA7Z.js +53 -0
  23. package/dist/chunk-7VU7V2E4.js +0 -0
  24. package/dist/chunk-7YN22L2H.cjs +87 -0
  25. package/dist/chunk-A2YMUCBT.cjs +105 -0
  26. package/dist/chunk-AQ5TNQB7.js +13 -0
  27. package/dist/chunk-AVYJ4YBN.js +58 -0
  28. package/dist/chunk-C72S6TTV.cjs +49 -0
  29. package/dist/chunk-C7PSERMH.js +87 -0
  30. package/dist/chunk-CDHS2QDC.js +179 -0
  31. package/dist/chunk-DK4A7DLE.js +266 -0
  32. package/dist/chunk-DRWHTIIZ.js +13 -0
  33. package/dist/chunk-DWKQZ2SK.cjs +254 -0
  34. package/dist/chunk-E3WHL7CO.js +14 -0
  35. package/dist/chunk-EYZYXJTJ.cjs +1 -0
  36. package/dist/chunk-FMOBD2OQ.js +39 -0
  37. package/dist/chunk-FTQ7IQUC.js +991 -0
  38. package/dist/chunk-FZ46M34D.cjs +1 -0
  39. package/dist/chunk-G2FUJNA2.cjs +193 -0
  40. package/dist/chunk-G2UTNSGU.js +277 -0
  41. package/dist/chunk-GBHYAGX3.cjs +26 -0
  42. package/dist/chunk-GMMT7RVN.cjs +39 -0
  43. package/dist/chunk-H5JUT3KV.js +105 -0
  44. package/dist/chunk-HQP3K7PU.cjs +431 -0
  45. package/dist/chunk-JO5NLTLY.js +46 -0
  46. package/dist/chunk-JWC6APFQ.js +23 -0
  47. package/dist/chunk-KUU2RNG6.cjs +46 -0
  48. package/dist/chunk-LKZJEL6E.cjs +124 -0
  49. package/dist/chunk-MNYKI4CI.js +94 -0
  50. package/dist/chunk-MZNPGW5Q.js +26 -0
  51. package/dist/chunk-N3V2J5ZO.cjs +53 -0
  52. package/dist/chunk-NYUNU75A.cjs +44 -0
  53. package/dist/chunk-NZYN7C3P.cjs +179 -0
  54. package/dist/chunk-O466Y272.js +235 -0
  55. package/dist/chunk-PXUJF5HS.js +124 -0
  56. package/dist/chunk-QBZJ7P2T.cjs +13 -0
  57. package/dist/chunk-QOCNX236.js +46 -0
  58. package/dist/chunk-QXD4MV2X.cjs +23 -0
  59. package/dist/chunk-R3HK6R4B.cjs +13 -0
  60. package/dist/chunk-RSJ3TWJQ.cjs +186 -0
  61. package/dist/chunk-S4WBV25P.js +913 -0
  62. package/dist/chunk-SN3YWBX7.cjs +4400 -0
  63. package/dist/chunk-SPVWWUHX.cjs +46 -0
  64. package/dist/chunk-SUEGJ75D.cjs +764 -0
  65. package/dist/chunk-SVNDB62D.cjs +58 -0
  66. package/dist/chunk-UYPEN5XE.cjs +235 -0
  67. package/dist/chunk-V5KPWNYX.cjs +147 -0
  68. package/dist/chunk-VVEHS2LI.js +431 -0
  69. package/dist/chunk-XBSYYZIY.cjs +94 -0
  70. package/dist/chunk-YGXJGRFI.cjs +913 -0
  71. package/dist/chunk-YMFAJFQT.cjs +173 -0
  72. package/dist/chunk-Z7LUNECE.js +49 -0
  73. package/dist/chunk-Z7Y2NPHY.js +186 -0
  74. package/dist/chunk-ZJZAOKI7.js +254 -0
  75. package/dist/chunk-ZLX3KGXX.js +173 -0
  76. package/dist/chunk-ZSVQT3PW.js +43 -0
  77. package/dist/completion/ApiKeyPoolService.cjs +23 -48
  78. package/dist/completion/ApiKeyPoolService.d.cts +150 -2
  79. package/dist/completion/ApiKeyPoolService.d.ts +150 -2
  80. package/dist/completion/BuiltinToolExecutor.cjs +9 -324
  81. package/dist/completion/BuiltinToolExecutor.js +6 -290
  82. package/dist/completion/CompletionService.cjs +38 -3487
  83. package/dist/completion/CompletionService.d.cts +3 -2
  84. package/dist/completion/CompletionService.d.ts +3 -2
  85. package/dist/completion/CompletionService.js +34 -3457
  86. package/dist/completion/NativeSearchInjector.cjs +8 -192
  87. package/dist/completion/NativeSearchInjector.js +7 -162
  88. package/dist/completion/ProviderSearchInjector.cjs +12 -39
  89. package/dist/completion/native-search-types.cjs +13 -66
  90. package/dist/completion/native-search-types.js +7 -31
  91. package/dist/completion/openrouter-headers.cjs +15 -71
  92. package/dist/completion/openrouter-headers.js +8 -34
  93. package/dist/completion/openrouter-models.cjs +10 -67
  94. package/dist/completion/openrouter-models.js +4 -34
  95. package/dist/completion/types.cjs +1 -18
  96. package/dist/completion/url-builder.cjs +24 -138
  97. package/dist/completion/url-builder.js +12 -92
  98. package/dist/completion.cjs +55 -3989
  99. package/dist/completion.d.cts +3 -2
  100. package/dist/completion.d.ts +3 -2
  101. package/dist/completion.js +63 -3953
  102. package/dist/index.cjs +197 -10429
  103. package/dist/index.d.cts +9 -2
  104. package/dist/index.d.ts +9 -2
  105. package/dist/index.js +125 -10268
  106. package/dist/logger-4GvQNzhE.d.cts +22 -0
  107. package/dist/logger-4GvQNzhE.d.ts +22 -0
  108. package/dist/outbound-api/routeResolver.cjs +7 -217
  109. package/dist/outbound-api/routeResolver.d.cts +2 -1
  110. package/dist/outbound-api/routeResolver.d.ts +2 -1
  111. package/dist/outbound-api/routeResolver.js +6 -187
  112. package/dist/outbound-api/subscriptionRegistryPort.cjs +7 -37
  113. package/dist/outbound-api/subscriptionRegistryPort.d.cts +2 -1
  114. package/dist/outbound-api/subscriptionRegistryPort.d.ts +2 -1
  115. package/dist/outbound-api/subscriptionRegistryPort.js +4 -8
  116. package/dist/outbound-api/types.cjs +1 -18
  117. package/dist/outbound-api/types.d.cts +2 -1
  118. package/dist/outbound-api/types.d.ts +2 -1
  119. package/dist/outbound-api.cjs +35 -5221
  120. package/dist/outbound-api.d.cts +2 -1
  121. package/dist/outbound-api.d.ts +2 -1
  122. package/dist/outbound-api.js +54 -5194
  123. package/dist/pipeline/AuthSource.cjs +1 -18
  124. package/dist/pipeline/LlmConfigProviderAuth.cjs +5 -166
  125. package/dist/pipeline/LlmConfigProviderAuth.d.cts +2 -1
  126. package/dist/pipeline/LlmConfigProviderAuth.d.ts +2 -1
  127. package/dist/pipeline/LlmConfigProviderAuth.js +5 -139
  128. package/dist/pipeline/SubscriptionAuthSource.cjs +6 -128
  129. package/dist/pipeline/SubscriptionAuthSource.js +5 -99
  130. package/dist/pipeline/SubscriptionAuthStrategy.cjs +1 -18
  131. package/dist/pipeline/executeProviderCall.cjs +5 -69
  132. package/dist/pipeline/executeProviderCall.js +3 -42
  133. package/dist/pipeline/resolveProviderChain.cjs +5 -46
  134. package/dist/pipeline/resolveProviderChain.js +3 -19
  135. package/dist/pipeline/resolveSubscriptionChain.cjs +5 -67
  136. package/dist/pipeline/resolveSubscriptionChain.js +3 -40
  137. package/dist/ports/gemini-code-assist-resolver.cjs +7 -37
  138. package/dist/ports/gemini-code-assist-resolver.js +4 -8
  139. package/dist/ports/pricing-store.cjs +1 -0
  140. package/dist/ports/pricing-store.d.cts +50 -0
  141. package/dist/ports/pricing-store.d.ts +50 -0
  142. package/dist/ports/pricing-store.js +0 -0
  143. package/dist/ports/provider-config-source.cjs +1 -18
  144. package/dist/ports/usage-event-store.cjs +1 -0
  145. package/dist/ports/usage-event-store.d.cts +29 -0
  146. package/dist/ports/usage-event-store.d.ts +29 -0
  147. package/dist/ports/usage-event-store.js +0 -0
  148. package/dist/ports/web-search-backend.cjs +1 -18
  149. package/dist/ports.cjs +1 -18
  150. package/dist/ports.d.cts +6 -1
  151. package/dist/ports.d.ts +6 -1
  152. package/dist/provider-proxy/ProviderProxy.cjs +40 -4643
  153. package/dist/provider-proxy/ProviderProxy.d.cts +2 -1
  154. package/dist/provider-proxy/ProviderProxy.d.ts +2 -1
  155. package/dist/provider-proxy/ProviderProxy.js +35 -4613
  156. package/dist/provider-proxy/ingress/providerProxyShared.cjs +35 -2941
  157. package/dist/provider-proxy/ingress/providerProxyShared.d.cts +2 -1
  158. package/dist/provider-proxy/ingress/providerProxyShared.d.ts +2 -1
  159. package/dist/provider-proxy/ingress/providerProxyShared.js +41 -2914
  160. package/dist/provider-proxy/matchText.cjs +11 -72
  161. package/dist/provider-proxy/matchText.js +6 -39
  162. package/dist/provider-proxy/types.cjs +1 -18
  163. package/dist/provider-proxy/types.d.cts +2 -1
  164. package/dist/provider-proxy/types.d.ts +2 -1
  165. package/dist/provider-proxy.cjs +35 -4655
  166. package/dist/provider-proxy.d.cts +2 -1
  167. package/dist/provider-proxy.d.ts +2 -1
  168. package/dist/provider-proxy.js +39 -4628
  169. package/dist/serializeError.cjs +5 -81
  170. package/dist/serializeError.js +3 -54
  171. package/dist/sse-parser.cjs +7 -455
  172. package/dist/sse-parser.js +4 -426
  173. package/dist/transformer/TransformerChainExecutor.cjs +4 -318
  174. package/dist/transformer/TransformerChainExecutor.js +4 -291
  175. package/dist/transformer/TransformerService.cjs +5 -289
  176. package/dist/transformer/TransformerService.js +3 -262
  177. package/dist/transformer/anthropicBetaInject.cjs +7 -50
  178. package/dist/transformer/anthropicBetaInject.js +4 -21
  179. package/dist/transformer/transformers/AnthropicTransformer.cjs +3 -1014
  180. package/dist/transformer/transformers/AnthropicTransformer.js +3 -987
  181. package/dist/transformer/transformers/GeminiCodeAssistTransformer.cjs +4 -1106
  182. package/dist/transformer/transformers/GeminiCodeAssistTransformer.js +7 -1079
  183. package/dist/transformer/transformers/GeminiTransformer.cjs +4 -1010
  184. package/dist/transformer/transformers/GeminiTransformer.js +4 -983
  185. package/dist/transformer/transformers/OpenAIResponseTransformer.cjs +3 -535
  186. package/dist/transformer/transformers/OpenAIResponseTransformer.js +3 -510
  187. package/dist/transformer/transformers/OpenCodeGoTransformer.cjs +5 -72
  188. package/dist/transformer/transformers/OpenCodeGoTransformer.js +3 -45
  189. package/dist/transformer/transformers/ReasoningTransformer.cjs +3 -270
  190. package/dist/transformer/transformers/ReasoningTransformer.js +2 -249
  191. package/dist/transformer/transformers.cjs +10 -3177
  192. package/dist/transformer/transformers.js +23 -3158
  193. package/dist/transformer/types.cjs +1 -18
  194. package/dist/transformer.cjs +13 -3726
  195. package/dist/transformer.js +17 -3706
  196. package/dist/usage/pricing-engine.cjs +6 -0
  197. package/dist/usage/pricing-engine.d.cts +83 -0
  198. package/dist/usage/pricing-engine.d.ts +83 -0
  199. package/dist/usage/pricing-engine.js +6 -0
  200. package/dist/usage/usage-recorder.cjs +6 -0
  201. package/dist/usage/usage-recorder.d.cts +73 -0
  202. package/dist/usage/usage-recorder.d.ts +73 -0
  203. package/dist/usage/usage-recorder.js +6 -0
  204. package/dist/usage.cjs +11 -0
  205. package/dist/usage.d.cts +8 -0
  206. package/dist/usage.d.ts +8 -0
  207. package/dist/usage.js +11 -0
  208. package/package.json +1 -1
  209. package/dist/ApiKeyPoolService-BmMkau07.d.cts +0 -170
  210. package/dist/ApiKeyPoolService-BmMkau07.d.ts +0 -170
@@ -0,0 +1,4400 @@
1
+ import {
2
+ collectMatchText
3
+ } from "./chunk-QOCNX236.js";
4
+ import {
5
+ streamSSEResponse
6
+ } from "./chunk-VVEHS2LI.js";
7
+ import {
8
+ TransformerChainExecutor
9
+ } from "./chunk-G2UTNSGU.js";
10
+ import {
11
+ injectExtendedContextBeta
12
+ } from "./chunk-MZNPGW5Q.js";
13
+ import {
14
+ GeminiTransformer
15
+ } from "./chunk-C7PSERMH.js";
16
+ import {
17
+ OpenAIResponseTransformer
18
+ } from "./chunk-5HTVET6E.js";
19
+ import {
20
+ AnthropicTransformer
21
+ } from "./chunk-FTQ7IQUC.js";
22
+ import {
23
+ getGeminiCodeAssistResolver
24
+ } from "./chunk-DRWHTIIZ.js";
25
+ import {
26
+ executeProviderCall
27
+ } from "./chunk-JO5NLTLY.js";
28
+ import {
29
+ LlmConfigProviderAuth,
30
+ getProviderHeaders
31
+ } from "./chunk-PXUJF5HS.js";
32
+ import {
33
+ resolveProviderChain
34
+ } from "./chunk-JWC6APFQ.js";
35
+ import {
36
+ resolveSubscriptionChain
37
+ } from "./chunk-5ERIZW3D.js";
38
+ import {
39
+ SubscriptionAuthSource
40
+ } from "./chunk-74TMJA7Z.js";
41
+ import {
42
+ serializeError
43
+ } from "./chunk-AVYJ4YBN.js";
44
+ import {
45
+ buildProviderApiUrl,
46
+ resolveApiFormat,
47
+ resolveProviderEndpoint
48
+ } from "./chunk-H5JUT3KV.js";
49
+ import {
50
+ resolveRoute
51
+ } from "./chunk-3MEACFK3.js";
52
+ import {
53
+ convertAnthropicToOpenAI,
54
+ convertOpenAIToAnthropic
55
+ } from "./chunk-5ZQBEOMD.js";
56
+ import {
57
+ applyAugmentation
58
+ } from "./chunk-745DV5FL.js";
59
+ import {
60
+ OPENROUTER_APP_HEADERS,
61
+ isOpenRouterProvider
62
+ } from "./chunk-E3WHL7CO.js";
63
+ import {
64
+ NATIVE_SEARCH_TOOL_NAMES
65
+ } from "./chunk-FMOBD2OQ.js";
66
+
67
+ // src/completion/StreamEventBuffer.ts
68
+ var TERMINAL_TYPES = /* @__PURE__ */ new Set(["done", "error", "abort"]);
69
+ var QUEUE_CAP = 200;
70
+ var entries = /* @__PURE__ */ new Map();
71
+ function channelOf(streamId) {
72
+ return `completion:stream:${streamId}`;
73
+ }
74
+ function safeIsDestroyed(sender) {
75
+ try {
76
+ return sender.isDestroyed();
77
+ } catch {
78
+ return true;
79
+ }
80
+ }
81
+ function safeSend(sender, channel, payload) {
82
+ if (safeIsDestroyed(sender)) return false;
83
+ try {
84
+ sender.send(channel, payload);
85
+ return true;
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
90
+ function register(streamId, sender) {
91
+ entries.set(streamId, { sender, queue: [], attached: false, closed: false });
92
+ }
93
+ function emit(streamId, event) {
94
+ const entry = entries.get(streamId);
95
+ if (!entry) {
96
+ return;
97
+ }
98
+ if (safeIsDestroyed(entry.sender)) {
99
+ entries.delete(streamId);
100
+ return;
101
+ }
102
+ if (entry.attached) {
103
+ safeSend(entry.sender, channelOf(streamId), event);
104
+ return;
105
+ }
106
+ if (entry.queue.length >= QUEUE_CAP) {
107
+ const evictAt = entry.queue.findIndex((e) => !TERMINAL_TYPES.has(e.type));
108
+ if (evictAt >= 0) {
109
+ entry.queue.splice(evictAt, 1);
110
+ } else {
111
+ entry.queue.shift();
112
+ }
113
+ }
114
+ entry.queue.push(event);
115
+ }
116
+ function attach(streamId) {
117
+ const entry = entries.get(streamId);
118
+ if (!entry) return { ok: false, drained: 0 };
119
+ if (entry.attached) return { ok: true, drained: 0 };
120
+ const channel = channelOf(streamId);
121
+ let drained = 0;
122
+ for (const event of entry.queue) {
123
+ if (safeSend(entry.sender, channel, event)) {
124
+ drained++;
125
+ }
126
+ }
127
+ entry.queue = [];
128
+ entry.attached = true;
129
+ return { ok: true, drained };
130
+ }
131
+ function release(streamId) {
132
+ const entry = entries.get(streamId);
133
+ if (!entry) return;
134
+ if (entry.closed) return;
135
+ entry.closed = true;
136
+ if (entry.attached) {
137
+ entries.delete(streamId);
138
+ return;
139
+ }
140
+ setImmediate(() => {
141
+ entries.delete(streamId);
142
+ });
143
+ }
144
+
145
+ // src/completion/message-converter.ts
146
+ var OPENROUTER_AUDIO_FORMAT_MAP = {
147
+ "audio/wav": "wav",
148
+ "audio/x-wav": "wav",
149
+ "audio/wave": "wav",
150
+ "audio/mpeg": "mp3",
151
+ "audio/mp3": "mp3",
152
+ "audio/aiff": "aiff",
153
+ "audio/x-aiff": "aiff",
154
+ "audio/aac": "aac",
155
+ "audio/ogg": "ogg",
156
+ "audio/flac": "flac",
157
+ "audio/x-flac": "flac",
158
+ "audio/mp4": "m4a",
159
+ "audio/m4a": "m4a",
160
+ "audio/x-m4a": "m4a",
161
+ "audio/L16": "pcm16",
162
+ "audio/L24": "pcm24"
163
+ };
164
+ var MAX_INLINE_VIDEO_BYTES = 25 * 1024 * 1024;
165
+ function decodeDataUrl(url) {
166
+ const match = url.match(/^data:([^;]+);base64,(.+)$/);
167
+ if (!match) return null;
168
+ return { mimeType: match[1], data: match[2] };
169
+ }
170
+ function looksLikeRemoteUrl(url) {
171
+ return /^https?:\/\//i.test(url);
172
+ }
173
+ function resolveAudioFormat(mimeType, sourceUrl) {
174
+ const candidate = (mimeType || "").toLowerCase();
175
+ const mapped = OPENROUTER_AUDIO_FORMAT_MAP[candidate];
176
+ if (mapped) return mapped;
177
+ const decoded = decodeDataUrl(sourceUrl);
178
+ if (decoded) {
179
+ const mappedFromUrl = OPENROUTER_AUDIO_FORMAT_MAP[decoded.mimeType.toLowerCase()];
180
+ if (mappedFromUrl) return mappedFromUrl;
181
+ }
182
+ throw new Error(
183
+ `Unsupported audio format for OpenAI/OpenRouter chat input: ${mimeType || "unknown"}. Supported: wav, mp3, aiff, aac, ogg, flac, m4a, pcm16, pcm24.`
184
+ );
185
+ }
186
+ function convertMessageToOpenAI(msg) {
187
+ const role = msg.role;
188
+ const hasImages = msg.images && msg.images.length > 0;
189
+ const hasAudios = msg.audios && msg.audios.length > 0;
190
+ const hasVideos = msg.videos && msg.videos.length > 0;
191
+ if (!hasImages && !hasAudios && !hasVideos) {
192
+ return { role, content: msg.content };
193
+ }
194
+ const content = [];
195
+ if (msg.content) {
196
+ content.push({ type: "text", text: msg.content });
197
+ }
198
+ if (msg.images) {
199
+ for (const img of msg.images) {
200
+ content.push({
201
+ type: "image_url",
202
+ image_url: { url: img.url }
203
+ });
204
+ }
205
+ }
206
+ if (msg.audios) {
207
+ for (const audio of msg.audios) {
208
+ const decoded = decodeDataUrl(audio.url);
209
+ if (decoded) {
210
+ const format = resolveAudioFormat(decoded.mimeType, audio.url);
211
+ content.push({ type: "input_audio", input_audio: { data: decoded.data, format } });
212
+ } else if (audio.mimeType) {
213
+ content.push({ type: "audio_url", audio_url: { url: audio.url, format: audio.mimeType } });
214
+ } else {
215
+ content.push({ type: "audio_url", audio_url: { url: audio.url } });
216
+ }
217
+ }
218
+ }
219
+ if (msg.videos) {
220
+ for (const video of msg.videos) {
221
+ if (looksLikeRemoteUrl(video.url)) {
222
+ content.push({ type: "video_url", video_url: { url: video.url } });
223
+ continue;
224
+ }
225
+ const decoded = decodeDataUrl(video.url);
226
+ if (decoded) {
227
+ const sizeBytes = Math.floor(decoded.data.length * 3 / 4);
228
+ if (sizeBytes > MAX_INLINE_VIDEO_BYTES) {
229
+ const sizeMb = (sizeBytes / 1024 / 1024).toFixed(1);
230
+ throw new Error(
231
+ `Video too large for inline upload: ${sizeMb} MB exceeds the 25 MB cap. Use a publicly accessible HTTPS URL instead.`
232
+ );
233
+ }
234
+ content.push({ type: "video_url", video_url: { url: video.url } });
235
+ } else {
236
+ content.push({ type: "video_url", video_url: { url: video.url } });
237
+ }
238
+ }
239
+ }
240
+ return { role, content };
241
+ }
242
+ function convertMessageToAnthropic(msg) {
243
+ const role = msg.role === "system" ? "user" : msg.role;
244
+ const hasImages = msg.images && msg.images.length > 0;
245
+ const hasAudios = msg.audios && msg.audios.length > 0;
246
+ if (!hasImages && !hasAudios) {
247
+ return { role, content: msg.content };
248
+ }
249
+ const content = [];
250
+ if (msg.content) {
251
+ content.push({ type: "text", text: msg.content });
252
+ }
253
+ if (msg.images) {
254
+ for (const img of msg.images) {
255
+ let base64Data = img.url;
256
+ let mediaType = img.mimeType || "image/jpeg";
257
+ if (img.url.startsWith("data:")) {
258
+ const match = img.url.match(/^data:([^;]+);base64,(.+)$/);
259
+ if (match) {
260
+ mediaType = match[1];
261
+ base64Data = match[2];
262
+ }
263
+ }
264
+ content.push({
265
+ type: "image",
266
+ source: { type: "base64", media_type: mediaType, data: base64Data }
267
+ });
268
+ }
269
+ }
270
+ if (msg.audios) {
271
+ for (const audio of msg.audios) {
272
+ let base64Data = audio.url;
273
+ let mediaType = audio.mimeType || "audio/wav";
274
+ if (audio.url.startsWith("data:")) {
275
+ const match = audio.url.match(/^data:([^;]+);base64,(.+)$/);
276
+ if (match) {
277
+ mediaType = match[1];
278
+ base64Data = match[2];
279
+ }
280
+ }
281
+ content.push({
282
+ type: "audio",
283
+ source: { type: "base64", media_type: mediaType, data: base64Data }
284
+ });
285
+ }
286
+ }
287
+ return { role, content };
288
+ }
289
+ function convertMessageToGemini(msg) {
290
+ const role = msg.role === "assistant" ? "model" : msg.role;
291
+ const parts = [];
292
+ if (msg.content) {
293
+ parts.push({ text: msg.content });
294
+ }
295
+ if (msg.images && msg.images.length > 0) {
296
+ for (const img of msg.images) {
297
+ let base64Data = img.url;
298
+ let mimeType = img.mimeType || "image/jpeg";
299
+ if (img.url.startsWith("data:")) {
300
+ const match = img.url.match(/^data:([^;]+);base64,(.+)$/);
301
+ if (match) {
302
+ mimeType = match[1];
303
+ base64Data = match[2];
304
+ }
305
+ }
306
+ parts.push({ inlineData: { mimeType, data: base64Data } });
307
+ }
308
+ }
309
+ if (msg.audios && msg.audios.length > 0) {
310
+ for (const audio of msg.audios) {
311
+ let base64Data = audio.url;
312
+ let mimeType = audio.mimeType || "audio/wav";
313
+ if (audio.url.startsWith("data:")) {
314
+ const match = audio.url.match(/^data:([^;]+);base64,(.+)$/);
315
+ if (match) {
316
+ mimeType = match[1];
317
+ base64Data = match[2];
318
+ }
319
+ }
320
+ parts.push({ inlineData: { mimeType, data: base64Data } });
321
+ }
322
+ }
323
+ return { role, parts };
324
+ }
325
+
326
+ // src/completion/openrouter-utils.ts
327
+ function getOpenRouterProviderConfig(provider, modelId) {
328
+ if (!isOpenRouterProvider(provider)) {
329
+ return void 0;
330
+ }
331
+ const modelConfig = provider.modelConfigs?.find((m) => m.id === modelId);
332
+ return modelConfig?.openRouterProvider;
333
+ }
334
+ function addOpenRouterProviderToRequest(requestBody, provider, modelId) {
335
+ const providerRouting = getOpenRouterProviderConfig(provider, modelId);
336
+ if (!providerRouting) {
337
+ return requestBody;
338
+ }
339
+ const hasConfig = Object.values(providerRouting).some(
340
+ (v) => v !== void 0 && v !== null && (Array.isArray(v) ? v.length > 0 : true)
341
+ );
342
+ if (!hasConfig) {
343
+ return requestBody;
344
+ }
345
+ return {
346
+ ...requestBody,
347
+ provider: providerRouting
348
+ };
349
+ }
350
+
351
+ // src/completion/DirectApiHandler.ts
352
+ import { getOpenAIReasoningEffort } from "@omnicross/contracts/thinking-config";
353
+
354
+ // src/provider-proxy/ProviderProxy.ts
355
+ import http from "http";
356
+
357
+ // src/provider-proxy/providerProxyRouteMap.ts
358
+ import { randomBytes } from "crypto";
359
+ var DEFAULT_ROUTE_IDLE_MS = 10 * 60 * 1e3;
360
+ var TOKEN_BYTES = 32;
361
+ var ProviderProxyRouteMap = class {
362
+ constructor(defaultIdleMs = DEFAULT_ROUTE_IDLE_MS) {
363
+ this.defaultIdleMs = defaultIdleMs;
364
+ }
365
+ defaultIdleMs;
366
+ routes = /* @__PURE__ */ new Map();
367
+ /**
368
+ * Register a route for one run and return its crypto-random token. The
369
+ * caller (next batch) injects the token as the forwarded auth-header sentinel
370
+ * (`ANTHROPIC_AUTH_TOKEN` / `OPENAI_API_KEY`). Optionally override the idle
371
+ * timeout (tests use a short one).
372
+ */
373
+ addRoute(context, idleMs) {
374
+ const token = randomBytes(TOKEN_BYTES).toString("hex");
375
+ const entry = {
376
+ context,
377
+ idleTimer: null,
378
+ idleMs: idleMs ?? this.defaultIdleMs
379
+ };
380
+ this.routes.set(token, entry);
381
+ this.armIdleTimer(token, entry);
382
+ return token;
383
+ }
384
+ /**
385
+ * Look up a route by its token, touching the idle timer so an active run's
386
+ * context survives. Returns `undefined` on a miss / reaped entry — the caller
387
+ * rejects (no fallback).
388
+ */
389
+ lookup(token) {
390
+ if (!token) return void 0;
391
+ const entry = this.routes.get(token);
392
+ if (!entry) return void 0;
393
+ this.armIdleTimer(token, entry);
394
+ return entry.context;
395
+ }
396
+ /** Remove a route at run end. Returns true if an entry existed. */
397
+ removeRoute(token) {
398
+ const entry = this.routes.get(token);
399
+ if (!entry) return false;
400
+ this.clearIdleTimer(entry);
401
+ this.routes.delete(token);
402
+ return true;
403
+ }
404
+ /** Current live-route count (tests / diagnostics). */
405
+ size() {
406
+ return this.routes.size;
407
+ }
408
+ /** Whether a token currently resolves (does NOT touch the idle timer). */
409
+ has(token) {
410
+ return this.routes.has(token);
411
+ }
412
+ /** Tear down every route (proxy stop / app teardown). */
413
+ clear() {
414
+ for (const entry of this.routes.values()) {
415
+ this.clearIdleTimer(entry);
416
+ }
417
+ this.routes.clear();
418
+ }
419
+ // ---------------------------------------------------------------------------
420
+ // Idle reaping (modeled on AcpSessionManager.armIdleTimer / clearIdleTimer)
421
+ // ---------------------------------------------------------------------------
422
+ armIdleTimer(token, entry) {
423
+ this.clearIdleTimer(entry);
424
+ entry.idleTimer = setTimeout(() => {
425
+ this.routes.delete(token);
426
+ }, entry.idleMs);
427
+ entry.idleTimer.unref?.();
428
+ }
429
+ clearIdleTimer(entry) {
430
+ if (entry.idleTimer) {
431
+ clearTimeout(entry.idleTimer);
432
+ entry.idleTimer = null;
433
+ }
434
+ }
435
+ };
436
+
437
+ // src/provider-proxy/usage/recordAnthropicUsage.ts
438
+ function readAnthropicUsage(usage) {
439
+ if (!usage) return null;
440
+ const inputTokens = Number(usage.input_tokens) || 0;
441
+ const outputTokens = Number(usage.output_tokens) || 0;
442
+ const cacheReadTokens = Number(usage.cache_read_input_tokens) || 0;
443
+ const cacheCreationTokens = Number(usage.cache_creation_input_tokens) || 0;
444
+ return {
445
+ inputTokens,
446
+ outputTokens,
447
+ cacheReadTokens,
448
+ cacheCreationTokens,
449
+ // The Anthropic wire carries no separate reasoning-token field; thinking
450
+ // tokens are folded into `output_tokens`.
451
+ reasoningTokens: 0
452
+ };
453
+ }
454
+ function recordAnthropicNonStreamUsage(recorder, bodyText, attribution) {
455
+ try {
456
+ const parsed = JSON.parse(bodyText);
457
+ const tapped = readAnthropicUsage(parsed.usage);
458
+ if (!tapped) return;
459
+ recorder.record({
460
+ messageId: null,
461
+ parentMessageId: null,
462
+ sessionId: attribution.sessionId,
463
+ providerId: attribution.providerId,
464
+ model: attribution.model,
465
+ apiKeyId: null,
466
+ engineOrigin: "codex-ingress",
467
+ usage: tapped,
468
+ rawUsage: parsed.usage
469
+ });
470
+ } catch {
471
+ }
472
+ }
473
+
474
+ // src/provider-proxy/ingress/providerProxyShared.ts
475
+ var sharedExecutor = null;
476
+ function getSharedExecutor() {
477
+ if (!sharedExecutor) {
478
+ sharedExecutor = new TransformerChainExecutor();
479
+ }
480
+ return sharedExecutor;
481
+ }
482
+ var sharedResponses = null;
483
+ function getResponsesEndpointTransformer() {
484
+ if (!sharedResponses) sharedResponses = new OpenAIResponseTransformer();
485
+ return sharedResponses;
486
+ }
487
+ var sharedAnthropic = null;
488
+ function getAnthropicEndpointTransformer() {
489
+ if (!sharedAnthropic) sharedAnthropic = new AnthropicTransformer();
490
+ return sharedAnthropic;
491
+ }
492
+ var sharedGemini = null;
493
+ function getGeminiEndpointTransformer() {
494
+ if (!sharedGemini) sharedGemini = new GeminiTransformer();
495
+ return sharedGemini;
496
+ }
497
+ function readBody(req) {
498
+ return new Promise((resolve, reject) => {
499
+ const chunks = [];
500
+ req.on("data", (chunk) => chunks.push(chunk));
501
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
502
+ req.on("error", reject);
503
+ });
504
+ }
505
+ async function relayResponse(res, providerResponse, isStream) {
506
+ const contentType = providerResponse.headers.get("Content-Type") ?? "";
507
+ const status = providerResponse.status && providerResponse.status >= 100 ? providerResponse.status : 200;
508
+ if (isStream || contentType.includes("text/event-stream")) {
509
+ res.writeHead(status, {
510
+ "Content-Type": "text/event-stream",
511
+ "Cache-Control": "no-cache",
512
+ Connection: "keep-alive"
513
+ });
514
+ if (!providerResponse.body) {
515
+ res.end();
516
+ return null;
517
+ }
518
+ const reader = providerResponse.body.getReader();
519
+ try {
520
+ for (; ; ) {
521
+ const { done, value } = await reader.read();
522
+ if (done) break;
523
+ res.write(value);
524
+ }
525
+ } finally {
526
+ reader.releaseLock();
527
+ res.end();
528
+ }
529
+ return null;
530
+ }
531
+ const bodyText = await providerResponse.text();
532
+ res.writeHead(status, { "Content-Type": contentType.includes("json") ? contentType : "application/json" });
533
+ res.end(bodyText);
534
+ return bodyText;
535
+ }
536
+ function writeError(res, status, message) {
537
+ if (res.headersSent) return;
538
+ res.writeHead(status, { "Content-Type": "application/json" });
539
+ res.end(JSON.stringify({ error: { type: "provider_proxy_error", message } }));
540
+ }
541
+ function resolveApiKey(apiKey) {
542
+ if (!apiKey) return "";
543
+ if (apiKey.startsWith("$")) {
544
+ return process.env[apiKey.slice(1)] || "";
545
+ }
546
+ return apiKey;
547
+ }
548
+ async function resolvePoolBoundKey(deps, providerId, provider, sessionId) {
549
+ if (deps.apiKeyPool && sessionId) {
550
+ const poolKey = await deps.apiKeyPool.getKeyForSession(providerId, sessionId);
551
+ if (poolKey) return poolKey;
552
+ }
553
+ return resolveApiKey(resolveProviderEndpoint(provider).apiKey);
554
+ }
555
+
556
+ // src/provider-proxy/ingress/anthropicSubscriptionPlan.ts
557
+ async function buildSubscriptionPlan(res, route, deps, anthropicBody, routeModel, isStream) {
558
+ const profile = route.subscriptionProfile;
559
+ if (!profile) {
560
+ writeError(res, 502, "Subscription mode requires an Anthropic subscription profile");
561
+ return null;
562
+ }
563
+ let resolvedModel = routeModel;
564
+ let scenario;
565
+ if (profile.modelMapper) {
566
+ const summary = buildSubscriptionRequestSummary(anthropicBody);
567
+ const mapped = profile.modelMapper(routeModel, summary, route.subscriptionConfig);
568
+ resolvedModel = mapped.resolvedModel;
569
+ scenario = mapped.scenario;
570
+ }
571
+ const plan = buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isStream, scenario);
572
+ if (!plan) {
573
+ writeError(res, 502, "Subscription profile is missing resolveUpstreamUrl");
574
+ return null;
575
+ }
576
+ return plan;
577
+ }
578
+ function buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isStream, scenario) {
579
+ const upstreamUrl = profile.resolveUpstreamUrl?.(resolvedModel, route.subscriptionConfig);
580
+ if (!upstreamUrl) return null;
581
+ const auth = route.auth ?? new SubscriptionAuthSource(profile);
582
+ const sameFormat = profile.mode === "pass-through" || upstreamUrl.endsWith("/v1/messages");
583
+ const overrideNames = profile.resolveProviderTransformerNames?.(
584
+ resolvedModel,
585
+ route.subscriptionConfig
586
+ );
587
+ const chain = sameFormat ? { providerTransformers: [], modelTransformers: [] } : resolveSubscriptionChain(
588
+ profile,
589
+ deps.llmConfig.getTransformerService(),
590
+ getAnthropicEndpointTransformer(),
591
+ overrideNames
592
+ );
593
+ const transformerProvider = {
594
+ name: profile.authStrategy.providerId,
595
+ baseUrl: upstreamUrl,
596
+ apiKey: "",
597
+ models: [resolvedModel]
598
+ };
599
+ return {
600
+ auth,
601
+ chain,
602
+ transformerProvider,
603
+ resolvedModel,
604
+ isStream,
605
+ // EXCEPTION (the Responses chain) (Phase 3 task 3.3): the `openai-response`
606
+ // provider transformer emits an ABSOLUTE-path `config.url`
607
+ // (`new URL('/v1/responses', baseUrl)`) that DISCARDS any base PATH prefix — so
608
+ // for a path-prefixed base (zen `https://opencode.ai/zen/v1/responses` → drops
609
+ // `/zen/`; codex `https://chatgpt.com/backend-api/codex/responses` → drops
610
+ // `/backend-api/codex/`). On the core `/v1/messages` plan the endpoint
611
+ // transformer is `anthropic` and the codex/zen-responses chain is
612
+ // `['openai-response']` (endpoint != provider), so `transformRequestIn` RUNS
613
+ // and emits the lossy URL. The guard makes this safe REGARDLESS of whether it
614
+ // ran: `usesResponsesChain(['openai-response'])` captures BOTH codex and zen
615
+ // responses, so the profile's complete `upstreamUrl` always wins (codex → its
616
+ // backend-api/codex/responses; zen → its `/zen/...` endpoint). The gemini
617
+ // chain's `config.url` is RELATIVE (`./{model}:{action}`) and PRESERVES the
618
+ // base path, so it correctly wins. The bypass-by-name optimization only applies
619
+ // on the core OpenAI-Responses ingress (endpoint == `openai-response`), NOT
620
+ // here. `// UNVERIFIED (no live zen key)`.
621
+ resolveUrl: usesResponsesChain(overrideNames ?? profile.providerTransformerNames) ? () => upstreamUrl : (config) => config.url instanceof URL ? config.url.toString() : typeof config.url === "string" ? config.url : upstreamUrl,
622
+ upstreamUrl,
623
+ sameFormat,
624
+ isSubscription: true,
625
+ scenario
626
+ };
627
+ }
628
+ function usesResponsesChain(names) {
629
+ return !!names && names.includes("openai-response");
630
+ }
631
+ function buildSubscriptionRequestSummary(anthropicBody) {
632
+ const messages = Array.isArray(anthropicBody.messages) ? anthropicBody.messages : [];
633
+ let totalChars = collectText(anthropicBody.system);
634
+ for (const message of messages) {
635
+ if (message && typeof message === "object") {
636
+ totalChars += collectText(message.content);
637
+ }
638
+ }
639
+ return {
640
+ messageCount: messages.length,
641
+ estimatedInputTokens: Math.ceil(totalChars / 4),
642
+ // Single source of truth (shared with the dispatcher builder) so both
643
+ // ingress paths yield identical `matchText` for the same body.
644
+ matchText: collectMatchText(anthropicBody)
645
+ };
646
+ }
647
+ function collectText(value) {
648
+ if (typeof value === "string") return value.length;
649
+ if (Array.isArray(value)) {
650
+ let sum = 0;
651
+ for (const item of value) sum += collectText(item);
652
+ return sum;
653
+ }
654
+ if (value && typeof value === "object") {
655
+ const text = value.text;
656
+ if (typeof text === "string") return text.length;
657
+ }
658
+ return 0;
659
+ }
660
+ async function runPipeline(anthropicBody, plan) {
661
+ const executor = getSharedExecutor();
662
+ const endpointTransformer = getAnthropicEndpointTransformer();
663
+ const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
664
+ const authHeaders = {};
665
+ await auth.applyHeaders(authHeaders, { upstreamUrl, model: resolvedModel });
666
+ let rawStatus = null;
667
+ const { response } = await executeProviderCall({
668
+ executor,
669
+ request: anthropicBody,
670
+ provider: transformerProvider,
671
+ chain,
672
+ endpointTransformer,
673
+ resolveUrl,
674
+ buildHeaders: (config) => {
675
+ const headers = { ...authHeaders };
676
+ if (config.headers) {
677
+ for (const [key, value] of Object.entries(config.headers)) {
678
+ if (value !== void 0 && !(key in headers)) headers[key] = value;
679
+ }
680
+ }
681
+ return headers;
682
+ },
683
+ fetchFn: (url, headers, body) => {
684
+ console.info(`[ProviderProxy:anthropic] -> ${url} model=${resolvedModel} stream=${isStream}`);
685
+ return fetch(url, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => {
686
+ rawStatus = r.status;
687
+ return r;
688
+ });
689
+ },
690
+ runResponseChain: true
691
+ });
692
+ return { response, rawStatus };
693
+ }
694
+ async function runSubscriptionSameFormatFetch(rawBody, plan) {
695
+ const headers = { "content-type": "application/json" };
696
+ await plan.auth.applyHeaders(headers, {
697
+ upstreamUrl: plan.upstreamUrl,
698
+ model: plan.resolvedModel
699
+ });
700
+ console.info(
701
+ `[ProviderProxy:anthropic] (subscription same-format) -> ${plan.upstreamUrl} model=${plan.resolvedModel} stream=${plan.isStream}`
702
+ );
703
+ const response = await fetch(plan.upstreamUrl, { method: "POST", headers, body: rawBody });
704
+ return { response, rawStatus: response.status };
705
+ }
706
+ async function runSubscriptionAttemptWith401Retry(anthropicBody, relayBody, plan) {
707
+ const runOnce = () => plan.sameFormat ? runSubscriptionSameFormatFetch(relayBody, plan) : runPipeline(anthropicBody, plan);
708
+ const first = await runOnce();
709
+ if (first.rawStatus !== 401) return first;
710
+ const refreshed = await plan.auth.onUnauthorized?.();
711
+ if (!refreshed) {
712
+ console.warn("[ProviderProxy:anthropic] 401 not recoverable (onUnauthorized returned false)");
713
+ return first;
714
+ }
715
+ console.info("[ProviderProxy:anthropic] 401 \u2192 token refreshed; retrying once");
716
+ return runOnce();
717
+ }
718
+ function isFallbackEligibleStatus(rawStatus) {
719
+ return rawStatus === null || rawStatus >= 500 || rawStatus === 429;
720
+ }
721
+ function breakerOutcome(rawStatus) {
722
+ if (isFallbackEligibleStatus(rawStatus)) return "failure";
723
+ if (rawStatus !== null && rawStatus >= 200 && rawStatus < 300) return "success";
724
+ return "neutral";
725
+ }
726
+ function recordBreakerOutcome(profile, modelId, rawStatus) {
727
+ const outcome = breakerOutcome(rawStatus);
728
+ if (outcome === "neutral") return;
729
+ profile?.recordModelOutcome?.(modelId, outcome === "success");
730
+ }
731
+ async function runSubscriptionAttemptOutcome(anthropicBody, relayBody, plan) {
732
+ try {
733
+ return { kind: "result", result: await runSubscriptionAttemptWith401Retry(anthropicBody, relayBody, plan) };
734
+ } catch (error) {
735
+ return { kind: "thrown", error };
736
+ }
737
+ }
738
+ function outcomeStatus(outcome) {
739
+ return outcome.kind === "thrown" ? null : outcome.result.rawStatus;
740
+ }
741
+ function settleOutcome(outcome) {
742
+ if (outcome.kind === "thrown") throw outcome.error;
743
+ return outcome.result;
744
+ }
745
+ async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialPlan, route, deps) {
746
+ const profile = route.subscriptionProfile;
747
+ const scenario = initialPlan.scenario;
748
+ if (!profile?.nextFallback || scenario === void 0) {
749
+ const loneOutcome = await runSubscriptionAttemptOutcome(anthropicBody, rawBody, initialPlan);
750
+ recordBreakerOutcome(profile, initialPlan.resolvedModel, outcomeStatus(loneOutcome));
751
+ return settleOutcome(loneOutcome);
752
+ }
753
+ const attempted = [];
754
+ let plan = initialPlan;
755
+ let firstRelayBody = rawBody;
756
+ let firstBodyObj = anthropicBody;
757
+ if (profile.allowModel && !profile.allowModel(initialPlan.resolvedModel)) {
758
+ attempted.push(initialPlan.resolvedModel);
759
+ const firstAdmitting = profile.nextFallback(scenario, attempted, route.subscriptionConfig);
760
+ const gatedPlan = firstAdmitting ? buildSubscriptionIterationPlan(profile, route, deps, firstAdmitting.modelId, initialPlan.isStream, scenario) : null;
761
+ if (firstAdmitting && gatedPlan) {
762
+ console.warn(
763
+ `[ProviderProxy:anthropic] subscription primary ${initialPlan.resolvedModel} circuit open -> first admitting fallback ${firstAdmitting.modelId}`
764
+ );
765
+ plan = gatedPlan;
766
+ firstBodyObj = { ...anthropicBody, model: firstAdmitting.modelId };
767
+ firstRelayBody = gatedPlan.sameFormat ? JSON.stringify(firstBodyObj) : rawBody;
768
+ } else {
769
+ attempted.length = 0;
770
+ console.warn(
771
+ `[ProviderProxy:anthropic] subscription all opencodego circuits open -> fail open to primary ${initialPlan.resolvedModel}`
772
+ );
773
+ }
774
+ }
775
+ let outcome = await runSubscriptionAttemptOutcome(firstBodyObj, firstRelayBody, plan);
776
+ recordBreakerOutcome(profile, plan.resolvedModel, outcomeStatus(outcome));
777
+ if (!attempted.includes(plan.resolvedModel)) attempted.push(plan.resolvedModel);
778
+ while (attempted.length < MAX_FALLBACK_ATTEMPTS) {
779
+ if (!isFallbackEligibleStatus(outcomeStatus(outcome))) return settleOutcome(outcome);
780
+ const next = profile.nextFallback(scenario, attempted, route.subscriptionConfig);
781
+ if (!next) return settleOutcome(outcome);
782
+ const nextPlan = buildSubscriptionIterationPlan(
783
+ profile,
784
+ route,
785
+ deps,
786
+ next.modelId,
787
+ plan.isStream,
788
+ scenario
789
+ );
790
+ if (!nextPlan) return settleOutcome(outcome);
791
+ const sinceLabel = outcome.kind === "thrown" ? "thrown error" : `status ${String(outcome.result.rawStatus)}`;
792
+ console.warn(
793
+ `[ProviderProxy:anthropic] subscription fallback ${plan.resolvedModel} -> ${next.modelId} after ${sinceLabel}`
794
+ );
795
+ const fallbackBodyObj = { ...anthropicBody, model: next.modelId };
796
+ const fallbackRelayBody = nextPlan.sameFormat ? JSON.stringify(fallbackBodyObj) : rawBody;
797
+ plan = nextPlan;
798
+ attempted.push(next.modelId);
799
+ outcome = await runSubscriptionAttemptOutcome(fallbackBodyObj, fallbackRelayBody, plan);
800
+ recordBreakerOutcome(profile, plan.resolvedModel, outcomeStatus(outcome));
801
+ }
802
+ return settleOutcome(outcome);
803
+ }
804
+ var MAX_FALLBACK_ATTEMPTS = 3;
805
+
806
+ // src/provider-proxy/ingress/anthropicMessagesByo.ts
807
+ async function handleAnthropicMessagesByo(res, rawBody, route, deps, options = {}) {
808
+ let anthropicBody;
809
+ try {
810
+ anthropicBody = JSON.parse(rawBody);
811
+ } catch {
812
+ writeError(res, 400, "Invalid JSON in request body");
813
+ return;
814
+ }
815
+ const isStream = anthropicBody.stream === true;
816
+ const resolvedModel = route.model;
817
+ anthropicBody.model = resolvedModel;
818
+ try {
819
+ const plan = route.authMode === "subscription" ? await buildSubscriptionPlan(res, route, deps, anthropicBody, resolvedModel, isStream) : await buildByoPlan(res, route, deps, resolvedModel, isStream);
820
+ if (!plan) return;
821
+ anthropicBody.model = plan.resolvedModel;
822
+ const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry(anthropicBody, rawBody, plan, route, deps) : await runPipelineWithPoolReporting(anthropicBody, rawBody, plan, options);
823
+ const bodyText = await relayResponse(res, providerResponse.response, isStream);
824
+ if (bodyText && deps.usageRecorder) {
825
+ recordAnthropicNonStreamUsage(deps.usageRecorder, bodyText, {
826
+ sessionId: route.sessionId,
827
+ providerId: route.providerId ?? "anthropic",
828
+ model: plan.resolvedModel
829
+ });
830
+ }
831
+ } catch (err) {
832
+ const errMsg = serializeError(err);
833
+ console.error("[ProviderProxy:anthropic] Pipeline error:", errMsg);
834
+ writeError(res, 502, errMsg);
835
+ }
836
+ }
837
+ async function buildByoPlan(res, route, deps, resolvedModel, isStream) {
838
+ const providerId = route.providerId;
839
+ if (!providerId) {
840
+ writeError(res, 502, "BYO route is missing a providerId");
841
+ return null;
842
+ }
843
+ const provider = await deps.llmConfig.getProvider(providerId);
844
+ if (!provider) {
845
+ writeError(res, 502, `Provider not found: ${providerId}`);
846
+ return null;
847
+ }
848
+ const apiKey = await resolvePoolBoundKey(deps, providerId, provider, route.sessionId);
849
+ if (!apiKey) {
850
+ writeError(res, 502, "API key not configured");
851
+ return null;
852
+ }
853
+ const auth = route.auth ?? new LlmConfigProviderAuth({
854
+ provider,
855
+ apiKey,
856
+ apiKeyPool: deps.apiKeyPool ?? null,
857
+ providerId,
858
+ sessionId: route.sessionId
859
+ });
860
+ const { chain } = await resolveProviderChain(deps.llmConfig, providerId, resolvedModel);
861
+ const transformerProvider = {
862
+ name: provider.name,
863
+ baseUrl: provider.api_base_url,
864
+ apiKey,
865
+ models: provider.models || []
866
+ };
867
+ const byoUrl = buildProviderApiUrl(provider, { model: resolvedModel, stream: isStream });
868
+ const sameFormat = route.targetProviderFormat === "anthropic" || resolveApiFormat(provider) === "anthropic";
869
+ return {
870
+ auth,
871
+ chain,
872
+ transformerProvider,
873
+ resolvedModel,
874
+ isStream,
875
+ resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : byoUrl,
876
+ upstreamUrl: byoUrl,
877
+ sameFormat,
878
+ isSubscription: false,
879
+ provider,
880
+ apiKey,
881
+ extendedContextEnabled: route.anthropicSdkHints?.extendedContext?.enabled ?? false
882
+ };
883
+ }
884
+ async function runSameFormatFetch(rawBody, plan, options, keyOverride) {
885
+ const { provider, apiKey, resolvedModel, isStream, extendedContextEnabled } = plan;
886
+ if (!provider) {
887
+ throw new Error("[ProviderProxy:anthropic] same-format BYO path requires a provider row");
888
+ }
889
+ const effectiveKey = keyOverride ?? apiKey ?? "";
890
+ const headers = getProviderHeaders(provider, effectiveKey);
891
+ const callerBeta = options.callerAnthropicBeta?.trim();
892
+ if (callerBeta) {
893
+ const existing = headers["anthropic-beta"] ?? headers["Anthropic-Beta"];
894
+ const parts = new Set(
895
+ [existing, callerBeta].filter((v) => typeof v === "string" && v.length > 0).flatMap((v) => v.split(",").map((s) => s.trim()).filter((s) => s.length > 0))
896
+ );
897
+ if (parts.size > 0) {
898
+ delete headers["Anthropic-Beta"];
899
+ headers["anthropic-beta"] = [...parts].join(",");
900
+ }
901
+ }
902
+ injectExtendedContextBeta(headers, resolvedModel, extendedContextEnabled ?? false);
903
+ const url = buildProviderApiUrl(provider, { model: resolvedModel, stream: isStream });
904
+ console.info(`[ProviderProxy:anthropic] (same-format) -> ${url} model=${resolvedModel} stream=${isStream}`);
905
+ const response = await fetch(url, { method: "POST", headers, body: rawBody });
906
+ return { response, rawStatus: response.status };
907
+ }
908
+ async function runPipelineWithPoolReporting(anthropicBody, rawBody, plan, options) {
909
+ const runOnce = (keyOverride) => plan.sameFormat ? runSameFormatFetch(rawBody, plan, options, keyOverride) : runPipeline(anthropicBody, plan);
910
+ const first = await runOnce();
911
+ const outcome = await plan.auth.onResult?.(first.rawStatus);
912
+ if (outcome?.rebound) {
913
+ console.info(
914
+ "[ProviderProxy:anthropic] pool re-bound key after status",
915
+ first.rawStatus,
916
+ "\u2192 retrying once"
917
+ );
918
+ return runOnce(outcome.newKey);
919
+ }
920
+ return first;
921
+ }
922
+
923
+ // src/provider-proxy/ingress/anthropicMessagesIngress.ts
924
+ function isAnthropicMessagesRequest(method, url) {
925
+ return method === "POST" && !!url && url.includes("/v1/messages");
926
+ }
927
+ async function handleAnthropicMessagesRequest(req, res, route, deps) {
928
+ const handlerFactory = deps.anthropicIngressHandlerFactory;
929
+ if (!handlerFactory) {
930
+ const rawBody = await readBody(req);
931
+ const callerBetaRaw = req.headers["anthropic-beta"];
932
+ const callerAnthropicBeta = Array.isArray(callerBetaRaw) ? callerBetaRaw.join(",") : callerBetaRaw;
933
+ await handleAnthropicMessagesByo(res, rawBody, route, deps, { callerAnthropicBeta });
934
+ return;
935
+ }
936
+ const hints = route.anthropicSdkHints;
937
+ if (!hints) {
938
+ writeError(res, 502, "Anthropic route is missing anthropicSdkHints");
939
+ return;
940
+ }
941
+ if (!hints.passThrough && !route.providerId) {
942
+ writeError(res, 502, "Anthropic route is missing a providerId");
943
+ return;
944
+ }
945
+ const handler = handlerFactory({
946
+ llmConfig: deps.llmConfig,
947
+ providerId: route.providerId ?? "",
948
+ model: route.model,
949
+ apiKey: hints.apiKey,
950
+ backgroundTaskModel: route.backgroundTaskModel,
951
+ isOfficialProvider: hints.isOfficialProvider,
952
+ thinkingLevel: hints.thinkingLevel,
953
+ extendedContext: hints.extendedContext ?? null,
954
+ passThrough: hints.passThrough,
955
+ passThroughAuthToken: hints.passThroughAuthToken ?? null,
956
+ resolvePassThroughAuthToken: hints.resolvePassThroughAuthToken ?? null,
957
+ subscriptionProfile: hints.subscriptionProfile ?? null,
958
+ maxConcurrency: hints.maxConcurrency,
959
+ webSearchService: hints.webSearchService ?? null,
960
+ onRetry: hints.onRetry,
961
+ onStreamEvent: hints.onStreamEvent,
962
+ usageRecorder: hints.usageRecorder ?? null,
963
+ attribution: hints.attribution ?? null
964
+ });
965
+ await handler.handle(req, res);
966
+ }
967
+
968
+ // src/provider-proxy/usage/recordGeminiUsage.ts
969
+ function readGeminiUsage(usage) {
970
+ if (!usage) return null;
971
+ const promptTokens = Number(usage.promptTokenCount) || 0;
972
+ const candidatesTokens = Number(usage.candidatesTokenCount) || 0;
973
+ const cacheReadTokens = Number(usage.cachedContentTokenCount) || 0;
974
+ const reasoningTokens = Number(usage.thoughtsTokenCount) || 0;
975
+ const billableInput = Math.max(0, promptTokens - cacheReadTokens);
976
+ return {
977
+ inputTokens: billableInput,
978
+ outputTokens: candidatesTokens,
979
+ cacheReadTokens,
980
+ cacheCreationTokens: 0,
981
+ reasoningTokens
982
+ };
983
+ }
984
+ function recordGeminiNonStreamUsage(recorder, bodyText, attribution) {
985
+ try {
986
+ const parsed = JSON.parse(bodyText);
987
+ const tapped = readGeminiUsage(parsed.usageMetadata);
988
+ if (!tapped) return;
989
+ recorder.record({
990
+ messageId: null,
991
+ parentMessageId: null,
992
+ sessionId: attribution.sessionId,
993
+ providerId: attribution.providerId,
994
+ model: attribution.model,
995
+ apiKeyId: null,
996
+ engineOrigin: "codex-ingress",
997
+ usage: tapped,
998
+ rawUsage: parsed.usageMetadata
999
+ });
1000
+ } catch {
1001
+ }
1002
+ }
1003
+
1004
+ // src/provider-proxy/ingress/geminiGenerateContentIngress.ts
1005
+ var GENERATE_CONTENT_ACTION = ":generateContent";
1006
+ var STREAM_GENERATE_CONTENT_ACTION = ":streamGenerateContent";
1007
+ function isGeminiGenerateContentRequest(method, url) {
1008
+ if (method !== "POST" || !url) return false;
1009
+ const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
1010
+ const lastSegment = path.split("/").pop() ?? "";
1011
+ return lastSegment.includes(STREAM_GENERATE_CONTENT_ACTION) || lastSegment.includes(GENERATE_CONTENT_ACTION);
1012
+ }
1013
+ function isGeminiStreamRequest(url) {
1014
+ if (!url) return false;
1015
+ const [path, query = ""] = url.split("?");
1016
+ const lastSegment = (path?.replace(/\/+$/, "") ?? "").split("/").pop() ?? "";
1017
+ if (lastSegment.includes(STREAM_GENERATE_CONTENT_ACTION)) return true;
1018
+ return /(^|&)alt=sse(&|$)/.test(query);
1019
+ }
1020
+ async function handleGeminiGenerateContentRequest(res, rawBody, url, route, deps) {
1021
+ let geminiBody;
1022
+ try {
1023
+ geminiBody = JSON.parse(rawBody);
1024
+ } catch {
1025
+ writeError(res, 400, "Invalid JSON in request body");
1026
+ return;
1027
+ }
1028
+ if (route.authMode === "subscription") {
1029
+ writeError(res, 502, "Gemini generateContent ingress does not support subscription auth");
1030
+ return;
1031
+ }
1032
+ const isStream = isGeminiStreamRequest(url);
1033
+ geminiBody.stream = isStream;
1034
+ const resolvedModel = route.model;
1035
+ geminiBody.model = resolvedModel;
1036
+ try {
1037
+ const plan = await buildByoPlan2(res, route, deps, resolvedModel, isStream);
1038
+ if (!plan) return;
1039
+ const providerResponse = await runPipelineWithPoolReporting2(geminiBody, plan);
1040
+ const bodyText = await relayResponse(res, providerResponse.response, isStream);
1041
+ if (bodyText && deps.usageRecorder) {
1042
+ recordGeminiNonStreamUsage(deps.usageRecorder, bodyText, {
1043
+ sessionId: route.sessionId,
1044
+ providerId: route.providerId ?? "gemini",
1045
+ model: resolvedModel
1046
+ });
1047
+ }
1048
+ } catch (err) {
1049
+ const errMsg = serializeError(err);
1050
+ console.error("[ProviderProxy:gemini] Pipeline error:", errMsg);
1051
+ writeError(res, 502, errMsg);
1052
+ }
1053
+ }
1054
+ async function buildByoPlan2(res, route, deps, resolvedModel, isStream) {
1055
+ const providerId = route.providerId;
1056
+ if (!providerId) {
1057
+ writeError(res, 502, "BYO route is missing a providerId");
1058
+ return null;
1059
+ }
1060
+ const provider = await deps.llmConfig.getProvider(providerId);
1061
+ if (!provider) {
1062
+ writeError(res, 502, `Provider not found: ${providerId}`);
1063
+ return null;
1064
+ }
1065
+ const apiKey = await resolvePoolBoundKey(deps, providerId, provider, route.sessionId);
1066
+ if (!apiKey) {
1067
+ writeError(res, 502, "API key not configured");
1068
+ return null;
1069
+ }
1070
+ const auth = route.auth ?? new LlmConfigProviderAuth({
1071
+ provider,
1072
+ apiKey,
1073
+ apiKeyPool: deps.apiKeyPool ?? null,
1074
+ providerId,
1075
+ sessionId: route.sessionId
1076
+ });
1077
+ const { chain } = await resolveProviderChain(deps.llmConfig, providerId, resolvedModel);
1078
+ const transformerProvider = {
1079
+ name: provider.name,
1080
+ baseUrl: provider.api_base_url,
1081
+ apiKey,
1082
+ models: provider.models || []
1083
+ };
1084
+ const byoUrl = buildProviderApiUrl(provider, { model: resolvedModel, stream: isStream });
1085
+ return {
1086
+ auth,
1087
+ chain,
1088
+ transformerProvider,
1089
+ resolvedModel,
1090
+ isStream,
1091
+ resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : byoUrl,
1092
+ upstreamUrl: byoUrl
1093
+ };
1094
+ }
1095
+ async function runPipeline2(geminiBody, plan) {
1096
+ const executor = getSharedExecutor();
1097
+ const endpointTransformer = getGeminiEndpointTransformer();
1098
+ const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
1099
+ const authHeaders = {};
1100
+ await auth.applyHeaders(authHeaders, { upstreamUrl, model: resolvedModel });
1101
+ let rawStatus = null;
1102
+ const { response } = await executeProviderCall({
1103
+ executor,
1104
+ request: geminiBody,
1105
+ provider: transformerProvider,
1106
+ chain,
1107
+ endpointTransformer,
1108
+ resolveUrl,
1109
+ buildHeaders: (config) => {
1110
+ const headers = { ...authHeaders };
1111
+ if (config.headers) {
1112
+ for (const [key, value] of Object.entries(config.headers)) {
1113
+ if (value !== void 0 && !(key in headers)) headers[key] = value;
1114
+ }
1115
+ }
1116
+ return headers;
1117
+ },
1118
+ fetchFn: (url, headers, body) => {
1119
+ console.log(`[ProviderProxy:gemini] -> ${url} model=${resolvedModel} stream=${isStream}`);
1120
+ return fetch(url, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => {
1121
+ rawStatus = r.status;
1122
+ return r;
1123
+ });
1124
+ },
1125
+ runResponseChain: true
1126
+ });
1127
+ return { response, rawStatus };
1128
+ }
1129
+ async function runPipelineWithPoolReporting2(geminiBody, plan) {
1130
+ const first = await runPipeline2(geminiBody, plan);
1131
+ const outcome = await plan.auth.onResult?.(first.rawStatus);
1132
+ if (outcome?.rebound) {
1133
+ console.log(
1134
+ "[ProviderProxy:gemini] pool re-bound key after status",
1135
+ first.rawStatus,
1136
+ "\u2192 retrying once"
1137
+ );
1138
+ return runPipeline2(geminiBody, plan);
1139
+ }
1140
+ return first;
1141
+ }
1142
+
1143
+ // src/provider-proxy/usage/recordChatCompletionsUsage.ts
1144
+ function readChatCompletionsUsage(usage) {
1145
+ if (!usage) return null;
1146
+ const promptTokens = Number(usage.prompt_tokens) || 0;
1147
+ const completionTokens = Number(usage.completion_tokens) || 0;
1148
+ const promptDetails = usage.prompt_tokens_details ?? {};
1149
+ const cacheReadTokens = Number(promptDetails.cached_tokens) || 0;
1150
+ const completionDetails = usage.completion_tokens_details ?? {};
1151
+ const reasoningTokens = Number(completionDetails.reasoning_tokens) || 0;
1152
+ const billableInput = Math.max(0, promptTokens - cacheReadTokens);
1153
+ return {
1154
+ inputTokens: billableInput,
1155
+ outputTokens: completionTokens,
1156
+ cacheReadTokens,
1157
+ cacheCreationTokens: 0,
1158
+ reasoningTokens
1159
+ };
1160
+ }
1161
+ function recordChatCompletionsNonStreamUsage(recorder, bodyText, attribution) {
1162
+ try {
1163
+ const parsed = JSON.parse(bodyText);
1164
+ const tapped = readChatCompletionsUsage(parsed.usage);
1165
+ if (!tapped) return;
1166
+ recorder.record({
1167
+ messageId: null,
1168
+ parentMessageId: null,
1169
+ sessionId: attribution.sessionId,
1170
+ providerId: attribution.providerId,
1171
+ model: attribution.model,
1172
+ apiKeyId: null,
1173
+ engineOrigin: "codex-ingress",
1174
+ usage: tapped,
1175
+ rawUsage: parsed.usage
1176
+ });
1177
+ } catch {
1178
+ }
1179
+ }
1180
+
1181
+ // src/provider-proxy/ingress/openaiChatIngress.ts
1182
+ function isOpenAIChatRequest(method, url) {
1183
+ if (method !== "POST" || !url) return false;
1184
+ const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
1185
+ return path.endsWith("/chat/completions");
1186
+ }
1187
+ async function handleOpenAIChatRequest(res, rawBody, route, deps) {
1188
+ let chatBody;
1189
+ try {
1190
+ chatBody = JSON.parse(rawBody);
1191
+ } catch {
1192
+ writeError(res, 400, "Invalid JSON in request body");
1193
+ return;
1194
+ }
1195
+ if (route.authMode === "subscription") {
1196
+ writeError(res, 502, "OpenAI Chat Completions ingress does not support subscription auth");
1197
+ return;
1198
+ }
1199
+ const isStream = chatBody.stream === true;
1200
+ const resolvedModel = route.model;
1201
+ chatBody.model = resolvedModel;
1202
+ try {
1203
+ const plan = await buildByoPlan3(res, route, deps, resolvedModel, isStream);
1204
+ if (!plan) return;
1205
+ const providerResponse = await runPipelineWithPoolReporting3(chatBody, plan);
1206
+ const bodyText = await relayResponse(res, providerResponse.response, isStream);
1207
+ if (bodyText && deps.usageRecorder) {
1208
+ recordChatCompletionsNonStreamUsage(deps.usageRecorder, bodyText, {
1209
+ sessionId: route.sessionId,
1210
+ providerId: route.providerId ?? "openai",
1211
+ model: resolvedModel
1212
+ });
1213
+ }
1214
+ } catch (err) {
1215
+ const errMsg = serializeError(err);
1216
+ console.error("[ProviderProxy:chat] Pipeline error:", errMsg);
1217
+ writeError(res, 502, errMsg);
1218
+ }
1219
+ }
1220
+ async function buildByoPlan3(res, route, deps, resolvedModel, isStream) {
1221
+ const providerId = route.providerId;
1222
+ if (!providerId) {
1223
+ writeError(res, 502, "BYO route is missing a providerId");
1224
+ return null;
1225
+ }
1226
+ const provider = await deps.llmConfig.getProvider(providerId);
1227
+ if (!provider) {
1228
+ writeError(res, 502, `Provider not found: ${providerId}`);
1229
+ return null;
1230
+ }
1231
+ const apiKey = await resolvePoolBoundKey(deps, providerId, provider, route.sessionId);
1232
+ if (!apiKey) {
1233
+ writeError(res, 502, "API key not configured");
1234
+ return null;
1235
+ }
1236
+ const auth = route.auth ?? new LlmConfigProviderAuth({
1237
+ provider,
1238
+ apiKey,
1239
+ apiKeyPool: deps.apiKeyPool ?? null,
1240
+ providerId,
1241
+ sessionId: route.sessionId
1242
+ });
1243
+ const { chain } = await resolveProviderChain(deps.llmConfig, providerId, resolvedModel);
1244
+ const transformerProvider = {
1245
+ name: provider.name,
1246
+ baseUrl: provider.api_base_url,
1247
+ apiKey,
1248
+ models: provider.models || []
1249
+ };
1250
+ const byoUrl = buildProviderApiUrl(provider, { model: resolvedModel, stream: isStream });
1251
+ return {
1252
+ auth,
1253
+ chain,
1254
+ transformerProvider,
1255
+ resolvedModel,
1256
+ isStream,
1257
+ resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : byoUrl,
1258
+ upstreamUrl: byoUrl
1259
+ };
1260
+ }
1261
+ async function runPipeline3(chatBody, plan) {
1262
+ const executor = getSharedExecutor();
1263
+ const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
1264
+ const authHeaders = {};
1265
+ await auth.applyHeaders(authHeaders, { upstreamUrl, model: resolvedModel });
1266
+ let rawStatus = null;
1267
+ const { response } = await executeProviderCall({
1268
+ executor,
1269
+ request: chatBody,
1270
+ provider: transformerProvider,
1271
+ chain,
1272
+ resolveUrl,
1273
+ buildHeaders: (config) => {
1274
+ const headers = { ...authHeaders };
1275
+ if (config.headers) {
1276
+ for (const [key, value] of Object.entries(config.headers)) {
1277
+ if (value !== void 0 && !(key in headers)) headers[key] = value;
1278
+ }
1279
+ }
1280
+ return headers;
1281
+ },
1282
+ fetchFn: (url, headers, body) => {
1283
+ console.log(`[ProviderProxy:chat] -> ${url} model=${resolvedModel} stream=${isStream}`);
1284
+ return fetch(url, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => {
1285
+ rawStatus = r.status;
1286
+ return r;
1287
+ });
1288
+ },
1289
+ runResponseChain: true
1290
+ });
1291
+ return { response, rawStatus };
1292
+ }
1293
+ async function runPipelineWithPoolReporting3(chatBody, plan) {
1294
+ const first = await runPipeline3(chatBody, plan);
1295
+ const outcome = await plan.auth.onResult?.(first.rawStatus);
1296
+ if (outcome?.rebound) {
1297
+ console.log(
1298
+ "[ProviderProxy:chat] pool re-bound key after status",
1299
+ first.rawStatus,
1300
+ "\u2192 retrying once"
1301
+ );
1302
+ return runPipeline3(chatBody, plan);
1303
+ }
1304
+ return first;
1305
+ }
1306
+
1307
+ // src/provider-proxy/usage/recordResponsesUsage.ts
1308
+ function readResponsesUsage(usage) {
1309
+ if (!usage) return null;
1310
+ const inputTokens = Number(usage.input_tokens) || 0;
1311
+ const outputTokens = Number(usage.output_tokens) || 0;
1312
+ const inputDetails = usage.input_tokens_details ?? {};
1313
+ const cacheReadTokens = Number(inputDetails.cached_tokens) || 0;
1314
+ const outputDetails = usage.output_tokens_details ?? {};
1315
+ const reasoningTokens = Number(outputDetails.reasoning_tokens) || 0;
1316
+ const billableInput = Math.max(0, inputTokens - cacheReadTokens);
1317
+ return {
1318
+ inputTokens: billableInput,
1319
+ outputTokens,
1320
+ cacheReadTokens,
1321
+ cacheCreationTokens: 0,
1322
+ reasoningTokens
1323
+ };
1324
+ }
1325
+ function recordResponsesNonStreamUsage(recorder, bodyText, attribution) {
1326
+ try {
1327
+ const parsed = JSON.parse(bodyText);
1328
+ const tapped = readResponsesUsage(parsed.usage);
1329
+ if (!tapped) return;
1330
+ recorder.record({
1331
+ messageId: null,
1332
+ parentMessageId: null,
1333
+ sessionId: attribution.sessionId,
1334
+ providerId: attribution.providerId,
1335
+ model: attribution.model,
1336
+ apiKeyId: null,
1337
+ engineOrigin: "codex-ingress",
1338
+ usage: tapped,
1339
+ rawUsage: parsed.usage
1340
+ });
1341
+ } catch {
1342
+ }
1343
+ }
1344
+
1345
+ // src/provider-proxy/ingress/openaiResponsesIngress.ts
1346
+ function isOpenAIResponsesRequest(method, url) {
1347
+ if (method !== "POST" || !url) return false;
1348
+ const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
1349
+ return path.endsWith("/responses");
1350
+ }
1351
+ async function handleOpenAIResponsesRequest(res, rawBody, route, deps) {
1352
+ let responsesBody;
1353
+ try {
1354
+ responsesBody = JSON.parse(rawBody);
1355
+ } catch {
1356
+ writeError(res, 400, "Invalid JSON in request body");
1357
+ return;
1358
+ }
1359
+ const isStream = responsesBody.stream === true;
1360
+ const resolvedModel = route.model;
1361
+ responsesBody.model = resolvedModel;
1362
+ try {
1363
+ const plan = route.authMode === "subscription" ? await buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) : await buildByoPlan4(res, route, deps, resolvedModel, isStream);
1364
+ if (!plan) return;
1365
+ const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry2(responsesBody, plan) : await runPipelineWithPoolReporting4(responsesBody, plan);
1366
+ const bodyText = await relayResponse(res, providerResponse.response, isStream);
1367
+ if (bodyText && deps.usageRecorder) {
1368
+ recordResponsesNonStreamUsage(deps.usageRecorder, bodyText, {
1369
+ sessionId: route.sessionId,
1370
+ providerId: route.providerId ?? "codex",
1371
+ model: resolvedModel
1372
+ });
1373
+ }
1374
+ } catch (err) {
1375
+ const errMsg = serializeError(err);
1376
+ console.error("[ProviderProxy:responses] Pipeline error:", errMsg);
1377
+ writeError(res, 502, errMsg);
1378
+ }
1379
+ }
1380
+ async function buildByoPlan4(res, route, deps, resolvedModel, isStream) {
1381
+ const providerId = route.providerId;
1382
+ if (!providerId) {
1383
+ writeError(res, 502, "BYO route is missing a providerId");
1384
+ return null;
1385
+ }
1386
+ const provider = await deps.llmConfig.getProvider(providerId);
1387
+ if (!provider) {
1388
+ writeError(res, 502, `Provider not found: ${providerId}`);
1389
+ return null;
1390
+ }
1391
+ const apiKey = await resolvePoolBoundKey(deps, providerId, provider, route.sessionId);
1392
+ if (!apiKey) {
1393
+ writeError(res, 502, "API key not configured");
1394
+ return null;
1395
+ }
1396
+ const auth = route.auth ?? new LlmConfigProviderAuth({
1397
+ provider,
1398
+ apiKey,
1399
+ apiKeyPool: deps.apiKeyPool ?? null,
1400
+ providerId,
1401
+ sessionId: route.sessionId
1402
+ });
1403
+ const { chain } = await resolveProviderChain(deps.llmConfig, providerId, resolvedModel);
1404
+ const transformerProvider = {
1405
+ name: provider.name,
1406
+ baseUrl: provider.api_base_url,
1407
+ apiKey,
1408
+ models: provider.models || []
1409
+ };
1410
+ const byoUrl = buildProviderApiUrl(provider, { model: resolvedModel, stream: isStream });
1411
+ return {
1412
+ auth,
1413
+ chain,
1414
+ transformerProvider,
1415
+ resolvedModel,
1416
+ isStream,
1417
+ resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : byoUrl,
1418
+ upstreamUrl: byoUrl
1419
+ };
1420
+ }
1421
+ async function buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) {
1422
+ const profile = route.subscriptionProfile;
1423
+ if (!profile) {
1424
+ writeError(res, 502, "Subscription mode requires a codex subscription profile");
1425
+ return null;
1426
+ }
1427
+ const upstreamUrl = profile.resolveUpstreamUrl?.(resolvedModel);
1428
+ if (!upstreamUrl) {
1429
+ writeError(res, 502, "Subscription profile is missing resolveUpstreamUrl");
1430
+ return null;
1431
+ }
1432
+ const auth = route.auth ?? new SubscriptionAuthSource(profile);
1433
+ const chain = resolveSubscriptionChain(
1434
+ profile,
1435
+ deps.llmConfig.getTransformerService(),
1436
+ getResponsesEndpointTransformer()
1437
+ );
1438
+ const transformerProvider = {
1439
+ name: profile.authStrategy.providerId,
1440
+ baseUrl: upstreamUrl,
1441
+ apiKey: "",
1442
+ models: [resolvedModel]
1443
+ };
1444
+ if (profile.authStrategy.providerId === "gemini") {
1445
+ try {
1446
+ transformerProvider.geminiProject = await resolveGeminiCodeAssistProject(profile);
1447
+ } catch (err) {
1448
+ writeError(res, 502, serializeError(err));
1449
+ return null;
1450
+ }
1451
+ }
1452
+ return {
1453
+ auth,
1454
+ chain,
1455
+ transformerProvider,
1456
+ resolvedModel,
1457
+ isStream,
1458
+ // Prefer the transformer-supplied URL (Code Assist carries the correct
1459
+ // stream vs non-stream colon-method URL in `config.url`); fall back to the
1460
+ // profile's `resolveUpstreamUrl` for non-Code-Assist subscriptions.
1461
+ resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : typeof config.url === "string" ? config.url : upstreamUrl,
1462
+ upstreamUrl
1463
+ };
1464
+ }
1465
+ async function resolveGeminiCodeAssistProject(profile) {
1466
+ const probe = {};
1467
+ await profile.authStrategy.applyHeaders(probe);
1468
+ const bearer = probe.Authorization ?? probe.authorization ?? "";
1469
+ const accessToken = bearer.replace(/^Bearer\s+/i, "").trim();
1470
+ if (!accessToken) return void 0;
1471
+ const resolver = getGeminiCodeAssistResolver();
1472
+ if (!resolver) return void 0;
1473
+ return resolver.resolveProject(accessToken);
1474
+ }
1475
+ async function runPipeline4(responsesBody, plan) {
1476
+ const executor = getSharedExecutor();
1477
+ const endpointTransformer = getResponsesEndpointTransformer();
1478
+ const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
1479
+ const authHeaders = {};
1480
+ await auth.applyHeaders(authHeaders, { upstreamUrl, model: resolvedModel });
1481
+ let rawStatus = null;
1482
+ const { response } = await executeProviderCall({
1483
+ executor,
1484
+ request: responsesBody,
1485
+ provider: transformerProvider,
1486
+ chain,
1487
+ endpointTransformer,
1488
+ resolveUrl,
1489
+ buildHeaders: (config) => {
1490
+ const headers = { ...authHeaders };
1491
+ if (config.headers) {
1492
+ for (const [key, value] of Object.entries(config.headers)) {
1493
+ if (value !== void 0 && !(key in headers)) headers[key] = value;
1494
+ }
1495
+ }
1496
+ return headers;
1497
+ },
1498
+ fetchFn: (url, headers, body) => {
1499
+ console.log(`[ProviderProxy:responses] -> ${url} model=${resolvedModel} stream=${isStream}`);
1500
+ return fetch(url, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => {
1501
+ rawStatus = r.status;
1502
+ return r;
1503
+ });
1504
+ },
1505
+ runResponseChain: true
1506
+ });
1507
+ return { response, rawStatus };
1508
+ }
1509
+ async function runPipelineWithPoolReporting4(responsesBody, plan) {
1510
+ const first = await runPipeline4(responsesBody, plan);
1511
+ const outcome = await plan.auth.onResult?.(first.rawStatus);
1512
+ if (outcome?.rebound) {
1513
+ console.log(
1514
+ "[ProviderProxy:responses] pool re-bound key after status",
1515
+ first.rawStatus,
1516
+ "\u2192 retrying once"
1517
+ );
1518
+ return runPipeline4(responsesBody, plan);
1519
+ }
1520
+ return first;
1521
+ }
1522
+ async function runPipelineWithSubscriptionRetry2(responsesBody, plan) {
1523
+ const first = await runPipeline4(responsesBody, plan);
1524
+ if (first.rawStatus !== 401) return first;
1525
+ const refreshed = await plan.auth.onUnauthorized?.();
1526
+ if (!refreshed) {
1527
+ console.warn("[ProviderProxy:responses] 401 not recoverable (onUnauthorized returned false)");
1528
+ return first;
1529
+ }
1530
+ console.log("[ProviderProxy:responses] 401 \u2192 token refreshed; retrying once");
1531
+ return runPipeline4(responsesBody, plan);
1532
+ }
1533
+
1534
+ // src/provider-proxy/providerProxyRouter.ts
1535
+ function extractRouteToken(authHeader) {
1536
+ if (!authHeader) return void 0;
1537
+ const trimmed = authHeader.trim();
1538
+ if (!trimmed) return void 0;
1539
+ const m = /^Bearer\s+(.+)$/i.exec(trimmed);
1540
+ return (m ? m[1] : trimmed).trim() || void 0;
1541
+ }
1542
+ function resolveRouteToken(req) {
1543
+ const fromAuth = extractRouteToken(req.headers["authorization"]);
1544
+ if (fromAuth) return fromAuth;
1545
+ const googKey = req.headers["x-goog-api-key"];
1546
+ const value = Array.isArray(googKey) ? googKey[0] : googKey;
1547
+ return value?.trim() || void 0;
1548
+ }
1549
+ async function routeRequest(req, res, routes, deps) {
1550
+ const token = resolveRouteToken(req);
1551
+ const route = routes.lookup(token);
1552
+ if (!route) {
1553
+ writeError(res, 401, "Invalid or expired route token");
1554
+ return;
1555
+ }
1556
+ const method = req.method;
1557
+ const url = req.url;
1558
+ if (isAnthropicMessagesRequest(method, url)) {
1559
+ await handleAnthropicMessagesRequest(req, res, route, deps);
1560
+ return;
1561
+ }
1562
+ if (isOpenAIResponsesRequest(method, url)) {
1563
+ const rawBody = await readBody(req);
1564
+ await handleOpenAIResponsesRequest(res, rawBody, route, deps);
1565
+ return;
1566
+ }
1567
+ if (isOpenAIChatRequest(method, url)) {
1568
+ const rawBody = await readBody(req);
1569
+ await handleOpenAIChatRequest(res, rawBody, route, deps);
1570
+ return;
1571
+ }
1572
+ if (isGeminiGenerateContentRequest(method, url)) {
1573
+ const rawBody = await readBody(req);
1574
+ await handleGeminiGenerateContentRequest(res, rawBody, url, route, deps);
1575
+ return;
1576
+ }
1577
+ writeError(res, 404, `Unsupported: ${method} ${url}`);
1578
+ }
1579
+
1580
+ // src/provider-proxy/ProviderProxy.ts
1581
+ function isLoopbackAddress(addr) {
1582
+ if (!addr) return false;
1583
+ const normalized = addr.startsWith("::ffff:") ? addr.slice("::ffff:".length) : addr;
1584
+ return normalized === "127.0.0.1" || normalized.startsWith("127.") || normalized === "::1" || addr === "::1";
1585
+ }
1586
+ var ProviderProxy = class {
1587
+ constructor(deps, routes) {
1588
+ this.deps = deps;
1589
+ this.routes = routes ?? new ProviderProxyRouteMap();
1590
+ }
1591
+ deps;
1592
+ server = null;
1593
+ port = 0;
1594
+ routes;
1595
+ /**
1596
+ * Start the resident listener on a stable port on 127.0.0.1. Idempotent —
1597
+ * a second `start()` returns the already-bound port.
1598
+ */
1599
+ async start() {
1600
+ if (this.server) return this.port;
1601
+ return new Promise((resolve, reject) => {
1602
+ const server = http.createServer((req, res) => {
1603
+ const peer = req.socket.remoteAddress;
1604
+ if (!isLoopbackAddress(peer)) {
1605
+ res.writeHead(403, { "Content-Type": "application/json" });
1606
+ res.end(JSON.stringify({ error: { type: "provider_proxy_error", message: "Loopback only" } }));
1607
+ req.socket.destroy();
1608
+ return;
1609
+ }
1610
+ routeRequest(req, res, this.routes, this.deps).catch((err) => {
1611
+ const errMsg = serializeError(err);
1612
+ console.error("[ProviderProxy] Unhandled error:", errMsg);
1613
+ if (!res.headersSent) {
1614
+ res.writeHead(500, { "Content-Type": "application/json" });
1615
+ res.end(JSON.stringify({ error: { type: "provider_proxy_error", message: errMsg } }));
1616
+ }
1617
+ });
1618
+ });
1619
+ server.listen(0, "127.0.0.1", () => {
1620
+ const addr = server.address();
1621
+ if (addr && typeof addr === "object") {
1622
+ this.port = addr.port;
1623
+ this.server = server;
1624
+ console.log(`[ProviderProxy] Listening on 127.0.0.1:${this.port}`);
1625
+ resolve(this.port);
1626
+ } else {
1627
+ reject(new Error("Failed to get server address"));
1628
+ }
1629
+ });
1630
+ server.on("error", reject);
1631
+ });
1632
+ }
1633
+ /** Stop the listener, clear all routes, and release the port. */
1634
+ async stop() {
1635
+ this.routes.clear();
1636
+ const server = this.server;
1637
+ if (!server) return;
1638
+ return new Promise((resolve) => {
1639
+ server.close(() => {
1640
+ console.log(`[ProviderProxy] Stopped (port=${this.port})`);
1641
+ this.server = null;
1642
+ this.port = 0;
1643
+ resolve();
1644
+ });
1645
+ });
1646
+ }
1647
+ /** Base URL for injector wiring (`ANTHROPIC_BASE_URL` / codex `base_url`). */
1648
+ getBaseUrl() {
1649
+ return `http://127.0.0.1:${this.port}`;
1650
+ }
1651
+ /**
1652
+ * The SHARED route map. Exposed so the outbound API server
1653
+ * (`outbound-api-server`) can mint per-request routes on the SAME map and
1654
+ * delegate to the existing `routeRequest()` dispatch — guaranteeing a single
1655
+ * conversion stack. Not used by the resident per-run flow.
1656
+ */
1657
+ getRouteMap() {
1658
+ return this.routes;
1659
+ }
1660
+ /**
1661
+ * The app-session deps the proxy services all routes with. Exposed so the
1662
+ * outbound server can pass them verbatim into `routeRequest()`.
1663
+ */
1664
+ getDeps() {
1665
+ return this.deps;
1666
+ }
1667
+ /** Register a route for one run; returns the crypto route token (task 2.2). */
1668
+ addRoute(context, idleMs) {
1669
+ return this.routes.addRoute(context, idleMs);
1670
+ }
1671
+ /** Remove a route at run end. Returns true if an entry existed. */
1672
+ removeRoute(token) {
1673
+ return this.routes.removeRoute(token);
1674
+ }
1675
+ /** Live-route count (diagnostics / tests). */
1676
+ routeCount() {
1677
+ return this.routes.size();
1678
+ }
1679
+ };
1680
+
1681
+ // src/provider-proxy/index.ts
1682
+ var instance = null;
1683
+ function getProviderProxy(deps) {
1684
+ if (!instance) {
1685
+ if (!deps) {
1686
+ throw new Error("getProviderProxy: deps are required on first construction");
1687
+ }
1688
+ instance = new ProviderProxy(deps);
1689
+ }
1690
+ return instance;
1691
+ }
1692
+ function __resetProviderProxyForTests() {
1693
+ instance = null;
1694
+ }
1695
+
1696
+ // src/outbound-api/OutboundApiServer.ts
1697
+ import http2 from "http";
1698
+ import { networkInterfaces } from "os";
1699
+
1700
+ // src/outbound-api/outboundApiRouter.ts
1701
+ import { Readable } from "stream";
1702
+
1703
+ // src/outbound-api/outboundApiKeyAuth.ts
1704
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
1705
+ var SECRET_BYTES = 32;
1706
+ var PREFIX_LEN = 14;
1707
+ var KEY_PREFIX = "sk-omnicross-";
1708
+ var BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
1709
+ var BASE62_REJECT_THRESHOLD = 248;
1710
+ function randomBase62(count) {
1711
+ let out = "";
1712
+ while (out.length < count) {
1713
+ const buf = randomBytes2(count - out.length + 8);
1714
+ for (const byte of buf) {
1715
+ if (out.length >= count) break;
1716
+ if (byte >= BASE62_REJECT_THRESHOLD) continue;
1717
+ out += BASE62[byte % 62];
1718
+ }
1719
+ }
1720
+ return out;
1721
+ }
1722
+ function generateSecret() {
1723
+ return KEY_PREFIX + randomBase62(SECRET_BYTES);
1724
+ }
1725
+ function hashKey(secret) {
1726
+ return createHash("sha256").update(secret, "utf8").digest("hex");
1727
+ }
1728
+ function keyPrefix(secret) {
1729
+ return secret.slice(0, PREFIX_LEN);
1730
+ }
1731
+ function newKeyId() {
1732
+ return `oak_${Date.now()}_${randomBytes2(6).toString("hex")}`;
1733
+ }
1734
+ async function createNamedKey(db, name) {
1735
+ const secret = generateSecret();
1736
+ const id = newKeyId();
1737
+ const prefix = keyPrefix(secret);
1738
+ const row = await db.outboundApiKeysCreate({
1739
+ id,
1740
+ name,
1741
+ keyHash: hashKey(secret),
1742
+ keyPrefix: prefix
1743
+ });
1744
+ return {
1745
+ id: row.id,
1746
+ name: row.name,
1747
+ keyPrefix: row.keyPrefix,
1748
+ createdAt: row.createdAt,
1749
+ plaintextOnce: secret
1750
+ };
1751
+ }
1752
+ async function verifyPresentedKey(db, presentedKey) {
1753
+ if (!presentedKey) return null;
1754
+ const trimmed = presentedKey.trim();
1755
+ if (!trimmed) return null;
1756
+ const row = await db.outboundApiKeysGetByHash(hashKey(trimmed));
1757
+ if (!row) return null;
1758
+ if (!row.enabled || row.revokedAt !== null) return null;
1759
+ void db.outboundApiKeysTouchLastUsed(row.id).catch(() => {
1760
+ });
1761
+ return { id: row.id };
1762
+ }
1763
+
1764
+ // src/outbound-api/roleDetection.ts
1765
+ import { normalizeModelId } from "@omnicross/contracts/canonical-models";
1766
+ var BACKGROUND_TIER_TOKENS = /* @__PURE__ */ new Set([
1767
+ "haiku",
1768
+ "mini",
1769
+ "flash",
1770
+ "small",
1771
+ "lite",
1772
+ "nano",
1773
+ "8b"
1774
+ ]);
1775
+ function modelTokens(normalizedId) {
1776
+ return normalizedId.split(/[-._:/\s]+/).filter(Boolean);
1777
+ }
1778
+ function isBackgroundTierModel(rawModelId) {
1779
+ if (!rawModelId) return false;
1780
+ const tokens = modelTokens(normalizeModelId(rawModelId));
1781
+ return tokens.some((tok) => BACKGROUND_TIER_TOKENS.has(tok));
1782
+ }
1783
+ function extractRequestedModel(ingressFormat, body) {
1784
+ const model = body["model"];
1785
+ return typeof model === "string" && model ? model : void 0;
1786
+ }
1787
+ function anthropicHasVision(body) {
1788
+ const messages = body["messages"];
1789
+ if (!Array.isArray(messages)) return false;
1790
+ for (const msg of messages) {
1791
+ const content = msg?.content;
1792
+ if (!Array.isArray(content)) continue;
1793
+ for (const part of content) {
1794
+ if (part?.type === "image") return true;
1795
+ }
1796
+ }
1797
+ return false;
1798
+ }
1799
+ function openaiChatHasVision(body) {
1800
+ const messages = body["messages"];
1801
+ if (!Array.isArray(messages)) return false;
1802
+ for (const msg of messages) {
1803
+ const content = msg?.content;
1804
+ if (!Array.isArray(content)) continue;
1805
+ for (const part of content) {
1806
+ if (part?.type === "image_url") return true;
1807
+ }
1808
+ }
1809
+ return false;
1810
+ }
1811
+ function openaiResponsesHasVision(body) {
1812
+ const input = body["input"];
1813
+ if (!Array.isArray(input)) return false;
1814
+ for (const item of input) {
1815
+ const content = item?.content;
1816
+ if (Array.isArray(content)) {
1817
+ for (const part of content) {
1818
+ if (part?.type === "input_image") return true;
1819
+ }
1820
+ }
1821
+ if (item?.type === "input_image") return true;
1822
+ }
1823
+ return false;
1824
+ }
1825
+ function geminiHasVision(body) {
1826
+ const contents = body["contents"];
1827
+ if (!Array.isArray(contents)) return false;
1828
+ for (const content of contents) {
1829
+ const parts = content?.parts;
1830
+ if (!Array.isArray(parts)) continue;
1831
+ for (const part of parts) {
1832
+ const p = part;
1833
+ if (p?.inline_data || p?.inlineData || p?.file_data || p?.fileData) return true;
1834
+ }
1835
+ }
1836
+ return false;
1837
+ }
1838
+ function hasVisionContent(ingressFormat, body) {
1839
+ switch (ingressFormat) {
1840
+ case "anthropic-messages":
1841
+ return anthropicHasVision(body);
1842
+ case "openai-chat":
1843
+ return openaiChatHasVision(body);
1844
+ case "openai-responses":
1845
+ return openaiResponsesHasVision(body);
1846
+ case "gemini-generatecontent":
1847
+ return geminiHasVision(body);
1848
+ default:
1849
+ return false;
1850
+ }
1851
+ }
1852
+ function detectRequestRole(ingressFormat, body, options) {
1853
+ if (hasVisionContent(ingressFormat, body)) return "vision";
1854
+ const requestedModel = extractRequestedModel(ingressFormat, body);
1855
+ if (requestedModel) {
1856
+ const overrides = options?.backgroundModelIds;
1857
+ if (overrides && overrides.length > 0) {
1858
+ const reqNorm = normalizeModelId(requestedModel);
1859
+ const inOverride = overrides.some((id) => {
1860
+ if (!id) return false;
1861
+ const candidate = id.includes(",") ? id.slice(id.indexOf(",") + 1) : id;
1862
+ return normalizeModelId(candidate) === reqNorm || candidate === requestedModel;
1863
+ });
1864
+ if (inOverride) return "background";
1865
+ }
1866
+ if (isBackgroundTierModel(requestedModel)) return "background";
1867
+ }
1868
+ return "default";
1869
+ }
1870
+ function endpointToIngressFormat(endpoint) {
1871
+ switch (endpoint) {
1872
+ case "chat":
1873
+ return "openai-chat";
1874
+ case "responses":
1875
+ return "openai-responses";
1876
+ case "messages":
1877
+ return "anthropic-messages";
1878
+ case "gemini":
1879
+ return "gemini-generatecontent";
1880
+ }
1881
+ }
1882
+
1883
+ // src/outbound-api/outboundApiRouter.ts
1884
+ function writeJsonError(res, status, message, headers = {}) {
1885
+ if (res.headersSent) return;
1886
+ res.writeHead(status, { "Content-Type": "application/json", ...headers });
1887
+ res.end(JSON.stringify({ error: { type: "outbound_api_error", message } }));
1888
+ }
1889
+ function extractPresentedKey(req) {
1890
+ const auth = req.headers["authorization"];
1891
+ if (typeof auth === "string" && auth.trim()) {
1892
+ const m = /^Bearer\s+(.+)$/i.exec(auth.trim());
1893
+ return (m ? m[1] : auth).trim() || void 0;
1894
+ }
1895
+ const xApiKey = req.headers["x-api-key"];
1896
+ const xApi = Array.isArray(xApiKey) ? xApiKey[0] : xApiKey;
1897
+ if (xApi?.trim()) return xApi.trim();
1898
+ const goog = req.headers["x-goog-api-key"];
1899
+ const g = Array.isArray(goog) ? goog[0] : goog;
1900
+ return g?.trim() || void 0;
1901
+ }
1902
+ function selectEndpoint(method, url) {
1903
+ if (method !== "POST" || !url) return null;
1904
+ const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
1905
+ if (path.endsWith("/chat/completions")) return "chat";
1906
+ if (path.endsWith("/responses")) return "responses";
1907
+ if (path.includes("/v1/messages")) return "messages";
1908
+ const lastSeg = path.split("/").pop() ?? "";
1909
+ if (lastSeg.endsWith(":generateContent") || lastSeg.endsWith(":streamGenerateContent")) {
1910
+ return "gemini";
1911
+ }
1912
+ return null;
1913
+ }
1914
+ function extractGeminiModelFromUrl(url) {
1915
+ if (!url) return void 0;
1916
+ const path = url.split("?")[0] ?? "";
1917
+ const m = /\/models\/([^/:]+):(?:stream)?[gG]enerateContent$/.exec(path.replace(/\/+$/, ""));
1918
+ return m ? decodeURIComponent(m[1]) : void 0;
1919
+ }
1920
+ function readBody2(req) {
1921
+ return new Promise((resolve, reject) => {
1922
+ const chunks = [];
1923
+ req.on("data", (chunk) => chunks.push(chunk));
1924
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
1925
+ req.on("error", reject);
1926
+ });
1927
+ }
1928
+ function shimAuthHeader(req, routeToken) {
1929
+ req.headers["authorization"] = `Bearer ${routeToken}`;
1930
+ delete req.headers["x-goog-api-key"];
1931
+ delete req.headers["x-api-key"];
1932
+ }
1933
+ async function handleOutboundRequest(req, res, deps, config, rateLimiter) {
1934
+ const presented = extractPresentedKey(req);
1935
+ const verified = await verifyPresentedKey(deps.db, presented);
1936
+ if (!verified) {
1937
+ writeJsonError(res, 401, "Invalid or missing API key");
1938
+ return;
1939
+ }
1940
+ const decision = rateLimiter.check(verified.id);
1941
+ if (!decision.allowed) {
1942
+ writeJsonError(res, 429, "Rate limit exceeded", {
1943
+ "Retry-After": String(decision.retryAfterSeconds)
1944
+ });
1945
+ return;
1946
+ }
1947
+ const endpoint = selectEndpoint(req.method, req.url);
1948
+ if (!endpoint) {
1949
+ writeJsonError(res, 404, `Unsupported: ${req.method} ${req.url}`);
1950
+ return;
1951
+ }
1952
+ const endpointConfig = config.endpoints.find((e) => e.endpoint === endpoint);
1953
+ if (!endpointConfig) {
1954
+ writeJsonError(res, 503, `endpoint '${endpoint}' is not configured`);
1955
+ return;
1956
+ }
1957
+ const ingressFormat = endpointToIngressFormat(endpoint);
1958
+ const rawBody = await readBody2(req);
1959
+ let parsedBody = {};
1960
+ try {
1961
+ parsedBody = rawBody ? JSON.parse(rawBody) : {};
1962
+ } catch {
1963
+ writeJsonError(res, 400, "Invalid JSON in request body");
1964
+ return;
1965
+ }
1966
+ if (endpoint === "gemini" && typeof parsedBody["model"] !== "string") {
1967
+ const urlModel = extractGeminiModelFromUrl(req.url);
1968
+ if (urlModel) parsedBody["model"] = urlModel;
1969
+ }
1970
+ const role = detectRequestRole(ingressFormat, parsedBody, {
1971
+ backgroundModelIds: endpointConfig.backgroundModelIds
1972
+ });
1973
+ const resolved = await resolveRoute({
1974
+ config: endpointConfig,
1975
+ role,
1976
+ ingressFormat,
1977
+ llmConfig: deps.llmConfig,
1978
+ // pool-seam (omnicross-daemon-parity-poolseam, design D1/D2(a)): synthesize a
1979
+ // STABLE per-verified-key sessionId so the BYO ingress can seed a pool session
1980
+ // binding and 429/529/401/403 failover actually fires. Synthesize ONLY when
1981
+ // the pool is wired — pool-null embedders/tests keep `route.sessionId === null`
1982
+ // (byte-identical to pre-seam). The `outbound:` prefix is namespace-isolated
1983
+ // from real chat-session ids; `verified.id` is a small operator-controlled set,
1984
+ // so `sessionBindings` stays bounded (one binding per named key, not per
1985
+ // request — `sessionBindings` has no TTL).
1986
+ sessionId: deps.proxyDeps.apiKeyPool ? `outbound:${verified.id}` : null
1987
+ });
1988
+ if (!resolved.ok) {
1989
+ writeJsonError(res, resolved.error.status, resolved.error.message);
1990
+ return;
1991
+ }
1992
+ const routeMap = deps.providerProxy.getRouteMap();
1993
+ const token = routeMap.addRoute(resolved.route);
1994
+ try {
1995
+ shimAuthHeader(req, token);
1996
+ const replay = makeReplayRequest(req, rawBody);
1997
+ await routeRequest(replay, res, routeMap, deps.proxyDeps);
1998
+ } catch (err) {
1999
+ const message = serializeError(err);
2000
+ console.error("[OutboundApi] dispatch error:", message);
2001
+ writeJsonError(res, 502, message);
2002
+ } finally {
2003
+ routeMap.removeRoute(token);
2004
+ }
2005
+ }
2006
+ function makeReplayRequest(req, rawBody) {
2007
+ const buf = Buffer.from(rawBody, "utf8");
2008
+ const readable = Readable.from(buf.length > 0 ? [buf] : []);
2009
+ readable.method = req.method;
2010
+ readable.url = req.url;
2011
+ readable.headers = req.headers;
2012
+ readable.httpVersion = req.httpVersion;
2013
+ readable.socket = req.socket;
2014
+ return readable;
2015
+ }
2016
+
2017
+ // src/outbound-api/outboundRateLimiter.ts
2018
+ var DEFAULT_WINDOW_MS = 6e4;
2019
+ var DEFAULT_MAX_REQUESTS = 60;
2020
+ var OutboundRateLimiter = class {
2021
+ windowMs;
2022
+ maxRequests;
2023
+ /** apiKeyId → ascending request timestamps within the current window. */
2024
+ hits = /* @__PURE__ */ new Map();
2025
+ constructor(options = {}) {
2026
+ this.windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
2027
+ this.maxRequests = options.maxRequests ?? DEFAULT_MAX_REQUESTS;
2028
+ }
2029
+ /**
2030
+ * Record a request for `apiKeyId` and decide whether it is allowed. Prunes
2031
+ * timestamps older than the window first; when allowed, the request's
2032
+ * timestamp is appended.
2033
+ */
2034
+ check(apiKeyId, now = Date.now()) {
2035
+ const cutoff = now - this.windowMs;
2036
+ const timestamps = (this.hits.get(apiKeyId) ?? []).filter((ts) => ts > cutoff);
2037
+ if (timestamps.length >= this.maxRequests) {
2038
+ if (timestamps.length === 0) {
2039
+ this.hits.delete(apiKeyId);
2040
+ return { allowed: false, retryAfterSeconds: Math.max(1, Math.ceil(this.windowMs / 1e3)) };
2041
+ }
2042
+ const oldest = timestamps[0];
2043
+ const retryAfterMs = Math.max(0, oldest + this.windowMs - now);
2044
+ this.hits.set(apiKeyId, timestamps);
2045
+ return {
2046
+ allowed: false,
2047
+ retryAfterSeconds: Math.max(1, Math.ceil(retryAfterMs / 1e3))
2048
+ };
2049
+ }
2050
+ timestamps.push(now);
2051
+ this.hits.set(apiKeyId, timestamps);
2052
+ return { allowed: true, retryAfterSeconds: 0 };
2053
+ }
2054
+ /** Drop all recorded state (tests / teardown). */
2055
+ reset() {
2056
+ this.hits.clear();
2057
+ }
2058
+ };
2059
+
2060
+ // src/outbound-api/OutboundApiServer.ts
2061
+ var DEFAULT_OUTBOUND_PORT = 8765;
2062
+ var LOOPBACK_ADDR = "127.0.0.1";
2063
+ var LAN_ADDR = "0.0.0.0";
2064
+ var OutboundApiServer = class {
2065
+ constructor(deps, onPortChange) {
2066
+ this.deps = deps;
2067
+ this.onPortChange = onPortChange;
2068
+ }
2069
+ deps;
2070
+ onPortChange;
2071
+ server = null;
2072
+ boundPort = 0;
2073
+ boundAddr = LOOPBACK_ADDR;
2074
+ endpoints = [];
2075
+ rateLimiter = new OutboundRateLimiter();
2076
+ /**
2077
+ * Apply a config. Restarts the listener ONLY when the bind address or port
2078
+ * changes (or when toggling enabled); per-endpoint routing config is updated
2079
+ * in place (read live per request — no restart).
2080
+ */
2081
+ async applyConfig(input) {
2082
+ this.endpoints = input.endpoints;
2083
+ const wantAddr = input.networkBinding ? LAN_ADDR : LOOPBACK_ADDR;
2084
+ const wantPort = input.port ?? DEFAULT_OUTBOUND_PORT;
2085
+ if (!input.enabled) {
2086
+ await this.stop();
2087
+ return;
2088
+ }
2089
+ const running = this.server !== null;
2090
+ const bindChanged = running && (this.boundAddr !== wantAddr || this.boundPort !== wantPort);
2091
+ if (running && !bindChanged) {
2092
+ return;
2093
+ }
2094
+ if (running) await this.stop();
2095
+ await this.start(wantAddr, wantPort);
2096
+ }
2097
+ /** Start the listener on `bindAddr:port`, falling back on EADDRINUSE. */
2098
+ async start(bindAddr, port) {
2099
+ if (this.server) return this.boundPort;
2100
+ const actualPort = await this.listen(bindAddr, port);
2101
+ this.boundAddr = bindAddr;
2102
+ this.boundPort = actualPort;
2103
+ if (actualPort !== port) this.onPortChange?.(actualPort);
2104
+ console.log(`[OutboundApiServer] Listening on ${bindAddr}:${actualPort}`);
2105
+ return actualPort;
2106
+ }
2107
+ /** Bind once; on EADDRINUSE retry with an ephemeral port (port 0). */
2108
+ listen(bindAddr, port) {
2109
+ return new Promise((resolve, reject) => {
2110
+ const server = http2.createServer((req, res) => {
2111
+ this.onRequest(req, res);
2112
+ });
2113
+ const onError = (err) => {
2114
+ if (err.code === "EADDRINUSE" && port !== 0) {
2115
+ server.removeListener("error", onError);
2116
+ this.listen(bindAddr, 0).then(resolve, reject);
2117
+ return;
2118
+ }
2119
+ reject(err);
2120
+ };
2121
+ server.on("error", onError);
2122
+ server.listen(port, bindAddr, () => {
2123
+ const addr = server.address();
2124
+ if (addr && typeof addr === "object") {
2125
+ server.removeListener("error", onError);
2126
+ server.on("error", (e) => console.error("[OutboundApiServer] server error", serializeError(e)));
2127
+ this.server = server;
2128
+ resolve(addr.port);
2129
+ } else {
2130
+ reject(new Error("Failed to get outbound server address"));
2131
+ }
2132
+ });
2133
+ });
2134
+ }
2135
+ /** Per-request handler. Auth is enforced on EVERY request (incl. loopback). */
2136
+ onRequest(req, res) {
2137
+ handleOutboundRequest(
2138
+ req,
2139
+ res,
2140
+ this.deps,
2141
+ { endpoints: this.endpoints },
2142
+ this.rateLimiter
2143
+ ).catch((err) => {
2144
+ const message = serializeError(err);
2145
+ console.error("[OutboundApiServer] unhandled error:", message);
2146
+ if (!res.headersSent) {
2147
+ res.writeHead(500, { "Content-Type": "application/json" });
2148
+ res.end(JSON.stringify({ error: { type: "outbound_api_error", message } }));
2149
+ }
2150
+ });
2151
+ }
2152
+ /** Stop the listener and release the port. */
2153
+ async stop() {
2154
+ const server = this.server;
2155
+ if (!server) return;
2156
+ this.server = null;
2157
+ this.boundPort = 0;
2158
+ return new Promise((resolve) => {
2159
+ server.close(() => {
2160
+ console.log("[OutboundApiServer] Stopped");
2161
+ resolve();
2162
+ });
2163
+ });
2164
+ }
2165
+ /** A live status snapshot for the Settings tab. */
2166
+ getStatus() {
2167
+ const running = this.server !== null;
2168
+ if (!running) {
2169
+ return {
2170
+ running: false,
2171
+ port: 0,
2172
+ loopbackUrl: null,
2173
+ lanUrl: null,
2174
+ formats: null,
2175
+ lanFormats: null
2176
+ };
2177
+ }
2178
+ const port = this.boundPort;
2179
+ const loopbackBase = `http://${LOOPBACK_ADDR}:${port}`;
2180
+ const isLan = this.boundAddr === LAN_ADDR;
2181
+ const lanIp = isLan ? firstLanIPv4() : null;
2182
+ const lanBase = lanIp ? `http://${lanIp}:${port}` : null;
2183
+ return {
2184
+ running: true,
2185
+ port,
2186
+ loopbackUrl: loopbackBase,
2187
+ lanUrl: lanBase,
2188
+ formats: formatUrls(loopbackBase),
2189
+ lanFormats: lanBase ? formatUrls(lanBase) : null
2190
+ };
2191
+ }
2192
+ };
2193
+ function formatUrls(base) {
2194
+ return {
2195
+ chat: `${base}/v1/chat/completions`,
2196
+ responses: `${base}/v1/responses`,
2197
+ messages: `${base}/v1/messages`,
2198
+ gemini: `${base}/v1beta/models/{model}:generateContent`
2199
+ };
2200
+ }
2201
+ function firstLanIPv4() {
2202
+ const ifaces = networkInterfaces();
2203
+ for (const addrs of Object.values(ifaces)) {
2204
+ if (!addrs) continue;
2205
+ for (const addr of addrs) {
2206
+ if (addr.family === "IPv4" && !addr.internal) return addr.address;
2207
+ }
2208
+ }
2209
+ return null;
2210
+ }
2211
+
2212
+ // src/outbound-api/apiServerConfig.ts
2213
+ var OUTBOUND_API_SERVER_CONFIG_KEY = "outboundApiServer.config";
2214
+ var ALL_ENDPOINTS = ["chat", "responses", "messages", "gemini"];
2215
+ function defaultEndpointConfig(endpoint) {
2216
+ return {
2217
+ endpoint,
2218
+ defaultModel: "",
2219
+ backgroundModel: "",
2220
+ useSubscription: false
2221
+ };
2222
+ }
2223
+ function defaultServerConfig() {
2224
+ return {
2225
+ enabled: false,
2226
+ networkBinding: false,
2227
+ endpoints: ALL_ENDPOINTS.map(defaultEndpointConfig),
2228
+ port: DEFAULT_OUTBOUND_PORT
2229
+ };
2230
+ }
2231
+ function normalizeServerConfig(raw) {
2232
+ const base = defaultServerConfig();
2233
+ if (!raw) return base;
2234
+ const byEndpoint = /* @__PURE__ */ new Map();
2235
+ for (const e of raw.endpoints ?? []) {
2236
+ if (e && ALL_ENDPOINTS.includes(e.endpoint)) {
2237
+ byEndpoint.set(e.endpoint, {
2238
+ endpoint: e.endpoint,
2239
+ defaultModel: e.defaultModel ?? "",
2240
+ backgroundModel: e.backgroundModel ?? "",
2241
+ visionModel: e.visionModel,
2242
+ useSubscription: e.useSubscription === true,
2243
+ backgroundModelIds: e.backgroundModelIds
2244
+ });
2245
+ }
2246
+ }
2247
+ return {
2248
+ enabled: raw.enabled === true,
2249
+ networkBinding: raw.networkBinding === true,
2250
+ endpoints: ALL_ENDPOINTS.map(
2251
+ (ep) => byEndpoint.get(ep) ?? defaultEndpointConfig(ep)
2252
+ ),
2253
+ port: raw.port ?? base.port
2254
+ };
2255
+ }
2256
+ async function loadServerConfig(store) {
2257
+ const raw = await store.get(
2258
+ OUTBOUND_API_SERVER_CONFIG_KEY
2259
+ );
2260
+ return normalizeServerConfig(raw);
2261
+ }
2262
+ async function saveServerConfig(store, config) {
2263
+ await store.set(OUTBOUND_API_SERVER_CONFIG_KEY, config);
2264
+ }
2265
+ function mergeServerConfig(current, patch) {
2266
+ return normalizeServerConfig({
2267
+ enabled: patch.enabled ?? current.enabled,
2268
+ networkBinding: patch.networkBinding ?? current.networkBinding,
2269
+ endpoints: patch.endpoints ?? current.endpoints,
2270
+ port: patch.port ?? current.port
2271
+ });
2272
+ }
2273
+
2274
+ // src/outbound-api/index.ts
2275
+ var instance2 = null;
2276
+ function getOutboundApiServer(deps, onPortChange) {
2277
+ if (!instance2) {
2278
+ if (!deps) {
2279
+ throw new Error("getOutboundApiServer: deps are required on first construction");
2280
+ }
2281
+ instance2 = new OutboundApiServer(deps, onPortChange);
2282
+ }
2283
+ return instance2;
2284
+ }
2285
+ function __resetOutboundApiServerForTests() {
2286
+ instance2 = null;
2287
+ }
2288
+
2289
+ // src/completion/DirectApiHandler.ts
2290
+ async function callOpenAICompletion(provider, apiKey, options, logger) {
2291
+ const request = {
2292
+ model: options.model,
2293
+ messages: options.messages.map((m) => convertMessageToOpenAI(m)),
2294
+ // Only set max_tokens if explicitly provided, otherwise let API use its default
2295
+ ...options.maxTokens ? { max_tokens: options.maxTokens } : {},
2296
+ temperature: options.temperature,
2297
+ stream: false
2298
+ // For now, non-streaming only
2299
+ };
2300
+ if (options.thinkLevel && options.thinkLevel !== "none") {
2301
+ const effort = getOpenAIReasoningEffort(options.thinkLevel);
2302
+ if (effort) {
2303
+ request.reasoning_effort = effort;
2304
+ }
2305
+ }
2306
+ const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: false });
2307
+ const headers = getProviderHeaders(provider, apiKey);
2308
+ logger.info("Calling OpenAI completion API", { url: apiUrl, model: options.model });
2309
+ const response = await fetch(apiUrl, {
2310
+ method: "POST",
2311
+ headers,
2312
+ body: JSON.stringify(request)
2313
+ });
2314
+ if (!response.ok) {
2315
+ const errorText = await response.text();
2316
+ return {
2317
+ success: false,
2318
+ error: `API error (${response.status}): ${errorText}`
2319
+ };
2320
+ }
2321
+ const data = await response.json();
2322
+ const choice = data.choices[0];
2323
+ return {
2324
+ success: true,
2325
+ message: {
2326
+ id: `msg_${Date.now()}`,
2327
+ role: "assistant",
2328
+ content: choice.message.content || "",
2329
+ timestamp: Date.now(),
2330
+ thinking: data.reasoning_content ? { content: data.reasoning_content } : void 0
2331
+ },
2332
+ usage: {
2333
+ promptTokens: data.usage.prompt_tokens,
2334
+ completionTokens: data.usage.completion_tokens,
2335
+ totalTokens: data.usage.total_tokens
2336
+ }
2337
+ };
2338
+ }
2339
+ async function callAnthropicCompletion(provider, apiKey, options, logger) {
2340
+ const hasImages = options.messages.some((m) => m.images && m.images.length > 0);
2341
+ if (hasImages) {
2342
+ const systemMessages = options.messages.filter((m) => m.role === "system");
2343
+ const nonSystemMessages = options.messages.filter((m) => m.role !== "system");
2344
+ const anthropicRequest2 = {
2345
+ model: options.model,
2346
+ max_tokens: options.maxTokens || 16384,
2347
+ temperature: options.temperature,
2348
+ ...systemMessages.length > 0 ? { system: systemMessages.map((m) => m.content).join("\n\n") } : {},
2349
+ messages: nonSystemMessages.map((m) => convertMessageToAnthropic(m)),
2350
+ stream: false
2351
+ };
2352
+ const apiUrl2 = buildProviderApiUrl(provider, { model: options.model, stream: false });
2353
+ const headers2 = getProviderHeaders(provider, apiKey);
2354
+ logger.info("Calling Anthropic completion API with images", { url: apiUrl2, model: options.model });
2355
+ const response2 = await fetch(apiUrl2, {
2356
+ method: "POST",
2357
+ headers: headers2,
2358
+ body: JSON.stringify(anthropicRequest2)
2359
+ });
2360
+ if (!response2.ok) {
2361
+ const errorText = await response2.text();
2362
+ return {
2363
+ success: false,
2364
+ error: `API error (${response2.status}): ${errorText}`
2365
+ };
2366
+ }
2367
+ const anthropicResponse2 = await response2.json();
2368
+ const openaiResponse2 = convertAnthropicToOpenAI(anthropicResponse2);
2369
+ const choice2 = openaiResponse2.choices[0];
2370
+ return {
2371
+ success: true,
2372
+ message: {
2373
+ id: `assistant-${Date.now()}`,
2374
+ role: "assistant",
2375
+ content: choice2.message?.content || "",
2376
+ timestamp: Date.now()
2377
+ },
2378
+ usage: openaiResponse2.usage ? {
2379
+ promptTokens: openaiResponse2.usage.prompt_tokens,
2380
+ completionTokens: openaiResponse2.usage.completion_tokens,
2381
+ totalTokens: openaiResponse2.usage.total_tokens
2382
+ } : void 0
2383
+ };
2384
+ }
2385
+ const openaiRequest = {
2386
+ model: options.model,
2387
+ messages: options.messages.map((m) => ({
2388
+ role: m.role,
2389
+ content: m.content
2390
+ })),
2391
+ // Anthropic requires max_tokens; use 16384 default if not explicitly set
2392
+ max_tokens: options.maxTokens || 16384,
2393
+ temperature: options.temperature,
2394
+ stream: false
2395
+ };
2396
+ const config = {
2397
+ defaultModel: options.model
2398
+ };
2399
+ const anthropicRequest = convertOpenAIToAnthropic(openaiRequest, config);
2400
+ const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: false });
2401
+ const headers = getProviderHeaders(provider, apiKey);
2402
+ logger.info("Calling Anthropic completion API", { url: apiUrl, model: options.model });
2403
+ const response = await fetch(apiUrl, {
2404
+ method: "POST",
2405
+ headers,
2406
+ body: JSON.stringify(anthropicRequest)
2407
+ });
2408
+ if (!response.ok) {
2409
+ const errorText = await response.text();
2410
+ return {
2411
+ success: false,
2412
+ error: `API error (${response.status}): ${errorText}`
2413
+ };
2414
+ }
2415
+ const anthropicResponse = await response.json();
2416
+ const openaiResponse = convertAnthropicToOpenAI(anthropicResponse);
2417
+ const choice = openaiResponse.choices[0];
2418
+ return {
2419
+ success: true,
2420
+ message: {
2421
+ id: `msg_${Date.now()}`,
2422
+ role: "assistant",
2423
+ content: choice.message.content || "",
2424
+ timestamp: Date.now(),
2425
+ thinking: openaiResponse.reasoning_content ? { content: openaiResponse.reasoning_content } : void 0
2426
+ },
2427
+ usage: {
2428
+ promptTokens: openaiResponse.usage.prompt_tokens,
2429
+ completionTokens: openaiResponse.usage.completion_tokens,
2430
+ totalTokens: openaiResponse.usage.total_tokens
2431
+ }
2432
+ };
2433
+ }
2434
+ async function callGeminiCompletion(provider, apiKey, options, logger) {
2435
+ const contents = [];
2436
+ let systemInstruction;
2437
+ for (const msg of options.messages) {
2438
+ if (msg.role === "system") {
2439
+ systemInstruction = { parts: [{ text: msg.content }] };
2440
+ } else {
2441
+ contents.push(convertMessageToGemini(msg));
2442
+ }
2443
+ }
2444
+ const request = {
2445
+ contents,
2446
+ generationConfig: {
2447
+ ...options.maxTokens ? { maxOutputTokens: options.maxTokens } : {},
2448
+ ...options.temperature !== void 0 ? { temperature: options.temperature } : {}
2449
+ }
2450
+ };
2451
+ if (systemInstruction) {
2452
+ request.systemInstruction = systemInstruction;
2453
+ }
2454
+ const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: false });
2455
+ const headers = getProviderHeaders(provider, apiKey);
2456
+ logger.info("Calling Gemini completion API", { url: apiUrl, model: options.model });
2457
+ const response = await fetch(apiUrl, {
2458
+ method: "POST",
2459
+ headers,
2460
+ body: JSON.stringify(request)
2461
+ });
2462
+ if (!response.ok) {
2463
+ const errorText = await response.text();
2464
+ return {
2465
+ success: false,
2466
+ error: `API error (${response.status}): ${errorText}`
2467
+ };
2468
+ }
2469
+ const data = await response.json();
2470
+ const candidates = data.candidates;
2471
+ if (!candidates || candidates.length === 0) {
2472
+ return {
2473
+ success: false,
2474
+ error: "No candidates in response"
2475
+ };
2476
+ }
2477
+ const candidate = candidates[0];
2478
+ const content = candidate.content;
2479
+ let textContent = "";
2480
+ if (content?.parts) {
2481
+ for (const part of content.parts) {
2482
+ if (part.text) {
2483
+ textContent += part.text;
2484
+ }
2485
+ }
2486
+ }
2487
+ const usage = data.usageMetadata;
2488
+ return {
2489
+ success: true,
2490
+ message: {
2491
+ id: `msg_${Date.now()}`,
2492
+ role: "assistant",
2493
+ content: textContent,
2494
+ timestamp: Date.now()
2495
+ },
2496
+ usage: usage ? {
2497
+ promptTokens: usage.promptTokenCount || 0,
2498
+ completionTokens: usage.candidatesTokenCount || 0,
2499
+ totalTokens: usage.totalTokenCount || 0
2500
+ } : void 0
2501
+ };
2502
+ }
2503
+ async function callOpenAIResponseCompletion(provider, apiKey, options, logger) {
2504
+ const input = [];
2505
+ for (const msg of options.messages) {
2506
+ if (msg.role === "system") {
2507
+ input.push({ role: "developer", content: msg.content });
2508
+ } else {
2509
+ input.push({ role: msg.role, content: msg.content });
2510
+ }
2511
+ }
2512
+ const request = {
2513
+ model: options.model,
2514
+ input,
2515
+ stream: false,
2516
+ ...options.maxTokens ? { max_output_tokens: options.maxTokens } : {},
2517
+ ...options.temperature !== void 0 ? { temperature: options.temperature } : {}
2518
+ };
2519
+ if (options.thinkLevel && options.thinkLevel !== "none") {
2520
+ const effort = getOpenAIReasoningEffort(options.thinkLevel);
2521
+ if (effort) {
2522
+ request.reasoning = { effort, summary: "auto" };
2523
+ }
2524
+ }
2525
+ const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: false });
2526
+ const headers = getProviderHeaders(provider, apiKey);
2527
+ logger.info("Calling OpenAI Response API", { url: apiUrl, model: options.model });
2528
+ const response = await fetch(apiUrl, {
2529
+ method: "POST",
2530
+ headers,
2531
+ body: JSON.stringify(request)
2532
+ });
2533
+ if (!response.ok) {
2534
+ const errorText = await response.text();
2535
+ return {
2536
+ success: false,
2537
+ error: `API error (${response.status}): ${errorText}`
2538
+ };
2539
+ }
2540
+ const data = await response.json();
2541
+ let textContent = "";
2542
+ const output = data.output;
2543
+ if (output) {
2544
+ for (const item of output) {
2545
+ if (item.type === "message") {
2546
+ const content = item.content;
2547
+ if (content) {
2548
+ for (const part of content) {
2549
+ if (part.type === "output_text" && typeof part.text === "string") {
2550
+ textContent += part.text;
2551
+ }
2552
+ }
2553
+ }
2554
+ }
2555
+ }
2556
+ }
2557
+ const usage = data.usage;
2558
+ return {
2559
+ success: true,
2560
+ message: {
2561
+ id: `msg_${Date.now()}`,
2562
+ role: "assistant",
2563
+ content: textContent,
2564
+ timestamp: Date.now()
2565
+ },
2566
+ usage: usage ? {
2567
+ promptTokens: usage.input_tokens || 0,
2568
+ completionTokens: usage.output_tokens || 0,
2569
+ totalTokens: (usage.input_tokens || 0) + (usage.output_tokens || 0)
2570
+ } : void 0
2571
+ };
2572
+ }
2573
+
2574
+ // src/completion/StreamHandler.ts
2575
+ import { buildAnthropicThinking, getOpenAIReasoningEffort as getOpenAIReasoningEffort2 } from "@omnicross/contracts/thinking-config";
2576
+ async function streamOpenAICompletion(provider, apiKey, options, messageId, callbacks, logger) {
2577
+ const request = {
2578
+ model: options.model,
2579
+ messages: options.messages.map((m) => convertMessageToOpenAI(m)),
2580
+ // Only set max_tokens if explicitly provided, otherwise let API use its default
2581
+ ...options.maxTokens ? { max_tokens: options.maxTokens } : {},
2582
+ temperature: options.temperature,
2583
+ stream: true
2584
+ };
2585
+ if (options.thinkLevel && options.thinkLevel !== "none") {
2586
+ const effort = getOpenAIReasoningEffort2(options.thinkLevel);
2587
+ if (effort) {
2588
+ request.reasoning_effort = effort;
2589
+ }
2590
+ }
2591
+ const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: true });
2592
+ const headers = getProviderHeaders(provider, apiKey);
2593
+ if (options.nativeSearchAugmentation) {
2594
+ applyAugmentation(request, options.nativeSearchAugmentation);
2595
+ }
2596
+ logger.info("Streaming OpenAI completion request", {
2597
+ url: apiUrl,
2598
+ providerId: options.providerId,
2599
+ model: options.model,
2600
+ maxTokens: options.maxTokens,
2601
+ max_tokens_in_request: request.max_tokens,
2602
+ temperature: options.temperature,
2603
+ messagesCount: options.messages.length,
2604
+ hasNativeSearchAugmentation: !!options.nativeSearchAugmentation
2605
+ });
2606
+ const response = await fetch(apiUrl, {
2607
+ method: "POST",
2608
+ headers,
2609
+ body: JSON.stringify(request)
2610
+ });
2611
+ if (!response.ok) {
2612
+ const errorText = await response.text();
2613
+ callbacks.onError?.(`API error (${response.status}): ${errorText}`);
2614
+ return;
2615
+ }
2616
+ const result = await streamSSEResponse(response, "openai", {
2617
+ onDelta: callbacks.onDelta,
2618
+ onReasoning: callbacks.onReasoning,
2619
+ onAudio: callbacks.onAudio,
2620
+ onVideo: callbacks.onVideo,
2621
+ onError: callbacks.onError
2622
+ });
2623
+ callbacks.onDone?.(
2624
+ {
2625
+ id: messageId,
2626
+ role: "assistant",
2627
+ content: result.content,
2628
+ timestamp: Date.now(),
2629
+ thinking: result.reasoning ? { content: result.reasoning } : void 0,
2630
+ audios: result.audios.length > 0 ? result.audios : void 0,
2631
+ videos: result.videos.length > 0 ? result.videos : void 0
2632
+ },
2633
+ result.usage,
2634
+ result.metrics
2635
+ );
2636
+ }
2637
+ async function streamAnthropicCompletion(provider, apiKey, options, messageId, callbacks, logger) {
2638
+ const hasImages = options.messages.some((m) => m.images && m.images.length > 0);
2639
+ const MAX_TOKENS_FOR_THINKING = 16384;
2640
+ let effectiveMaxTokens = options.maxTokens || 16384;
2641
+ const thinkingMaxTokens = options.thinkLevel && options.thinkLevel !== "none" ? Math.min(effectiveMaxTokens, MAX_TOKENS_FOR_THINKING) : effectiveMaxTokens;
2642
+ const thinkingConfig = options.thinkLevel && options.thinkLevel !== "none" ? buildAnthropicThinking(options.model, options.thinkLevel, thinkingMaxTokens) : void 0;
2643
+ if (thinkingConfig) {
2644
+ effectiveMaxTokens = thinkingMaxTokens;
2645
+ }
2646
+ logger.debug("Anthropic thinking configuration", {
2647
+ thinkLevel: options.thinkLevel,
2648
+ thinkingConfig,
2649
+ effectiveMaxTokens
2650
+ });
2651
+ let anthropicRequest;
2652
+ if (hasImages) {
2653
+ const systemMessages = options.messages.filter((m) => m.role === "system");
2654
+ const nonSystemMessages = options.messages.filter((m) => m.role !== "system");
2655
+ anthropicRequest = {
2656
+ model: options.model,
2657
+ max_tokens: effectiveMaxTokens,
2658
+ // Omit temperature when thinking is enabled (Anthropic will use default temperature=1)
2659
+ ...thinkingConfig ? {} : { temperature: options.temperature },
2660
+ ...systemMessages.length > 0 ? { system: systemMessages.map((m) => m.content).join("\n\n") } : {},
2661
+ messages: nonSystemMessages.map((m) => convertMessageToAnthropic(m)),
2662
+ stream: true,
2663
+ ...thinkingConfig ? { thinking: thinkingConfig } : {}
2664
+ };
2665
+ } else {
2666
+ const config = {
2667
+ defaultModel: options.model
2668
+ };
2669
+ const openaiRequest = {
2670
+ model: options.model,
2671
+ messages: options.messages.map((m) => ({
2672
+ role: m.role,
2673
+ content: m.content
2674
+ })),
2675
+ // Anthropic requires max_tokens; use adjusted value
2676
+ max_tokens: effectiveMaxTokens,
2677
+ // Omit temperature when thinking is enabled (Anthropic will use default temperature=1)
2678
+ temperature: thinkingConfig ? void 0 : options.temperature,
2679
+ stream: true
2680
+ };
2681
+ anthropicRequest = convertOpenAIToAnthropic(openaiRequest, config);
2682
+ if (thinkingConfig) {
2683
+ anthropicRequest.thinking = thinkingConfig;
2684
+ delete anthropicRequest.temperature;
2685
+ }
2686
+ }
2687
+ const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: true });
2688
+ const headers = getProviderHeaders(provider, apiKey);
2689
+ if (options.nativeSearchAugmentation) {
2690
+ applyAugmentation(anthropicRequest, options.nativeSearchAugmentation);
2691
+ }
2692
+ logger.info("Streaming Anthropic completion request", {
2693
+ url: apiUrl,
2694
+ model: anthropicRequest.model,
2695
+ messagesCount: anthropicRequest.messages?.length,
2696
+ max_tokens: anthropicRequest.max_tokens,
2697
+ temperature: anthropicRequest.temperature,
2698
+ stream: anthropicRequest.stream,
2699
+ hasImages,
2700
+ hasThinking: !!anthropicRequest.thinking,
2701
+ hasNativeSearchAugmentation: !!options.nativeSearchAugmentation
2702
+ });
2703
+ const response = await fetch(apiUrl, {
2704
+ method: "POST",
2705
+ headers,
2706
+ body: JSON.stringify(anthropicRequest)
2707
+ });
2708
+ logger.debug("Anthropic response status", { status: response.status });
2709
+ if (!response.ok) {
2710
+ const errorText = await response.text();
2711
+ logger.error("Anthropic API error", void 0, { status: response.status, errorText });
2712
+ callbacks.onError?.(`API error (${response.status}): ${errorText}`);
2713
+ return;
2714
+ }
2715
+ const result = await streamSSEResponse(response, "anthropic", {
2716
+ onDelta: callbacks.onDelta,
2717
+ onReasoning: callbacks.onReasoning,
2718
+ onError: callbacks.onError,
2719
+ onBlock: callbacks.onBlock
2720
+ });
2721
+ logger.info("Anthropic stream complete", {
2722
+ contentLength: result.content.length,
2723
+ reasoningLength: result.reasoning?.length || 0,
2724
+ blocksCount: result.blocks.length
2725
+ });
2726
+ callbacks.onDone?.(
2727
+ {
2728
+ id: messageId,
2729
+ role: "assistant",
2730
+ content: result.content,
2731
+ timestamp: Date.now(),
2732
+ thinking: result.reasoning ? { content: result.reasoning } : void 0,
2733
+ blocks: result.blocks.length > 0 ? result.blocks : void 0
2734
+ },
2735
+ result.usage,
2736
+ result.metrics
2737
+ );
2738
+ }
2739
+ async function streamGeminiCompletion(provider, apiKey, options, messageId, callbacks, logger) {
2740
+ const contents = [];
2741
+ let systemInstruction;
2742
+ for (const msg of options.messages) {
2743
+ if (msg.role === "system") {
2744
+ systemInstruction = { parts: [{ text: msg.content }] };
2745
+ } else {
2746
+ contents.push(convertMessageToGemini(msg));
2747
+ }
2748
+ }
2749
+ const request = {
2750
+ contents,
2751
+ generationConfig: {
2752
+ ...options.maxTokens ? { maxOutputTokens: options.maxTokens } : {},
2753
+ ...options.temperature !== void 0 ? { temperature: options.temperature } : {}
2754
+ }
2755
+ };
2756
+ if (systemInstruction) {
2757
+ request.systemInstruction = systemInstruction;
2758
+ }
2759
+ const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: true });
2760
+ const headers = getProviderHeaders(provider, apiKey);
2761
+ if (options.nativeSearchAugmentation) {
2762
+ applyAugmentation(request, options.nativeSearchAugmentation);
2763
+ }
2764
+ logger.info("Streaming Gemini completion request", {
2765
+ url: apiUrl,
2766
+ model: options.model,
2767
+ contentsCount: contents.length,
2768
+ hasSystemInstruction: !!systemInstruction,
2769
+ maxOutputTokens: options.maxTokens,
2770
+ temperature: options.temperature,
2771
+ hasNativeSearchAugmentation: !!options.nativeSearchAugmentation
2772
+ });
2773
+ const response = await fetch(apiUrl, {
2774
+ method: "POST",
2775
+ headers,
2776
+ body: JSON.stringify(request)
2777
+ });
2778
+ logger.debug("Gemini response status", { status: response.status });
2779
+ if (!response.ok) {
2780
+ const errorText = await response.text();
2781
+ logger.error("Gemini API error", void 0, { status: response.status, errorText });
2782
+ callbacks.onError?.(`API error (${response.status}): ${errorText}`);
2783
+ return;
2784
+ }
2785
+ const result = await streamSSEResponse(response, "gemini", {
2786
+ onDelta: callbacks.onDelta,
2787
+ onReasoning: callbacks.onReasoning,
2788
+ onError: callbacks.onError
2789
+ });
2790
+ logger.info("Gemini stream complete", { contentLength: result.content.length });
2791
+ callbacks.onDone?.(
2792
+ {
2793
+ id: messageId,
2794
+ role: "assistant",
2795
+ content: result.content,
2796
+ timestamp: Date.now(),
2797
+ thinking: result.reasoning ? { content: result.reasoning } : void 0
2798
+ },
2799
+ result.usage,
2800
+ result.metrics
2801
+ );
2802
+ }
2803
+ async function streamOpenAIResponseCompletion(provider, apiKey, options, messageId, callbacks, logger) {
2804
+ const input = [];
2805
+ for (const msg of options.messages) {
2806
+ if (msg.role === "system") {
2807
+ input.push({ role: "developer", content: msg.content });
2808
+ } else {
2809
+ input.push({ role: msg.role, content: msg.content });
2810
+ }
2811
+ }
2812
+ const request = {
2813
+ model: options.model,
2814
+ input,
2815
+ stream: true,
2816
+ ...options.maxTokens ? { max_output_tokens: options.maxTokens } : {},
2817
+ ...options.temperature !== void 0 ? { temperature: options.temperature } : {}
2818
+ };
2819
+ if (options.thinkLevel && options.thinkLevel !== "none") {
2820
+ const effort = getOpenAIReasoningEffort2(options.thinkLevel);
2821
+ if (effort) {
2822
+ request.reasoning = { effort, summary: "auto" };
2823
+ }
2824
+ }
2825
+ const apiUrl = buildProviderApiUrl(provider, { model: options.model, stream: true });
2826
+ const headers = getProviderHeaders(provider, apiKey);
2827
+ logger.info("Streaming OpenAI Response API request", {
2828
+ url: apiUrl,
2829
+ providerId: options.providerId,
2830
+ model: options.model,
2831
+ maxOutputTokens: options.maxTokens,
2832
+ temperature: options.temperature,
2833
+ inputCount: input.length,
2834
+ reasoning: request.reasoning
2835
+ });
2836
+ const response = await fetch(apiUrl, {
2837
+ method: "POST",
2838
+ headers,
2839
+ body: JSON.stringify(request)
2840
+ });
2841
+ if (!response.ok) {
2842
+ const errorText = await response.text();
2843
+ callbacks.onError?.(`API error (${response.status}): ${errorText}`);
2844
+ return;
2845
+ }
2846
+ const result = await streamSSEResponse(response, "openai-response", {
2847
+ onDelta: callbacks.onDelta,
2848
+ onReasoning: callbacks.onReasoning,
2849
+ onError: callbacks.onError
2850
+ });
2851
+ logger.info("OpenAI Response API stream complete", { contentLength: result.content.length });
2852
+ callbacks.onDone?.(
2853
+ {
2854
+ id: messageId,
2855
+ role: "assistant",
2856
+ content: result.content,
2857
+ timestamp: Date.now(),
2858
+ thinking: result.reasoning ? { content: result.reasoning } : void 0
2859
+ },
2860
+ result.usage,
2861
+ result.metrics
2862
+ );
2863
+ }
2864
+
2865
+ // src/completion/ThinkingResolver.ts
2866
+ import {
2867
+ buildAnthropicThinking as buildAnthropicThinking2,
2868
+ calculateThinkingBudget,
2869
+ DEFAULT_MAX_TOKENS,
2870
+ getClaudeMaxTokens,
2871
+ isReasoningModel
2872
+ } from "@omnicross/contracts/thinking-config";
2873
+ async function resolveEffectiveMaxTokens(llmConfig, getProvider, logger, providerId, modelId, sessionMaxTokens) {
2874
+ if (sessionMaxTokens !== void 0 && sessionMaxTokens > 0) {
2875
+ logger.debug("Using session maxTokens", { sessionMaxTokens });
2876
+ return sessionMaxTokens;
2877
+ }
2878
+ try {
2879
+ const globalParams = await llmConfig.getGlobalModelParameters();
2880
+ if (globalParams?.maxTokens?.enabled && globalParams.maxTokens.value > 0) {
2881
+ logger.debug("Using global maxTokens", { maxTokens: globalParams.maxTokens.value });
2882
+ return globalParams.maxTokens.value;
2883
+ }
2884
+ } catch (err) {
2885
+ logger.warn("Failed to get global params", err instanceof Error ? err : void 0);
2886
+ }
2887
+ const MAX_TOKENS_CAP = 131072;
2888
+ const provider = await getProvider(providerId);
2889
+ if (provider) {
2890
+ const modelConfig = provider.modelConfigs?.find((m) => m.id === modelId);
2891
+ if (modelConfig?.maxTokens && modelConfig.maxTokens > 0) {
2892
+ const cappedMaxTokens = Math.min(modelConfig.maxTokens, MAX_TOKENS_CAP);
2893
+ logger.debug("Using model config maxTokens", {
2894
+ maxTokens: modelConfig.maxTokens,
2895
+ cappedMaxTokens
2896
+ });
2897
+ return cappedMaxTokens;
2898
+ }
2899
+ if (provider.modelGroups) {
2900
+ for (const group of provider.modelGroups) {
2901
+ const model = group.models?.find((m) => m.id === modelId);
2902
+ if (model?.maxTokens && model.maxTokens > 0) {
2903
+ const cappedMaxTokens = Math.min(model.maxTokens, MAX_TOKENS_CAP);
2904
+ logger.debug("Using modelGroup model maxTokens", {
2905
+ maxTokens: model.maxTokens,
2906
+ cappedMaxTokens
2907
+ });
2908
+ return cappedMaxTokens;
2909
+ }
2910
+ }
2911
+ }
2912
+ }
2913
+ try {
2914
+ const discoveredMaxTokens = await llmConfig.getDiscoveredModelMaxTokens(providerId, modelId);
2915
+ if (discoveredMaxTokens && discoveredMaxTokens > 0) {
2916
+ const cappedMaxTokens = Math.min(discoveredMaxTokens, MAX_TOKENS_CAP);
2917
+ logger.debug("Using discovered model maxTokens", {
2918
+ discoveredMaxTokens,
2919
+ cappedMaxTokens
2920
+ });
2921
+ return cappedMaxTokens;
2922
+ }
2923
+ } catch (err) {
2924
+ logger.warn("Failed to get discovered model maxTokens", err instanceof Error ? err : void 0);
2925
+ }
2926
+ logger.debug("No maxTokens configured, returning undefined");
2927
+ return void 0;
2928
+ }
2929
+ async function getRequiredMaxTokens(llmConfig, getProvider, logger, providerId, modelId, sessionMaxTokens) {
2930
+ const resolved = await resolveEffectiveMaxTokens(llmConfig, getProvider, logger, providerId, modelId, sessionMaxTokens);
2931
+ if (resolved !== void 0) {
2932
+ return resolved;
2933
+ }
2934
+ logger.debug("Using default maxTokens", { defaultMaxTokens: DEFAULT_MAX_TOKENS });
2935
+ return DEFAULT_MAX_TOKENS;
2936
+ }
2937
+ async function resolveThinkingBudget(getProvider, logger, providerId, modelId, maxTokens, thinkLevel) {
2938
+ if (thinkLevel === "none" || !isReasoningModel(modelId)) {
2939
+ return {
2940
+ adjustedMaxTokens: maxTokens,
2941
+ thinkingBudget: void 0,
2942
+ thinkingConfig: void 0
2943
+ };
2944
+ }
2945
+ const thinkingBudget = calculateThinkingBudget(modelId, thinkLevel, maxTokens);
2946
+ const provider = await getProvider(providerId);
2947
+ const providerName = provider?.name?.toLowerCase() || "";
2948
+ const apiFormat = provider ? resolveApiFormat(provider) : "openai";
2949
+ if (apiFormat === "anthropic" || providerName === "anthropic" || providerName.includes("claude")) {
2950
+ const thinkingConfig = buildAnthropicThinking2(modelId, thinkLevel, maxTokens);
2951
+ const adjustedMaxTokens = getClaudeMaxTokens(maxTokens, thinkingBudget) || maxTokens;
2952
+ logger.debug("Claude model thinking budget", {
2953
+ thinkingBudget,
2954
+ adjustedMaxTokens
2955
+ });
2956
+ return {
2957
+ adjustedMaxTokens,
2958
+ thinkingBudget,
2959
+ thinkingConfig
2960
+ };
2961
+ }
2962
+ logger.debug("Non-Claude model thinking budget", { thinkingBudget });
2963
+ return {
2964
+ adjustedMaxTokens: maxTokens,
2965
+ thinkingBudget,
2966
+ thinkingConfig: thinkingBudget ? { type: "enabled", budget_tokens: thinkingBudget } : void 0
2967
+ };
2968
+ }
2969
+
2970
+ // src/completion/ToolExecutor.ts
2971
+ function logToolFormat(tools, logger) {
2972
+ if (!Array.isArray(tools) || tools.length === 0) {
2973
+ logger.warn("No tools provided");
2974
+ return;
2975
+ }
2976
+ const firstTool = tools[0];
2977
+ if ("function" in firstTool) {
2978
+ const openaiTools = tools;
2979
+ logger.info("Tools configured (OpenAI format)", {
2980
+ count: openaiTools.length,
2981
+ tools: openaiTools.map((t) => t.function.name),
2982
+ firstTool: {
2983
+ name: firstTool.function.name,
2984
+ description: firstTool.function.description?.slice(0, 100),
2985
+ parameters: firstTool.function.parameters
2986
+ }
2987
+ });
2988
+ } else if ("input_schema" in firstTool) {
2989
+ const anthropicTools = tools;
2990
+ logger.info("Tools configured (Anthropic format)", {
2991
+ count: anthropicTools.length,
2992
+ tools: anthropicTools.map((t) => t.name),
2993
+ firstTool: {
2994
+ name: firstTool.name,
2995
+ description: firstTool.description?.slice(0, 100),
2996
+ input_schema: firstTool.input_schema
2997
+ }
2998
+ });
2999
+ } else if ("functionDeclarations" in firstTool) {
3000
+ const geminiTools = tools;
3001
+ const allDeclarations = geminiTools.flatMap((t) => t.functionDeclarations);
3002
+ logger.info("Tools configured (Gemini format)", {
3003
+ count: allDeclarations.length,
3004
+ tools: allDeclarations.map((t) => t.name),
3005
+ firstTool: {
3006
+ name: allDeclarations[0]?.name,
3007
+ description: allDeclarations[0]?.description?.slice(0, 100),
3008
+ parameters: allDeclarations[0]?.parameters
3009
+ }
3010
+ });
3011
+ }
3012
+ }
3013
+ function buildToolRequest(apiFormat, conversationMessages, actualModel, options, provider) {
3014
+ let requestBody;
3015
+ let url;
3016
+ if (apiFormat === "google") {
3017
+ const contents = [];
3018
+ for (const msg of conversationMessages) {
3019
+ if (msg.role !== "system") {
3020
+ contents.push({
3021
+ role: msg.role === "assistant" ? "model" : "user",
3022
+ parts: [{ text: msg.content }]
3023
+ });
3024
+ }
3025
+ }
3026
+ requestBody = {
3027
+ contents,
3028
+ tools: options.tools
3029
+ };
3030
+ url = buildProviderApiUrl(provider, { model: actualModel, stream: true });
3031
+ } else if (apiFormat === "anthropic") {
3032
+ requestBody = {
3033
+ model: actualModel,
3034
+ messages: conversationMessages.filter((m) => m.role !== "system").map((m) => ({
3035
+ role: m.role,
3036
+ content: m.content
3037
+ })),
3038
+ max_tokens: options.maxTokens || 4096,
3039
+ temperature: options.temperature ?? 0.7,
3040
+ stream: true,
3041
+ tools: options.tools
3042
+ };
3043
+ const systemMsg = conversationMessages.find((m) => m.role === "system");
3044
+ if (systemMsg) {
3045
+ requestBody.system = systemMsg.content;
3046
+ }
3047
+ url = buildProviderApiUrl(provider, { model: actualModel, stream: true });
3048
+ } else {
3049
+ requestBody = {
3050
+ model: actualModel,
3051
+ messages: conversationMessages.map((m) => ({
3052
+ role: m.role,
3053
+ content: m.content
3054
+ })),
3055
+ max_tokens: options.maxTokens || 4096,
3056
+ temperature: options.temperature ?? 0.7,
3057
+ stream: true,
3058
+ tools: options.tools
3059
+ };
3060
+ url = buildProviderApiUrl(provider, { model: actualModel, stream: true });
3061
+ }
3062
+ return { requestBody, url };
3063
+ }
3064
+ function extractDeltaContent(rawJson, apiFormat) {
3065
+ const json = rawJson;
3066
+ if (apiFormat === "google") {
3067
+ const parts = json.candidates?.[0]?.content?.parts || [];
3068
+ let text = "";
3069
+ for (const part of parts) {
3070
+ if (part.thought === true) continue;
3071
+ if (part.text) text += part.text;
3072
+ }
3073
+ return text;
3074
+ } else if (apiFormat === "anthropic") {
3075
+ return json.delta?.text || "";
3076
+ } else {
3077
+ return json.choices?.[0]?.delta?.content || "";
3078
+ }
3079
+ }
3080
+ function extractDeltaReasoning(rawJson, apiFormat) {
3081
+ const json = rawJson;
3082
+ if (apiFormat === "google") {
3083
+ const parts = json.candidates?.[0]?.content?.parts || [];
3084
+ let reasoning = "";
3085
+ for (const part of parts) {
3086
+ if (part.thought === true && part.text) reasoning += part.text;
3087
+ }
3088
+ return reasoning;
3089
+ } else if (apiFormat === "anthropic") {
3090
+ return "";
3091
+ } else {
3092
+ const delta = json.choices?.[0]?.delta;
3093
+ return (delta?.thinking?.content || "") + (delta?.reasoning_content || "");
3094
+ }
3095
+ }
3096
+ function extractToolCalls(rawJson, apiFormat, toolCalls, callbacks, logger, pendingOpenAIToolCalls) {
3097
+ const json = rawJson;
3098
+ if (apiFormat === "google") {
3099
+ const parts = json.candidates?.[0]?.content?.parts || [];
3100
+ for (const part of parts) {
3101
+ if (part.functionCall) {
3102
+ logger.info("Function call detected", { functionCall: part.functionCall });
3103
+ const toolCall = {
3104
+ id: `call_${Date.now()}_${Math.random().toString(36).slice(2)}`,
3105
+ name: part.functionCall.name,
3106
+ args: part.functionCall.args || {}
3107
+ };
3108
+ toolCalls.push(toolCall);
3109
+ callbacks.onToolCall?.(toolCall);
3110
+ logger.info("Tool called", { toolName: toolCall.name });
3111
+ }
3112
+ }
3113
+ } else if (apiFormat !== "anthropic") {
3114
+ const delta = json.choices?.[0]?.delta;
3115
+ if (delta?.tool_calls && pendingOpenAIToolCalls) {
3116
+ for (const tc of delta.tool_calls) {
3117
+ const index = tc.index ?? 0;
3118
+ let pending = pendingOpenAIToolCalls.get(index);
3119
+ if (!pending) {
3120
+ pending = { id: "", name: "", arguments: "" };
3121
+ pendingOpenAIToolCalls.set(index, pending);
3122
+ }
3123
+ if (tc.id) pending.id = tc.id;
3124
+ if (tc.function?.name) pending.name += tc.function.name;
3125
+ if (tc.function?.arguments) pending.arguments += tc.function.arguments;
3126
+ }
3127
+ }
3128
+ }
3129
+ }
3130
+ function finalizeOpenAIToolCalls(pendingOpenAIToolCalls, toolCalls, callbacks, logger) {
3131
+ for (const [, pending] of pendingOpenAIToolCalls) {
3132
+ if (pending.name) {
3133
+ try {
3134
+ const toolCall = {
3135
+ id: pending.id || `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`,
3136
+ name: pending.name,
3137
+ args: JSON.parse(pending.arguments || "{}")
3138
+ };
3139
+ toolCalls.push(toolCall);
3140
+ callbacks.onToolCall?.(toolCall);
3141
+ logger.info("Tool called (OpenAI)", { toolName: toolCall.name, args: toolCall.args });
3142
+ } catch (e) {
3143
+ logger.error("Failed to parse OpenAI tool call arguments", e instanceof Error ? e : void 0, {
3144
+ name: pending.name,
3145
+ arguments: pending.arguments
3146
+ });
3147
+ }
3148
+ }
3149
+ }
3150
+ }
3151
+ function parseStreamChunk(rawJson, apiFormat, callbacks, logger) {
3152
+ const json = rawJson;
3153
+ if (apiFormat === "google") {
3154
+ const candidate = json.candidates?.[0];
3155
+ if (!candidate) {
3156
+ logger.warn("No candidate found in Gemini response");
3157
+ return;
3158
+ }
3159
+ const parts = candidate.content?.parts || [];
3160
+ for (const part of parts) {
3161
+ if (part.thought === true && part.text) {
3162
+ callbacks.onReasoning?.(part.text);
3163
+ continue;
3164
+ } else if (part.thoughtSignature && !part.thought) {
3165
+ logger.debug("Gemini returned thoughtSignature without thought content");
3166
+ }
3167
+ if (part.text) {
3168
+ callbacks.onDelta?.(part.text);
3169
+ }
3170
+ }
3171
+ } else if (apiFormat === "anthropic") {
3172
+ const delta = json.delta;
3173
+ if (delta?.text) {
3174
+ callbacks.onDelta?.(delta.text);
3175
+ }
3176
+ } else {
3177
+ const delta = json.choices?.[0]?.delta;
3178
+ if (delta?.content) {
3179
+ callbacks.onDelta?.(delta.content);
3180
+ }
3181
+ if (delta?.thinking?.content) {
3182
+ callbacks.onReasoning?.(delta.thinking.content);
3183
+ }
3184
+ if (delta?.reasoning_content) {
3185
+ callbacks.onReasoning?.(delta.reasoning_content);
3186
+ }
3187
+ }
3188
+ }
3189
+ function buildIterationBlocks(iteration, content, reasoning, blocks, callbacks) {
3190
+ const blockIdPrefix = `block_${iteration}_${Date.now()}`;
3191
+ if (reasoning && reasoning.trim()) {
3192
+ const thinkingBlock = {
3193
+ id: `${blockIdPrefix}_thinking`,
3194
+ type: "thinking",
3195
+ content: reasoning
3196
+ };
3197
+ blocks.push(thinkingBlock);
3198
+ callbacks.onBlock?.(thinkingBlock);
3199
+ }
3200
+ if (content && content.trim()) {
3201
+ const textBlock = {
3202
+ id: `${blockIdPrefix}_text`,
3203
+ type: "text",
3204
+ content
3205
+ };
3206
+ blocks.push(textBlock);
3207
+ callbacks.onBlock?.(textBlock);
3208
+ }
3209
+ }
3210
+ async function executeToolCalls(toolCalls, mcpTools, mcpService, blocks, callbacks, logger, builtinExecutor) {
3211
+ const toolResults = [];
3212
+ for (const toolCall of toolCalls) {
3213
+ const toolUseBlock = {
3214
+ id: `${toolCall.id}_use`,
3215
+ type: "tool_use",
3216
+ toolId: toolCall.id,
3217
+ toolName: toolCall.name,
3218
+ input: toolCall.args,
3219
+ status: "running"
3220
+ };
3221
+ blocks.push(toolUseBlock);
3222
+ callbacks.onBlock?.(toolUseBlock);
3223
+ try {
3224
+ logger.info("Executing tool", { toolName: toolCall.name });
3225
+ const mcpTool = mcpTools.find((t) => t.id === toolCall.name);
3226
+ if (!mcpTool) {
3227
+ logger.error("Tool not found", void 0, { toolName: toolCall.name });
3228
+ continue;
3229
+ }
3230
+ logger.debug("Tool info", {
3231
+ serverId: mcpTool.serverId,
3232
+ toolName: mcpTool.name,
3233
+ args: toolCall.args
3234
+ });
3235
+ const mappedArgs = mapToolArguments(toolCall.args, mcpTool, logger);
3236
+ let result;
3237
+ if (mcpTool.serverId === "builtin" && builtinExecutor) {
3238
+ result = await builtinExecutor.execute(mcpTool.name, mappedArgs);
3239
+ } else if (mcpService) {
3240
+ result = await mcpService.callTool(mcpTool.serverId, mcpTool.name, mappedArgs, toolCall.id);
3241
+ } else {
3242
+ result = { isError: true, content: [{ type: "text", text: "MCP service not available" }] };
3243
+ }
3244
+ logger.info("Tool result received", { result });
3245
+ callbacks.onToolResult?.(toolCall.id, result);
3246
+ const resultText = result.content?.[0]?.text || JSON.stringify(result);
3247
+ const toolResultBlock = {
3248
+ id: `${toolCall.id}_result`,
3249
+ type: "tool_result",
3250
+ toolId: toolCall.id,
3251
+ toolName: toolCall.name,
3252
+ output: resultText,
3253
+ isError: result.isError
3254
+ };
3255
+ blocks.push(toolResultBlock);
3256
+ callbacks.onBlock?.(toolResultBlock);
3257
+ const toolUseBlockRef = blocks.find((b) => b.id === `${toolCall.id}_use`);
3258
+ if (toolUseBlockRef && toolUseBlockRef.type === "tool_use") {
3259
+ toolUseBlockRef.status = result.isError ? "error" : "completed";
3260
+ callbacks.onBlock?.({ ...toolUseBlockRef });
3261
+ }
3262
+ toolResults.push({ toolCall, result });
3263
+ } catch (error) {
3264
+ logger.error("Tool execution error", error instanceof Error ? error : void 0);
3265
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
3266
+ const errorResult = {
3267
+ isError: true,
3268
+ content: [{ type: "text", text: `Error: ${errorMessage}` }]
3269
+ };
3270
+ callbacks.onToolResult?.(toolCall.id, errorResult);
3271
+ const errorToolResultBlock = {
3272
+ id: `${toolCall.id}_result`,
3273
+ type: "tool_result",
3274
+ toolId: toolCall.id,
3275
+ toolName: toolCall.name,
3276
+ error: errorMessage,
3277
+ isError: true
3278
+ };
3279
+ blocks.push(errorToolResultBlock);
3280
+ callbacks.onBlock?.(errorToolResultBlock);
3281
+ const errorToolUseBlockRef = blocks.find((b) => b.id === `${toolCall.id}_use`);
3282
+ if (errorToolUseBlockRef && errorToolUseBlockRef.type === "tool_use") {
3283
+ errorToolUseBlockRef.status = "error";
3284
+ callbacks.onBlock?.({ ...errorToolUseBlockRef });
3285
+ }
3286
+ toolResults.push({ toolCall, result: errorResult });
3287
+ }
3288
+ }
3289
+ return toolResults;
3290
+ }
3291
+ function mapToolArguments(args, mcpTool, logger) {
3292
+ const mappedArgs = { ...args };
3293
+ const toolSchema = mcpTool.inputSchema;
3294
+ if (toolSchema?.properties) {
3295
+ const expectedParams = Object.keys(toolSchema.properties);
3296
+ const paramMappings = {
3297
+ "search_query": ["query", "q", "searchQuery", "search"],
3298
+ "content": ["text", "body", "message"],
3299
+ "file_path": ["path", "filePath", "file"],
3300
+ "url": ["link", "uri"]
3301
+ };
3302
+ for (const expectedParam of expectedParams) {
3303
+ if (!(expectedParam in mappedArgs)) {
3304
+ const aliases = paramMappings[expectedParam];
3305
+ if (aliases) {
3306
+ for (const alias of aliases) {
3307
+ if (alias in mappedArgs) {
3308
+ logger.info("Mapping parameter", { from: alias, to: expectedParam });
3309
+ mappedArgs[expectedParam] = mappedArgs[alias];
3310
+ delete mappedArgs[alias];
3311
+ break;
3312
+ }
3313
+ }
3314
+ }
3315
+ }
3316
+ }
3317
+ }
3318
+ return mappedArgs;
3319
+ }
3320
+
3321
+ // src/completion/ToolHandler.ts
3322
+ async function streamWithTools(options, callbacks, mcpService, llmConfig, getProvider, resolveApiKey2, logger, builtinExecutor) {
3323
+ try {
3324
+ const routedInfo = await llmConfig.resolveRoutedModel(
3325
+ options.providerId,
3326
+ options.model
3327
+ );
3328
+ const actualProviderId = routedInfo?.actualProviderId || options.providerId;
3329
+ const actualModel = routedInfo?.actualModelId || options.model;
3330
+ const provider = await getProvider(actualProviderId);
3331
+ if (!provider) {
3332
+ callbacks.onError?.(`Provider not found: ${actualProviderId}`);
3333
+ return;
3334
+ }
3335
+ if (!provider.enabled) {
3336
+ callbacks.onError?.(`Provider is disabled: ${provider.name}`);
3337
+ return;
3338
+ }
3339
+ const { apiKey: effectiveKey } = resolveProviderEndpoint(provider);
3340
+ const apiKey = resolveApiKey2(effectiveKey);
3341
+ if (!apiKey) {
3342
+ callbacks.onError?.("API key not configured");
3343
+ return;
3344
+ }
3345
+ const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`;
3346
+ callbacks.onStart?.(messageId);
3347
+ const globalParams = await llmConfig.getGlobalModelParameters();
3348
+ const MAX_ITERATIONS = globalParams.toolMaxTurns ?? 100;
3349
+ let iteration = 0;
3350
+ const conversationMessages = [...options.messages];
3351
+ let finalContent = "";
3352
+ let finalReasoning = "";
3353
+ let finalUsageTokens;
3354
+ const blocks = [];
3355
+ logger.info("Sending request to LLM with tools", {
3356
+ model: actualModel,
3357
+ providerId: actualProviderId,
3358
+ messagesCount: options.messages.length
3359
+ });
3360
+ logToolFormat(options.tools, logger);
3361
+ const apiFormat = resolveApiFormat(provider);
3362
+ logger.info("API format determined", { apiFormat });
3363
+ while (iteration < MAX_ITERATIONS) {
3364
+ iteration++;
3365
+ logger.info("Agentic loop iteration", {
3366
+ iteration,
3367
+ maxIterations: MAX_ITERATIONS,
3368
+ messagesCount: conversationMessages.length
3369
+ });
3370
+ const { requestBody, url } = buildToolRequest(
3371
+ apiFormat,
3372
+ conversationMessages,
3373
+ actualModel,
3374
+ options,
3375
+ provider
3376
+ );
3377
+ if (options.nativeSearchAugmentation) {
3378
+ applyAugmentation(requestBody, options.nativeSearchAugmentation);
3379
+ }
3380
+ const headers = getProviderHeaders(provider, apiKey);
3381
+ logger.info("Sending tool call request", {
3382
+ url,
3383
+ headers,
3384
+ requestBody
3385
+ });
3386
+ const response = await fetch(url, {
3387
+ method: "POST",
3388
+ headers,
3389
+ body: JSON.stringify(requestBody)
3390
+ });
3391
+ if (!response.ok) {
3392
+ const errorText = await response.text();
3393
+ callbacks.onError?.(`API error (${response.status}): ${errorText}`);
3394
+ return;
3395
+ }
3396
+ const reader = response.body?.getReader();
3397
+ if (!reader) {
3398
+ callbacks.onError?.("No response body");
3399
+ return;
3400
+ }
3401
+ const decoder = new TextDecoder();
3402
+ let content = "";
3403
+ let reasoning = "";
3404
+ const toolCalls = [];
3405
+ const pendingOpenAIToolCalls = /* @__PURE__ */ new Map();
3406
+ let buffer = "";
3407
+ try {
3408
+ while (true) {
3409
+ const { done, value } = await reader.read();
3410
+ if (done) break;
3411
+ const chunk = decoder.decode(value, { stream: true });
3412
+ logger.debug("Received raw chunk", { chunkLength: chunk.length });
3413
+ buffer += chunk;
3414
+ const lines = buffer.split("\n");
3415
+ buffer = lines.pop() || "";
3416
+ for (const line of lines) {
3417
+ const trimmedLine = line.trim();
3418
+ if (!trimmedLine) continue;
3419
+ logger.debug("Processing line", { linePreview: trimmedLine.substring(0, 200) });
3420
+ if (trimmedLine.startsWith("data: ")) {
3421
+ const data = trimmedLine.slice(6);
3422
+ if (data === "[DONE]") {
3423
+ logger.debug("Stream done");
3424
+ continue;
3425
+ }
3426
+ try {
3427
+ const json = JSON.parse(data);
3428
+ logger.debug("Parsed JSON", { json });
3429
+ if (json.error) {
3430
+ logger.error("API Error", void 0, { error: json.error });
3431
+ callbacks.onError?.(`API Error: ${json.error.message || JSON.stringify(json.error)}`);
3432
+ return;
3433
+ }
3434
+ parseStreamChunk(json, apiFormat, callbacks, logger);
3435
+ content = content + extractDeltaContent(json, apiFormat);
3436
+ reasoning = reasoning + extractDeltaReasoning(json, apiFormat);
3437
+ extractToolCalls(json, apiFormat, toolCalls, callbacks, logger, pendingOpenAIToolCalls);
3438
+ if (apiFormat !== "google" && apiFormat !== "anthropic" && json.usage) {
3439
+ finalUsageTokens = {
3440
+ promptTokens: json.usage.prompt_tokens || 0,
3441
+ completionTokens: json.usage.completion_tokens || 0,
3442
+ totalTokens: json.usage.total_tokens || 0
3443
+ };
3444
+ }
3445
+ } catch (_e) {
3446
+ }
3447
+ }
3448
+ }
3449
+ }
3450
+ if (apiFormat !== "google" && apiFormat !== "anthropic") {
3451
+ finalizeOpenAIToolCalls(pendingOpenAIToolCalls, toolCalls, callbacks, logger);
3452
+ pendingOpenAIToolCalls.clear();
3453
+ }
3454
+ finalContent = content;
3455
+ finalReasoning = reasoning;
3456
+ buildIterationBlocks(iteration, content, reasoning, blocks, callbacks);
3457
+ const executableToolCalls = toolCalls.filter(
3458
+ (tc) => !NATIVE_SEARCH_TOOL_NAMES.includes(tc.name)
3459
+ );
3460
+ logger.info("Tool execution check", {
3461
+ toolCallsLength: toolCalls.length,
3462
+ executableToolCallsLength: executableToolCalls.length,
3463
+ hasMcpTools: !!options.mcpTools,
3464
+ mcpToolsLength: options.mcpTools?.length,
3465
+ hasMcpService: !!mcpService
3466
+ });
3467
+ if (executableToolCalls.length > 0 && options.mcpTools && (mcpService || builtinExecutor)) {
3468
+ logger.info("Executing tool calls", { count: executableToolCalls.length });
3469
+ conversationMessages.push({
3470
+ id: `assistant_${iteration}`,
3471
+ role: "assistant",
3472
+ content: content || "Calling tools...",
3473
+ timestamp: Date.now()
3474
+ });
3475
+ const toolResults = await executeToolCalls(
3476
+ executableToolCalls,
3477
+ options.mcpTools,
3478
+ mcpService,
3479
+ blocks,
3480
+ callbacks,
3481
+ logger,
3482
+ builtinExecutor
3483
+ );
3484
+ const toolResultsText = toolResults.map(({ toolCall, result }) => {
3485
+ const resultText = typeof result === "string" ? result : result.content?.[0]?.text || JSON.stringify(result);
3486
+ return `Tool ${toolCall.name} result:
3487
+ ${resultText}`;
3488
+ }).join("\n\n");
3489
+ conversationMessages.push({
3490
+ id: `tool_results_${iteration}`,
3491
+ role: "user",
3492
+ content: toolResultsText,
3493
+ timestamp: Date.now()
3494
+ });
3495
+ logger.info("Continuing to next iteration with tool results");
3496
+ continue;
3497
+ } else {
3498
+ logger.info("No tool calls, finishing");
3499
+ break;
3500
+ }
3501
+ } catch (streamError) {
3502
+ logger.error("Stream processing error", streamError instanceof Error ? streamError : void 0);
3503
+ callbacks.onError?.(`Stream error: ${streamError instanceof Error ? streamError.message : "Unknown error"}`);
3504
+ return;
3505
+ }
3506
+ }
3507
+ logger.info("Sending final callback", { blocksCount: blocks.length });
3508
+ callbacks.onDone?.({
3509
+ id: messageId,
3510
+ role: "assistant",
3511
+ content: finalContent,
3512
+ timestamp: Date.now(),
3513
+ thinking: finalReasoning ? { content: finalReasoning } : void 0,
3514
+ blocks: blocks.length > 0 ? blocks : void 0
3515
+ }, finalUsageTokens);
3516
+ } catch (error) {
3517
+ logger.error("Error in streamWithTools", error instanceof Error ? error : void 0);
3518
+ const message = error instanceof Error ? error.message : "Unknown error";
3519
+ callbacks.onError?.(message);
3520
+ }
3521
+ }
3522
+
3523
+ // src/completion/TransformerHandler.ts
3524
+ function readUsageFromOpenAIResponse(usage) {
3525
+ if (!usage) return null;
3526
+ const promptTokens = Number(usage.prompt_tokens) || 0;
3527
+ const completionTokens = Number(usage.completion_tokens) || 0;
3528
+ const promptDetails = usage.prompt_tokens_details ?? {};
3529
+ const cachedTokens = Number(promptDetails.cached_tokens) || Number(usage.cache_read_input_tokens) || 0;
3530
+ const cacheCreation = Number(usage.cache_creation_input_tokens) || 0;
3531
+ const inputTokens = Math.max(0, promptTokens - cachedTokens - cacheCreation);
3532
+ const reasoningDetails = usage.completion_tokens_details ?? usage.output_tokens_details ?? {};
3533
+ const reasoningTokens = Number(reasoningDetails.reasoning_tokens) || 0;
3534
+ return {
3535
+ inputTokens,
3536
+ outputTokens: completionTokens,
3537
+ cacheReadTokens: cachedTokens,
3538
+ cacheCreationTokens: cacheCreation,
3539
+ reasoningTokens
3540
+ };
3541
+ }
3542
+ var sharedExecutor2 = null;
3543
+ function getSharedExecutor2() {
3544
+ if (!sharedExecutor2) {
3545
+ sharedExecutor2 = new TransformerChainExecutor();
3546
+ }
3547
+ return sharedExecutor2;
3548
+ }
3549
+ async function resolveChainWithMain(llmConfig, providerId, model) {
3550
+ return resolveProviderChain(llmConfig, providerId, model);
3551
+ }
3552
+ async function completeWithTransformers(options, llmConfig, getProvider, resolveApiKey2, completeFallback, logger, recording) {
3553
+ try {
3554
+ const routedInfo = await llmConfig.resolveRoutedModel(
3555
+ options.providerId,
3556
+ options.model
3557
+ );
3558
+ const actualProviderId = routedInfo?.actualProviderId || options.providerId;
3559
+ const actualModel = routedInfo?.actualModelId || options.model;
3560
+ const provider = await getProvider(actualProviderId);
3561
+ if (!provider) {
3562
+ return { success: false, error: `Provider not found: ${actualProviderId}` };
3563
+ }
3564
+ if (!provider.enabled) {
3565
+ return { success: false, error: `Provider is disabled: ${provider.name}` };
3566
+ }
3567
+ const { apiKey: effectiveKey } = resolveProviderEndpoint(provider);
3568
+ const apiKey = resolveApiKey2(effectiveKey);
3569
+ if (!apiKey) {
3570
+ return { success: false, error: "API key not configured" };
3571
+ }
3572
+ const { chain, hasTransformers } = await resolveChainWithMain(
3573
+ llmConfig,
3574
+ actualProviderId,
3575
+ actualModel
3576
+ );
3577
+ if (!hasTransformers) {
3578
+ return completeFallback(options);
3579
+ }
3580
+ const unifiedRequest = {
3581
+ model: actualModel,
3582
+ messages: options.messages.map((m) => ({
3583
+ role: m.role,
3584
+ content: m.content
3585
+ })),
3586
+ max_tokens: options.maxTokens || 4096,
3587
+ temperature: options.temperature,
3588
+ stream: options.stream ?? false
3589
+ };
3590
+ const transformerProvider = {
3591
+ name: provider.name,
3592
+ baseUrl: provider.api_base_url,
3593
+ apiKey,
3594
+ models: provider.models || []
3595
+ };
3596
+ const executor = getSharedExecutor2();
3597
+ const { response } = await executeProviderCall({
3598
+ executor,
3599
+ request: unifiedRequest,
3600
+ provider: transformerProvider,
3601
+ chain,
3602
+ endpointTransformer: void 0,
3603
+ extendedContext: options.useExtendedContext ? { enabled: true, model: actualModel } : void 0,
3604
+ resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : buildProviderApiUrl(provider, { model: actualModel, stream: false }),
3605
+ buildHeaders: (config) => ({
3606
+ ...getProviderHeaders(provider, apiKey),
3607
+ ...config.headers,
3608
+ ...isOpenRouterProvider(provider) ? OPENROUTER_APP_HEADERS : {}
3609
+ }),
3610
+ // Add OpenRouter provider routing config if applicable
3611
+ prepareBody: (requestBody) => addOpenRouterProviderToRequest(
3612
+ requestBody,
3613
+ provider,
3614
+ actualModel
3615
+ ),
3616
+ fetchFn: (url, headers, body) => {
3617
+ logger.info("Calling completion with transformers", { url });
3618
+ return fetch(url, {
3619
+ method: "POST",
3620
+ headers,
3621
+ body: JSON.stringify(body)
3622
+ });
3623
+ }
3624
+ });
3625
+ if (!response.ok) {
3626
+ const errorText = await response.text();
3627
+ return {
3628
+ success: false,
3629
+ error: `API error (${response.status}): ${errorText}`
3630
+ };
3631
+ }
3632
+ const transformedResponse = await executor.executeResponseChain(
3633
+ unifiedRequest,
3634
+ response,
3635
+ transformerProvider,
3636
+ chain,
3637
+ { endpointTransformer: void 0 }
3638
+ );
3639
+ const data = await transformedResponse.json();
3640
+ const choice = data.choices?.[0];
3641
+ if (!choice) {
3642
+ return { success: false, error: "No choices in response" };
3643
+ }
3644
+ const rawToolCalls = choice.message?.tool_calls;
3645
+ const toolCalls = rawToolCalls?.map((tc) => ({
3646
+ id: tc.id,
3647
+ name: tc.function.name,
3648
+ args: typeof tc.function.arguments === "string" ? JSON.parse(tc.function.arguments) : tc.function.arguments
3649
+ }));
3650
+ const tapped = readUsageFromOpenAIResponse(data.usage);
3651
+ if (recording?.recorder && tapped) {
3652
+ recording.recorder.record({
3653
+ messageId: options.messageId ?? null,
3654
+ parentMessageId: options.parentMessageId ?? null,
3655
+ sessionId: options.sessionId ?? null,
3656
+ providerId: actualProviderId,
3657
+ model: actualModel,
3658
+ apiKeyId: recording.apiKeyId ?? null,
3659
+ engineOrigin: "completion",
3660
+ usage: tapped,
3661
+ rawUsage: data.usage
3662
+ });
3663
+ }
3664
+ return {
3665
+ success: true,
3666
+ message: {
3667
+ id: `msg_${Date.now()}`,
3668
+ role: "assistant",
3669
+ content: choice.message?.content || "",
3670
+ timestamp: Date.now(),
3671
+ thinking: data.reasoning_content || choice.message?.thinking?.content ? { content: data.reasoning_content || choice.message?.thinking?.content || "", signature: choice.message?.thinking?.signature } : void 0,
3672
+ toolCalls
3673
+ },
3674
+ usage: data.usage ? {
3675
+ promptTokens: data.usage.prompt_tokens || 0,
3676
+ completionTokens: data.usage.completion_tokens || 0,
3677
+ totalTokens: data.usage.total_tokens || 0
3678
+ } : void 0,
3679
+ finishReason: choice.finish_reason
3680
+ };
3681
+ } catch (error) {
3682
+ const message = error instanceof Error ? error.message : "Unknown error";
3683
+ return { success: false, error: message };
3684
+ }
3685
+ }
3686
+ async function completeStreamWithTransformers(options, callbacks, llmConfig, getProvider, resolveApiKey2, completeStreamFallback, logger, recording) {
3687
+ try {
3688
+ const routedInfo = await llmConfig.resolveRoutedModel(
3689
+ options.providerId,
3690
+ options.model
3691
+ );
3692
+ const actualProviderId = routedInfo?.actualProviderId || options.providerId;
3693
+ const actualModel = routedInfo?.actualModelId || options.model;
3694
+ const provider = await getProvider(actualProviderId);
3695
+ if (!provider) {
3696
+ callbacks.onError?.(`Provider not found: ${actualProviderId}`);
3697
+ return;
3698
+ }
3699
+ if (!provider.enabled) {
3700
+ callbacks.onError?.(`Provider is disabled: ${provider.name}`);
3701
+ return;
3702
+ }
3703
+ const { apiKey: effectiveKey } = resolveProviderEndpoint(provider);
3704
+ const apiKey = resolveApiKey2(effectiveKey);
3705
+ if (!apiKey) {
3706
+ callbacks.onError?.("API key not configured");
3707
+ return;
3708
+ }
3709
+ const { chain, hasTransformers } = await resolveChainWithMain(
3710
+ llmConfig,
3711
+ actualProviderId,
3712
+ actualModel
3713
+ );
3714
+ if (!hasTransformers) {
3715
+ return completeStreamFallback(options, callbacks);
3716
+ }
3717
+ const messageId = `msg_${Date.now()}`;
3718
+ callbacks.onStart?.(messageId);
3719
+ const unifiedRequest = {
3720
+ model: actualModel,
3721
+ messages: options.messages.map((m) => ({
3722
+ role: m.role,
3723
+ content: m.content
3724
+ })),
3725
+ max_tokens: options.maxTokens || 4096,
3726
+ temperature: options.temperature,
3727
+ stream: true
3728
+ };
3729
+ const transformerProvider = {
3730
+ name: provider.name,
3731
+ baseUrl: provider.api_base_url,
3732
+ apiKey,
3733
+ models: provider.models || []
3734
+ };
3735
+ const executor = getSharedExecutor2();
3736
+ const { response } = await executeProviderCall({
3737
+ executor,
3738
+ request: unifiedRequest,
3739
+ provider: transformerProvider,
3740
+ chain,
3741
+ endpointTransformer: void 0,
3742
+ extendedContext: options.useExtendedContext ? { enabled: true, model: actualModel } : void 0,
3743
+ resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : buildProviderApiUrl(provider, { model: actualModel, stream: true }),
3744
+ buildHeaders: (config) => ({
3745
+ ...getProviderHeaders(provider, apiKey),
3746
+ ...config.headers,
3747
+ ...isOpenRouterProvider(provider) ? OPENROUTER_APP_HEADERS : {}
3748
+ }),
3749
+ // Add OpenRouter provider routing config if applicable
3750
+ prepareBody: (requestBody) => addOpenRouterProviderToRequest(
3751
+ requestBody,
3752
+ provider,
3753
+ actualModel
3754
+ ),
3755
+ fetchFn: (url, headers, body) => {
3756
+ logger.info("Streaming completion with transformers", { url });
3757
+ return fetch(url, {
3758
+ method: "POST",
3759
+ headers,
3760
+ body: JSON.stringify(body)
3761
+ });
3762
+ }
3763
+ });
3764
+ if (!response.ok) {
3765
+ const errorText = await response.text();
3766
+ callbacks.onError?.(`API error (${response.status}): ${errorText}`);
3767
+ return;
3768
+ }
3769
+ const transformedResponse = await executor.executeResponseChain(
3770
+ unifiedRequest,
3771
+ response,
3772
+ transformerProvider,
3773
+ chain,
3774
+ { endpointTransformer: void 0 }
3775
+ );
3776
+ const reader = transformedResponse.body?.getReader();
3777
+ if (!reader) {
3778
+ callbacks.onError?.("No response body");
3779
+ return;
3780
+ }
3781
+ const decoder = new TextDecoder();
3782
+ let content = "";
3783
+ let reasoning = "";
3784
+ try {
3785
+ while (true) {
3786
+ const { done, value } = await reader.read();
3787
+ if (done) break;
3788
+ const chunk = decoder.decode(value, { stream: true });
3789
+ const lines = chunk.split("\n").filter((line) => line.trim() !== "");
3790
+ for (const line of lines) {
3791
+ if (line.startsWith("data: ")) {
3792
+ const data = line.slice(6);
3793
+ if (data === "[DONE]") continue;
3794
+ try {
3795
+ const json = JSON.parse(data);
3796
+ const delta = json.choices?.[0]?.delta;
3797
+ if (delta?.content) {
3798
+ content += delta.content;
3799
+ callbacks.onDelta?.(delta.content);
3800
+ }
3801
+ if (delta?.thinking?.content) {
3802
+ reasoning += delta.thinking.content;
3803
+ callbacks.onReasoning?.(delta.thinking.content);
3804
+ }
3805
+ if (delta?.reasoning_content) {
3806
+ reasoning += delta.reasoning_content;
3807
+ callbacks.onReasoning?.(delta.reasoning_content);
3808
+ }
3809
+ if (json.usage) {
3810
+ const tapped = readUsageFromOpenAIResponse(json.usage);
3811
+ if (recording?.recorder && tapped) {
3812
+ recording.recorder.record({
3813
+ messageId: options.messageId ?? null,
3814
+ parentMessageId: options.parentMessageId ?? null,
3815
+ sessionId: options.sessionId ?? null,
3816
+ providerId: actualProviderId,
3817
+ model: actualModel,
3818
+ apiKeyId: recording.apiKeyId ?? null,
3819
+ engineOrigin: "completion",
3820
+ usage: tapped,
3821
+ rawUsage: json.usage
3822
+ });
3823
+ }
3824
+ callbacks.onDone?.(
3825
+ {
3826
+ id: messageId,
3827
+ role: "assistant",
3828
+ content,
3829
+ timestamp: Date.now(),
3830
+ thinking: reasoning ? { content: reasoning } : void 0
3831
+ },
3832
+ {
3833
+ promptTokens: json.usage.prompt_tokens || 0,
3834
+ completionTokens: json.usage.completion_tokens || 0,
3835
+ totalTokens: json.usage.total_tokens || 0
3836
+ }
3837
+ );
3838
+ return;
3839
+ }
3840
+ } catch (_e) {
3841
+ }
3842
+ }
3843
+ }
3844
+ }
3845
+ callbacks.onDone?.({
3846
+ id: messageId,
3847
+ role: "assistant",
3848
+ content,
3849
+ timestamp: Date.now(),
3850
+ thinking: reasoning ? { content: reasoning } : void 0
3851
+ });
3852
+ } finally {
3853
+ reader.releaseLock();
3854
+ }
3855
+ } catch (error) {
3856
+ const message = error instanceof Error ? error.message : "Unknown error";
3857
+ callbacks.onError?.(message);
3858
+ }
3859
+ }
3860
+
3861
+ // src/completion/CompletionService.ts
3862
+ var CompletionService = class {
3863
+ constructor(paths, llmConfig, logger) {
3864
+ this.paths = paths;
3865
+ this.llmConfig = llmConfig;
3866
+ this.logger = logger;
3867
+ }
3868
+ paths;
3869
+ llmConfig;
3870
+ logger;
3871
+ apiKeyPool = null;
3872
+ usageRecorder = null;
3873
+ usageEventSink = null;
3874
+ visionFallbackProvider = null;
3875
+ /**
3876
+ * Set the API key pool service for multi-key load balancing.
3877
+ * When set, keys are resolved via the pool instead of directly from the provider.
3878
+ */
3879
+ setApiKeyPool(pool) {
3880
+ this.apiKeyPool = pool;
3881
+ }
3882
+ /**
3883
+ * Set the usage recorder so completion paths can persist token/cost stats.
3884
+ * Optional — when unset, all calls succeed but nothing is recorded.
3885
+ */
3886
+ setUsageRecorder(recorder) {
3887
+ this.usageRecorder = recorder;
3888
+ }
3889
+ /**
3890
+ * Set the usage-event sink so completion paths can push live usage events
3891
+ * (context-meter, aggregate recorder) into the in-process hub. Injected DOWN
3892
+ * at bootstrap with `getUsageEventHub()` immediately after construction, so
3893
+ * emission is unconditional in production. Optional — when unset (unit-test
3894
+ * constructors), the emit calls no-op, identical to `usageRecorder`.
3895
+ */
3896
+ setUsageEventSink(sink) {
3897
+ this.usageEventSink = sink;
3898
+ }
3899
+ /**
3900
+ * Set the vision-fallback provider used by `applyVisionFallback` to describe
3901
+ * images for non-vision models. Injected DOWN by the host at bootstrap
3902
+ * (the host's impl is built on top of CompletionService).
3903
+ * Optional — when unset, `applyVisionFallback` strips images instead.
3904
+ */
3905
+ setVisionFallbackProvider(provider) {
3906
+ this.visionFallbackProvider = provider;
3907
+ }
3908
+ /**
3909
+ * Get provider by ID
3910
+ * Delegates to the `ProviderConfigSource` which maintains its own in-memory cache.
3911
+ */
3912
+ async getProvider(providerId) {
3913
+ return this.llmConfig.getProvider(providerId);
3914
+ }
3915
+ /**
3916
+ * Send a completion request
3917
+ */
3918
+ async complete(options) {
3919
+ try {
3920
+ const routedInfo = await this.llmConfig.resolveRoutedModel(
3921
+ options.providerId,
3922
+ options.model
3923
+ );
3924
+ const actualProviderId = routedInfo?.actualProviderId || options.providerId;
3925
+ const actualModel = routedInfo?.actualModelId || options.model;
3926
+ const provider = await this.getProvider(actualProviderId);
3927
+ if (!provider) {
3928
+ return { success: false, error: `Provider not found: ${actualProviderId}` };
3929
+ }
3930
+ if (!provider.enabled) {
3931
+ return { success: false, error: `Provider is disabled: ${provider.name}` };
3932
+ }
3933
+ const apiKey = await this.resolveApiKeyForRequest(provider, actualProviderId, options.sessionId);
3934
+ if (!apiKey) {
3935
+ return { success: false, error: "API key not configured" };
3936
+ }
3937
+ const apiFormat = resolveApiFormat(provider);
3938
+ this.logger.info("Using API format for completion", { apiFormat, providerId: actualProviderId });
3939
+ const resolvedOptions = { ...options, model: actualModel };
3940
+ let result = await this.callDirectHandler(apiFormat, provider, apiKey, resolvedOptions);
3941
+ if (!result.success && result.error && this.apiKeyPool && options.sessionId) {
3942
+ const status = this.extractHttpStatus(result.error);
3943
+ if (status && (status === 429 || status === 529 || status === 401 || status === 403)) {
3944
+ const newKey = await this.apiKeyPool.reportError(actualProviderId, options.sessionId, status);
3945
+ if (newKey) {
3946
+ this.logger.info("Retrying completion with new API key", {
3947
+ providerId: actualProviderId,
3948
+ statusCode: status
3949
+ });
3950
+ result = await this.callDirectHandler(apiFormat, provider, newKey, resolvedOptions);
3951
+ }
3952
+ }
3953
+ }
3954
+ if (result.success && this.apiKeyPool && options.sessionId) {
3955
+ this.apiKeyPool.reportSuccess(options.sessionId);
3956
+ }
3957
+ if (result.success && result.usage && options.sessionId) {
3958
+ this.usageEventSink?.emit({
3959
+ sessionId: options.sessionId,
3960
+ modelId: actualModel,
3961
+ usage: result.usage,
3962
+ engineOrigin: "completion"
3963
+ });
3964
+ }
3965
+ return result;
3966
+ } catch (error) {
3967
+ const message = error instanceof Error ? error.message : "Unknown error";
3968
+ return { success: false, error: message };
3969
+ }
3970
+ }
3971
+ /**
3972
+ * Send a streaming completion request
3973
+ */
3974
+ async completeStream(options, callbacks) {
3975
+ try {
3976
+ await this.applyVisionFallback(options);
3977
+ this.logger.info("Starting stream completion", {
3978
+ providerId: options.providerId,
3979
+ model: options.model,
3980
+ messagesCount: options.messages.length
3981
+ });
3982
+ const routedInfo = await this.llmConfig.resolveRoutedModel(
3983
+ options.providerId,
3984
+ options.model
3985
+ );
3986
+ this.logger.debug("Resolved routed model", { routedInfo });
3987
+ const actualProviderId = routedInfo?.actualProviderId || options.providerId;
3988
+ const actualModel = routedInfo?.actualModelId || options.model;
3989
+ this.logger.debug("Resolved provider and model", { actualProviderId, actualModel });
3990
+ const provider = await this.getProvider(actualProviderId);
3991
+ this.logger.debug("Retrieved provider", provider ? {
3992
+ id: provider.id,
3993
+ name: provider.name,
3994
+ apiType: provider.apiType,
3995
+ apiFormat: provider.apiFormat,
3996
+ api_base_url: provider.api_base_url,
3997
+ enabled: provider.enabled
3998
+ } : { error: "Provider not found" });
3999
+ if (!provider) {
4000
+ callbacks.onError?.(`Provider not found: ${actualProviderId}`);
4001
+ return;
4002
+ }
4003
+ if (!provider.enabled) {
4004
+ callbacks.onError?.(`Provider is disabled: ${provider.name}`);
4005
+ return;
4006
+ }
4007
+ const apiKey = await this.resolveApiKeyForRequest(provider, actualProviderId, options.sessionId);
4008
+ if (!apiKey) {
4009
+ callbacks.onError?.("API key not configured");
4010
+ return;
4011
+ }
4012
+ const apiFormat = resolveApiFormat(provider);
4013
+ this.logger.info("Using API format for stream", { apiFormat });
4014
+ const messageId = `msg_${Date.now()}`;
4015
+ callbacks.onStart?.(messageId);
4016
+ const resolvedOptions = { ...options, model: actualModel };
4017
+ if (options.sessionId) {
4018
+ const sid = options.sessionId;
4019
+ const userOnDone = callbacks.onDone;
4020
+ callbacks = {
4021
+ ...callbacks,
4022
+ onDone: (message, usage, metrics) => {
4023
+ if (usage) {
4024
+ this.usageEventSink?.emit({
4025
+ sessionId: sid,
4026
+ modelId: actualModel,
4027
+ usage,
4028
+ engineOrigin: "completion"
4029
+ });
4030
+ }
4031
+ userOnDone?.(message, usage, metrics);
4032
+ }
4033
+ };
4034
+ }
4035
+ if (this.apiKeyPool && options.sessionId) {
4036
+ const retryState = { error: null };
4037
+ const interceptCallbacks = {
4038
+ ...callbacks,
4039
+ onStart: void 0,
4040
+ // already called above
4041
+ onError: (error) => {
4042
+ const status = this.extractHttpStatus(error);
4043
+ if (status && (status === 429 || status === 529 || status === 401 || status === 403)) {
4044
+ retryState.error = { status, message: error };
4045
+ return;
4046
+ }
4047
+ callbacks.onError?.(error);
4048
+ },
4049
+ onDone: (message, usage, metrics) => {
4050
+ this.apiKeyPool.reportSuccess(options.sessionId);
4051
+ callbacks.onDone?.(message, usage, metrics);
4052
+ }
4053
+ };
4054
+ await this.callStreamHandler(apiFormat, provider, apiKey, resolvedOptions, messageId, interceptCallbacks);
4055
+ if (retryState.error) {
4056
+ const newKey = await this.apiKeyPool.reportError(
4057
+ actualProviderId,
4058
+ options.sessionId,
4059
+ retryState.error.status
4060
+ );
4061
+ if (newKey) {
4062
+ this.logger.info("Retrying stream with new API key", {
4063
+ providerId: actualProviderId,
4064
+ statusCode: retryState.error.status
4065
+ });
4066
+ await this.callStreamHandler(apiFormat, provider, newKey, resolvedOptions, messageId, {
4067
+ ...callbacks,
4068
+ onStart: void 0
4069
+ // don't fire onStart again
4070
+ });
4071
+ } else {
4072
+ callbacks.onError?.(retryState.error.message);
4073
+ }
4074
+ }
4075
+ } else {
4076
+ await this.callStreamHandler(apiFormat, provider, apiKey, resolvedOptions, messageId, {
4077
+ ...callbacks,
4078
+ onStart: void 0
4079
+ // already called above
4080
+ });
4081
+ }
4082
+ } catch (error) {
4083
+ const message = error instanceof Error ? error.message : "Unknown error";
4084
+ this.logger.error("Stream completion error", error instanceof Error ? error : void 0, { message });
4085
+ callbacks.onError?.(message);
4086
+ }
4087
+ }
4088
+ /**
4089
+ * Get available models for a provider
4090
+ */
4091
+ async getAvailableModels(providerId) {
4092
+ const provider = await this.getProvider(providerId);
4093
+ if (!provider) return [];
4094
+ return provider.models || [];
4095
+ }
4096
+ /**
4097
+ * Test provider connection with a specific model.
4098
+ * Sends "Hello" and returns the AI response, duration, etc.
4099
+ */
4100
+ async testModel(providerId, modelId) {
4101
+ try {
4102
+ const provider = await this.getProvider(providerId);
4103
+ if (!provider) {
4104
+ return { success: false, message: "Provider not found", model: modelId };
4105
+ }
4106
+ const apiKey = this.resolveApiKey(provider.api_key);
4107
+ if (!apiKey) {
4108
+ return { success: false, message: "API key not configured", model: modelId };
4109
+ }
4110
+ const testMessages = [
4111
+ { id: "test", role: "user", content: "Hello", timestamp: Date.now() }
4112
+ ];
4113
+ const startTime = Date.now();
4114
+ const result = await this.complete({
4115
+ providerId,
4116
+ model: modelId,
4117
+ messages: testMessages,
4118
+ maxTokens: 100
4119
+ });
4120
+ const durationMs = Date.now() - startTime;
4121
+ if (result.success) {
4122
+ return {
4123
+ success: true,
4124
+ message: "Connection successful",
4125
+ response: result.message?.content || "",
4126
+ model: modelId,
4127
+ durationMs
4128
+ };
4129
+ } else {
4130
+ return {
4131
+ success: false,
4132
+ message: result.error || "Unknown error",
4133
+ model: modelId,
4134
+ durationMs
4135
+ };
4136
+ }
4137
+ } catch (error) {
4138
+ const message = error instanceof Error ? error.message : "Unknown error";
4139
+ return { success: false, message, model: modelId };
4140
+ }
4141
+ }
4142
+ /**
4143
+ * Check if any messages contain images and the model lacks vision capability.
4144
+ * If so, use the auxiliary vision model to describe images as text and remove
4145
+ * the image attachments from the messages.
4146
+ *
4147
+ * Mutates options.messages in-place.
4148
+ */
4149
+ async applyVisionFallback(options) {
4150
+ const hasImages = options.messages.some((m) => m.images && m.images.length > 0);
4151
+ if (!hasImages) return;
4152
+ const hasVision = await this.llmConfig.hasVisionCapability(options.providerId, options.model);
4153
+ if (hasVision) return;
4154
+ const { vision: effectiveVisionModel } = await this.llmConfig.resolveEffectiveModels();
4155
+ if (!effectiveVisionModel || !this.visionFallbackProvider) {
4156
+ this.logger.info("Messages contain images but no vision auxiliary model configured; stripping images");
4157
+ for (const msg of options.messages) {
4158
+ if (msg.images && msg.images.length > 0) {
4159
+ msg.images = void 0;
4160
+ }
4161
+ }
4162
+ return;
4163
+ }
4164
+ this.logger.info("Model lacks vision capability, using auxiliary vision model for image descriptions", {
4165
+ providerId: options.providerId,
4166
+ model: options.model
4167
+ });
4168
+ const visionService = this.visionFallbackProvider;
4169
+ for (const msg of options.messages) {
4170
+ if (msg.images && msg.images.length > 0) {
4171
+ const imageDataUrls = msg.images.map((img) => ({ data: img.url }));
4172
+ const description = await visionService.describeImages(imageDataUrls, msg.content, effectiveVisionModel);
4173
+ if (description && description !== "[Image description unavailable]") {
4174
+ msg.content = `${msg.content}
4175
+
4176
+ [Image Description]
4177
+ ${description}`;
4178
+ }
4179
+ msg.images = void 0;
4180
+ }
4181
+ }
4182
+ }
4183
+ /**
4184
+ * Resolve API key for a request with priority:
4185
+ * 1. Coding Plan override (if enabled)
4186
+ * 2. API key pool (session-affinity weighted round-robin)
4187
+ * 3. Legacy single key from provider config
4188
+ */
4189
+ async resolveApiKeyForRequest(provider, providerId, sessionId) {
4190
+ if (provider.codingPlan?.enabled && provider.codingPlan.apiKey) {
4191
+ return this.resolveApiKey(provider.codingPlan.apiKey);
4192
+ }
4193
+ if (this.apiKeyPool) {
4194
+ const poolKey = sessionId ? await this.apiKeyPool.getKeyForSession(providerId, sessionId) : await this.apiKeyPool.getKey(providerId);
4195
+ if (poolKey) return poolKey;
4196
+ }
4197
+ const { apiKey: effectiveKey } = resolveProviderEndpoint(provider);
4198
+ return this.resolveApiKey(effectiveKey);
4199
+ }
4200
+ // ==========================================================================
4201
+ // Handler dispatch helpers
4202
+ // ==========================================================================
4203
+ /**
4204
+ * Extract HTTP status code from error messages like "API error (429): ..."
4205
+ */
4206
+ extractHttpStatus(error) {
4207
+ const match = error.match(/\((\d{3})\):/);
4208
+ return match ? parseInt(match[1], 10) : null;
4209
+ }
4210
+ /**
4211
+ * Dispatch a non-streaming completion to the appropriate handler.
4212
+ */
4213
+ async callDirectHandler(apiFormat, provider, apiKey, options) {
4214
+ switch (apiFormat) {
4215
+ case "anthropic":
4216
+ return callAnthropicCompletion(provider, apiKey, options, this.logger);
4217
+ case "google":
4218
+ return callGeminiCompletion(provider, apiKey, options, this.logger);
4219
+ case "openai-response":
4220
+ return callOpenAIResponseCompletion(provider, apiKey, options, this.logger);
4221
+ case "azure-openai":
4222
+ case "openai":
4223
+ default:
4224
+ return callOpenAICompletion(provider, apiKey, options, this.logger);
4225
+ }
4226
+ }
4227
+ /**
4228
+ * Dispatch a streaming completion to the appropriate handler.
4229
+ */
4230
+ async callStreamHandler(apiFormat, provider, apiKey, options, messageId, callbacks) {
4231
+ switch (apiFormat) {
4232
+ case "openai-response":
4233
+ await streamOpenAIResponseCompletion(provider, apiKey, options, messageId, callbacks, this.logger);
4234
+ return;
4235
+ case "anthropic":
4236
+ await streamAnthropicCompletion(provider, apiKey, options, messageId, callbacks, this.logger);
4237
+ return;
4238
+ case "google":
4239
+ await streamGeminiCompletion(provider, apiKey, options, messageId, callbacks, this.logger);
4240
+ return;
4241
+ case "azure-openai":
4242
+ case "openai":
4243
+ default:
4244
+ await streamOpenAICompletion(provider, apiKey, options, messageId, callbacks, this.logger);
4245
+ return;
4246
+ }
4247
+ }
4248
+ /**
4249
+ * Resolve API key (handle environment variable references)
4250
+ */
4251
+ resolveApiKey(apiKey) {
4252
+ if (!apiKey) return "";
4253
+ if (apiKey.startsWith("$")) {
4254
+ const envVar = apiKey.slice(1);
4255
+ return process.env[envVar] || "";
4256
+ }
4257
+ return apiKey;
4258
+ }
4259
+ /**
4260
+ * Resolve effective max_tokens value with priority:
4261
+ * 1. Session settings (if provided)
4262
+ * 2. Global model parameters (if enabled)
4263
+ * 3. Model's maxTokens from provider config
4264
+ * 4. Discovered models cache (from API)
4265
+ * 5. undefined - let API use its default
4266
+ */
4267
+ async resolveEffectiveMaxTokens(providerId, modelId, sessionMaxTokens) {
4268
+ return resolveEffectiveMaxTokens(
4269
+ this.llmConfig,
4270
+ this.getProvider.bind(this),
4271
+ this.logger,
4272
+ providerId,
4273
+ modelId,
4274
+ sessionMaxTokens
4275
+ );
4276
+ }
4277
+ /**
4278
+ * Get required max_tokens for providers that need it (e.g., Anthropic)
4279
+ * Falls back to DEFAULT_MAX_TOKENS if no value is configured
4280
+ */
4281
+ async getRequiredMaxTokens(providerId, modelId, sessionMaxTokens) {
4282
+ return getRequiredMaxTokens(
4283
+ this.llmConfig,
4284
+ this.getProvider.bind(this),
4285
+ this.logger,
4286
+ providerId,
4287
+ modelId,
4288
+ sessionMaxTokens
4289
+ );
4290
+ }
4291
+ /**
4292
+ * Calculate thinking budget and adjust max_tokens for the provider
4293
+ */
4294
+ async resolveThinkingBudget(providerId, modelId, maxTokens, thinkLevel) {
4295
+ return resolveThinkingBudget(
4296
+ this.getProvider.bind(this),
4297
+ this.logger,
4298
+ providerId,
4299
+ modelId,
4300
+ maxTokens,
4301
+ thinkLevel
4302
+ );
4303
+ }
4304
+ // --------------------------------------------------------------------------
4305
+ // Transformer Chain Completion
4306
+ // --------------------------------------------------------------------------
4307
+ /**
4308
+ * Send a completion request using transformer chain
4309
+ */
4310
+ async completeWithTransformers(options) {
4311
+ return completeWithTransformers(
4312
+ options,
4313
+ this.llmConfig,
4314
+ this.getProvider.bind(this),
4315
+ this.resolveApiKey.bind(this),
4316
+ this.complete.bind(this),
4317
+ this.logger,
4318
+ this.usageRecorder ? { recorder: this.usageRecorder } : void 0
4319
+ );
4320
+ }
4321
+ /**
4322
+ * Send a streaming completion request using transformer chain
4323
+ */
4324
+ async completeStreamWithTransformers(options, callbacks) {
4325
+ return completeStreamWithTransformers(
4326
+ options,
4327
+ callbacks,
4328
+ this.llmConfig,
4329
+ this.getProvider.bind(this),
4330
+ this.resolveApiKey.bind(this),
4331
+ this.completeStream.bind(this),
4332
+ this.logger,
4333
+ this.usageRecorder ? { recorder: this.usageRecorder } : void 0
4334
+ );
4335
+ }
4336
+ // --------------------------------------------------------------------------
4337
+ // Tool-based Completion
4338
+ // --------------------------------------------------------------------------
4339
+ /**
4340
+ * Stream completion with MCP tools support (direct API call)
4341
+ * Implements agentic loop: calls LLM -> executes tools -> calls LLM again until done
4342
+ */
4343
+ async streamWithTools(options, callbacks, mcpService, builtinExecutor) {
4344
+ return streamWithTools(
4345
+ options,
4346
+ callbacks,
4347
+ mcpService,
4348
+ this.llmConfig,
4349
+ this.getProvider.bind(this),
4350
+ this.resolveApiKey.bind(this),
4351
+ this.logger,
4352
+ builtinExecutor
4353
+ );
4354
+ }
4355
+ };
4356
+
4357
+ export {
4358
+ CompletionService,
4359
+ register,
4360
+ emit,
4361
+ attach,
4362
+ release,
4363
+ convertMessageToOpenAI,
4364
+ convertMessageToAnthropic,
4365
+ convertMessageToGemini,
4366
+ getOpenRouterProviderConfig,
4367
+ addOpenRouterProviderToRequest,
4368
+ DEFAULT_ROUTE_IDLE_MS,
4369
+ ProviderProxyRouteMap,
4370
+ getSharedExecutor,
4371
+ getResponsesEndpointTransformer,
4372
+ getAnthropicEndpointTransformer,
4373
+ getGeminiEndpointTransformer,
4374
+ readBody,
4375
+ relayResponse,
4376
+ writeError,
4377
+ resolvePoolBoundKey,
4378
+ extractRouteToken,
4379
+ isLoopbackAddress,
4380
+ ProviderProxy,
4381
+ getProviderProxy,
4382
+ __resetProviderProxyForTests,
4383
+ hashKey,
4384
+ createNamedKey,
4385
+ verifyPresentedKey,
4386
+ detectRequestRole,
4387
+ endpointToIngressFormat,
4388
+ OutboundRateLimiter,
4389
+ DEFAULT_OUTBOUND_PORT,
4390
+ OutboundApiServer,
4391
+ formatUrls,
4392
+ OUTBOUND_API_SERVER_CONFIG_KEY,
4393
+ defaultServerConfig,
4394
+ normalizeServerConfig,
4395
+ loadServerConfig,
4396
+ saveServerConfig,
4397
+ mergeServerConfig,
4398
+ getOutboundApiServer,
4399
+ __resetOutboundApiServerForTests
4400
+ };