@omnicross/core 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/ApiConverter.cjs +799 -0
  2. package/dist/ApiConverter.d.cts +82 -0
  3. package/dist/ApiConverter.d.ts +82 -0
  4. package/dist/ApiConverter.js +763 -0
  5. package/dist/BuiltinToolExecutor-BluWyeob.d.ts +81 -0
  6. package/dist/BuiltinToolExecutor-CS2WpXhM.d.cts +81 -0
  7. package/dist/CompletionService-7fCmKAP3.d.ts +212 -0
  8. package/dist/CompletionService-DtOF_War.d.cts +212 -0
  9. package/dist/{ProviderProxy-f_8ziIhW.d.cts → ProviderProxy-C-xqrkKi.d.ts} +7 -2
  10. package/dist/{ProviderProxy-vjt8sQQk.d.ts → ProviderProxy-CnMQYN59.d.cts} +7 -2
  11. package/dist/completion/BuiltinToolExecutor.cjs +327 -0
  12. package/dist/completion/BuiltinToolExecutor.d.cts +4 -0
  13. package/dist/completion/BuiltinToolExecutor.d.ts +4 -0
  14. package/dist/completion/BuiltinToolExecutor.js +296 -0
  15. package/dist/completion/CompletionService.cjs +3487 -0
  16. package/dist/completion/CompletionService.d.cts +21 -0
  17. package/dist/completion/CompletionService.d.ts +21 -0
  18. package/dist/completion/CompletionService.js +3461 -0
  19. package/dist/completion/NativeSearchInjector.cjs +196 -0
  20. package/dist/completion/NativeSearchInjector.d.cts +42 -0
  21. package/dist/completion/NativeSearchInjector.d.ts +42 -0
  22. package/dist/completion/NativeSearchInjector.js +167 -0
  23. package/dist/completion/ProviderSearchInjector.cjs +87 -0
  24. package/dist/completion/ProviderSearchInjector.d.cts +47 -0
  25. package/dist/completion/ProviderSearchInjector.d.ts +47 -0
  26. package/dist/completion/ProviderSearchInjector.js +60 -0
  27. package/dist/completion/native-search-types.cjs +67 -0
  28. package/dist/completion/native-search-types.d.cts +3 -0
  29. package/dist/completion/native-search-types.d.ts +3 -0
  30. package/dist/completion/native-search-types.js +38 -0
  31. package/dist/completion/openrouter-headers.cjs +72 -0
  32. package/dist/completion/openrouter-headers.d.cts +44 -0
  33. package/dist/completion/openrouter-headers.d.ts +44 -0
  34. package/dist/completion/openrouter-headers.js +42 -0
  35. package/dist/completion/openrouter-models.cjs +86 -0
  36. package/dist/completion/openrouter-models.d.cts +27 -0
  37. package/dist/completion/openrouter-models.d.ts +27 -0
  38. package/dist/completion/openrouter-models.js +59 -0
  39. package/dist/completion/types.cjs +18 -0
  40. package/dist/completion/types.d.cts +3 -0
  41. package/dist/completion/types.d.ts +3 -0
  42. package/dist/completion/types.js +0 -0
  43. package/dist/completion/url-builder.cjs +138 -0
  44. package/dist/completion/url-builder.d.cts +87 -0
  45. package/dist/completion/url-builder.d.ts +87 -0
  46. package/dist/completion/url-builder.js +104 -0
  47. package/dist/completion.d.cts +148 -7
  48. package/dist/completion.d.ts +148 -7
  49. package/dist/index.cjs +1 -0
  50. package/dist/index.d.cts +27 -90
  51. package/dist/index.d.ts +27 -90
  52. package/dist/index.js +1 -0
  53. package/dist/outbound-api/routeResolver.cjs +221 -0
  54. package/dist/outbound-api/routeResolver.d.cts +18 -0
  55. package/dist/outbound-api/routeResolver.d.ts +18 -0
  56. package/dist/outbound-api/routeResolver.js +192 -0
  57. package/dist/outbound-api/subscriptionRegistryPort.d.cts +5 -2
  58. package/dist/outbound-api/subscriptionRegistryPort.d.ts +5 -2
  59. package/dist/outbound-api/types.cjs +18 -0
  60. package/dist/{types-CbCN2NQP.d.ts → outbound-api/types.d.cts} +17 -3
  61. package/dist/{types-CGGrKqC_.d.cts → outbound-api/types.d.ts} +17 -3
  62. package/dist/outbound-api/types.js +0 -0
  63. package/dist/outbound-api.cjs +1 -0
  64. package/dist/outbound-api.d.cts +14 -87
  65. package/dist/outbound-api.d.ts +14 -87
  66. package/dist/outbound-api.js +1 -0
  67. package/dist/pipeline/AuthSource.cjs +18 -0
  68. package/dist/pipeline/AuthSource.d.cts +101 -0
  69. package/dist/pipeline/AuthSource.d.ts +101 -0
  70. package/dist/pipeline/AuthSource.js +0 -0
  71. package/dist/pipeline/LlmConfigProviderAuth.cjs +169 -0
  72. package/dist/pipeline/LlmConfigProviderAuth.d.cts +86 -0
  73. package/dist/pipeline/LlmConfigProviderAuth.d.ts +86 -0
  74. package/dist/pipeline/LlmConfigProviderAuth.js +142 -0
  75. package/dist/pipeline/SubscriptionAuthSource.d.cts +165 -3
  76. package/dist/pipeline/SubscriptionAuthSource.d.ts +165 -3
  77. package/dist/pipeline/executeProviderCall.cjs +70 -0
  78. package/dist/pipeline/executeProviderCall.d.cts +149 -0
  79. package/dist/pipeline/executeProviderCall.d.ts +149 -0
  80. package/dist/pipeline/executeProviderCall.js +45 -0
  81. package/dist/pipeline/resolveProviderChain.cjs +47 -0
  82. package/dist/pipeline/resolveProviderChain.d.cts +58 -0
  83. package/dist/pipeline/resolveProviderChain.d.ts +58 -0
  84. package/dist/pipeline/resolveProviderChain.js +22 -0
  85. package/dist/pipeline/resolveSubscriptionChain.cjs +68 -0
  86. package/dist/pipeline/resolveSubscriptionChain.d.cts +68 -0
  87. package/dist/pipeline/resolveSubscriptionChain.d.ts +68 -0
  88. package/dist/pipeline/resolveSubscriptionChain.js +43 -0
  89. package/dist/ports/provider-config-source.cjs +18 -0
  90. package/dist/ports/provider-config-source.d.cts +51 -0
  91. package/dist/ports/provider-config-source.d.ts +51 -0
  92. package/dist/ports/provider-config-source.js +0 -0
  93. package/dist/ports/web-search-backend.cjs +18 -0
  94. package/dist/ports/web-search-backend.d.cts +29 -0
  95. package/dist/ports/web-search-backend.d.ts +29 -0
  96. package/dist/ports/web-search-backend.js +0 -0
  97. package/dist/ports.d.cts +10 -7
  98. package/dist/ports.d.ts +10 -7
  99. package/dist/provider-proxy/ProviderProxy.cjs +4643 -0
  100. package/dist/provider-proxy/ProviderProxy.d.cts +16 -0
  101. package/dist/provider-proxy/ProviderProxy.d.ts +16 -0
  102. package/dist/provider-proxy/ProviderProxy.js +4618 -0
  103. package/dist/provider-proxy/ingress/providerProxyShared.d.cts +5 -2
  104. package/dist/provider-proxy/ingress/providerProxyShared.d.ts +5 -2
  105. package/dist/provider-proxy/types.d.cts +406 -8
  106. package/dist/provider-proxy/types.d.ts +406 -8
  107. package/dist/provider-proxy.cjs +1 -0
  108. package/dist/provider-proxy.d.cts +8 -5
  109. package/dist/provider-proxy.d.ts +8 -5
  110. package/dist/provider-proxy.js +1 -0
  111. package/dist/routeResolver-BrbK6ja9.d.cts +88 -0
  112. package/dist/routeResolver-HE-ZO0fO.d.ts +88 -0
  113. package/dist/transformer/anthropicBetaInject.cjs +51 -0
  114. package/dist/transformer/anthropicBetaInject.d.cts +20 -0
  115. package/dist/transformer/anthropicBetaInject.d.ts +20 -0
  116. package/dist/transformer/anthropicBetaInject.js +25 -0
  117. package/dist/transformer/transformers/AnthropicTransformer.cjs +1017 -0
  118. package/dist/transformer/transformers/AnthropicTransformer.d.cts +148 -0
  119. package/dist/transformer/transformers/AnthropicTransformer.d.ts +148 -0
  120. package/dist/transformer/transformers/AnthropicTransformer.js +990 -0
  121. package/dist/transformer/transformers/ReasoningTransformer.cjs +273 -0
  122. package/dist/transformer/transformers/ReasoningTransformer.d.cts +47 -0
  123. package/dist/transformer/transformers/ReasoningTransformer.d.ts +47 -0
  124. package/dist/transformer/transformers/ReasoningTransformer.js +253 -0
  125. package/dist/transformer/transformers.cjs +3206 -0
  126. package/dist/transformer/transformers.d.cts +100 -0
  127. package/dist/transformer/transformers.d.ts +100 -0
  128. package/dist/transformer/transformers.js +3174 -0
  129. package/dist/transformer.d.cts +8 -31
  130. package/dist/transformer.d.ts +8 -31
  131. package/dist/types-BScIHmPr.d.cts +153 -0
  132. package/dist/types-BScIHmPr.d.ts +153 -0
  133. package/package.json +3 -3
  134. package/dist/SubscriptionAuthSource-Cr4fVEYY.d.cts +0 -264
  135. package/dist/SubscriptionAuthSource-D89zmiSS.d.ts +0 -264
  136. package/dist/index-BTSmc9Sm.d.ts +0 -645
  137. package/dist/index-DXazdTzZ.d.cts +0 -645
  138. package/dist/types-DCzHkhJt.d.ts +0 -467
  139. package/dist/types-DZIQbgp0.d.cts +0 -467
@@ -0,0 +1,4618 @@
1
+ // src/provider-proxy/ProviderProxy.ts
2
+ import http2 from "http";
3
+
4
+ // src/serializeError.ts
5
+ function serializeError(err) {
6
+ if (err == null) return "Unknown error (null)";
7
+ if (err instanceof Error) {
8
+ let msg = err.message || err.name || "Error";
9
+ if (err.cause) {
10
+ msg += ` [cause: ${serializeError(err.cause)}]`;
11
+ }
12
+ const anyErr = err;
13
+ if (anyErr.status != null) msg += ` (status: ${anyErr.status})`;
14
+ else if (anyErr.code != null) msg += ` (code: ${anyErr.code})`;
15
+ return msg;
16
+ }
17
+ if (typeof err === "string") return err || "Empty error string";
18
+ if (typeof err !== "object") return String(err);
19
+ const obj = err;
20
+ if (typeof obj.message === "string" && obj.message) {
21
+ let msg = obj.message;
22
+ if (obj.status != null) msg += ` (status: ${obj.status})`;
23
+ else if (obj.code != null) msg += ` (code: ${obj.code})`;
24
+ if (typeof obj.type === "string") msg += ` [type: ${obj.type}]`;
25
+ return msg;
26
+ }
27
+ if (typeof obj.error === "string" && obj.error) {
28
+ return obj.error;
29
+ }
30
+ if (obj.error && typeof obj.error === "object") {
31
+ const inner = obj.error;
32
+ if (typeof inner.message === "string" && inner.message) {
33
+ let msg = inner.message;
34
+ if (typeof inner.type === "string") msg += ` [type: ${inner.type}]`;
35
+ return msg;
36
+ }
37
+ }
38
+ try {
39
+ const json = JSON.stringify(err, getCircularReplacer(), 2);
40
+ if (json && json.length > 1e3) {
41
+ return json.slice(0, 1e3) + "... (truncated)";
42
+ }
43
+ return json || "Unserializable error";
44
+ } catch {
45
+ return `Unserializable error: ${Object.prototype.toString.call(err)}`;
46
+ }
47
+ }
48
+ function getCircularReplacer() {
49
+ const seen = /* @__PURE__ */ new WeakSet();
50
+ return (_key, value) => {
51
+ if (typeof value === "object" && value !== null) {
52
+ if (seen.has(value)) return "[Circular]";
53
+ seen.add(value);
54
+ }
55
+ return value;
56
+ };
57
+ }
58
+
59
+ // src/provider-proxy/providerProxyRouteMap.ts
60
+ import { randomBytes } from "crypto";
61
+ var DEFAULT_ROUTE_IDLE_MS = 10 * 60 * 1e3;
62
+ var TOKEN_BYTES = 32;
63
+ var ProviderProxyRouteMap = class {
64
+ constructor(defaultIdleMs = DEFAULT_ROUTE_IDLE_MS) {
65
+ this.defaultIdleMs = defaultIdleMs;
66
+ }
67
+ defaultIdleMs;
68
+ routes = /* @__PURE__ */ new Map();
69
+ /**
70
+ * Register a route for one run and return its crypto-random token. The
71
+ * caller (next batch) injects the token as the forwarded auth-header sentinel
72
+ * (`ANTHROPIC_AUTH_TOKEN` / `OPENAI_API_KEY`). Optionally override the idle
73
+ * timeout (tests use a short one).
74
+ */
75
+ addRoute(context, idleMs) {
76
+ const token = randomBytes(TOKEN_BYTES).toString("hex");
77
+ const entry = {
78
+ context,
79
+ idleTimer: null,
80
+ idleMs: idleMs ?? this.defaultIdleMs
81
+ };
82
+ this.routes.set(token, entry);
83
+ this.armIdleTimer(token, entry);
84
+ return token;
85
+ }
86
+ /**
87
+ * Look up a route by its token, touching the idle timer so an active run's
88
+ * context survives. Returns `undefined` on a miss / reaped entry — the caller
89
+ * rejects (no fallback).
90
+ */
91
+ lookup(token) {
92
+ if (!token) return void 0;
93
+ const entry = this.routes.get(token);
94
+ if (!entry) return void 0;
95
+ this.armIdleTimer(token, entry);
96
+ return entry.context;
97
+ }
98
+ /** Remove a route at run end. Returns true if an entry existed. */
99
+ removeRoute(token) {
100
+ const entry = this.routes.get(token);
101
+ if (!entry) return false;
102
+ this.clearIdleTimer(entry);
103
+ this.routes.delete(token);
104
+ return true;
105
+ }
106
+ /** Current live-route count (tests / diagnostics). */
107
+ size() {
108
+ return this.routes.size;
109
+ }
110
+ /** Whether a token currently resolves (does NOT touch the idle timer). */
111
+ has(token) {
112
+ return this.routes.has(token);
113
+ }
114
+ /** Tear down every route (proxy stop / app teardown). */
115
+ clear() {
116
+ for (const entry of this.routes.values()) {
117
+ this.clearIdleTimer(entry);
118
+ }
119
+ this.routes.clear();
120
+ }
121
+ // ---------------------------------------------------------------------------
122
+ // Idle reaping (modeled on AcpSessionManager.armIdleTimer / clearIdleTimer)
123
+ // ---------------------------------------------------------------------------
124
+ armIdleTimer(token, entry) {
125
+ this.clearIdleTimer(entry);
126
+ entry.idleTimer = setTimeout(() => {
127
+ this.routes.delete(token);
128
+ }, entry.idleMs);
129
+ entry.idleTimer.unref?.();
130
+ }
131
+ clearIdleTimer(entry) {
132
+ if (entry.idleTimer) {
133
+ clearTimeout(entry.idleTimer);
134
+ entry.idleTimer = null;
135
+ }
136
+ }
137
+ };
138
+
139
+ // src/completion/DirectApiHandler.ts
140
+ import { getOpenAIReasoningEffort as getOpenAIReasoningEffort2 } from "@omnicross/contracts/thinking-config";
141
+
142
+ // src/openrouter.ts
143
+ function isOpenRouterProvider(provider) {
144
+ const baseUrl = (provider.api_base_url || "").toLowerCase();
145
+ return baseUrl.includes("openrouter.ai");
146
+ }
147
+ var OPENROUTER_APP_HEADERS = {
148
+ "HTTP-Referer": "https://omnicross.dev",
149
+ "X-Title": "omnicross"
150
+ };
151
+
152
+ // src/outbound-api/OutboundApiServer.ts
153
+ import http from "http";
154
+ import { networkInterfaces } from "os";
155
+
156
+ // src/outbound-api/outboundApiRouter.ts
157
+ import { Readable } from "stream";
158
+
159
+ // src/outbound-api/outboundApiKeyAuth.ts
160
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
161
+
162
+ // src/outbound-api/roleDetection.ts
163
+ import { normalizeModelId } from "@omnicross/contracts/canonical-models";
164
+
165
+ // src/outbound-api/subscriptionSupport.ts
166
+ var SUBSCRIPTION_PROVIDER_IDS = [
167
+ "claude",
168
+ "codex",
169
+ "gemini",
170
+ "opencodego"
171
+ ];
172
+ var SUBSCRIPTION_ID_SET = new Set(SUBSCRIPTION_PROVIDER_IDS);
173
+
174
+ // src/transformer/anthropicBetaInject.ts
175
+ import { isExtendedContextCapable } from "@omnicross/contracts/extended-context";
176
+ var EXTENDED_CONTEXT_BETA = "context-1m-2025-08-07";
177
+ var ANTHROPIC_BETA_HEADER = "anthropic-beta";
178
+ function injectExtendedContextBeta(headers, model, useExtendedContext) {
179
+ if (!useExtendedContext) return;
180
+ if (!isExtendedContextCapable(model)) return;
181
+ let existingValue = "";
182
+ for (const key of Object.keys(headers)) {
183
+ if (key.toLowerCase() === ANTHROPIC_BETA_HEADER) {
184
+ const v = headers[key];
185
+ if (typeof v === "string") existingValue = v;
186
+ if (key !== ANTHROPIC_BETA_HEADER) delete headers[key];
187
+ }
188
+ }
189
+ const parts = existingValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
190
+ if (!parts.includes(EXTENDED_CONTEXT_BETA)) {
191
+ parts.push(EXTENDED_CONTEXT_BETA);
192
+ }
193
+ headers[ANTHROPIC_BETA_HEADER] = parts.join(",");
194
+ }
195
+
196
+ // src/transformer/TransformerChainExecutor.ts
197
+ var defaultLogger = {
198
+ debug: (msg, ...args) => console.debug(`[ChainExecutor] ${msg}`, ...args),
199
+ info: (msg, ...args) => console.info(`[ChainExecutor] ${msg}`, ...args),
200
+ warn: (msg, ...args) => console.warn(`[ChainExecutor] ${msg}`, ...args),
201
+ error: (msg, ...args) => console.error(`[ChainExecutor] ${msg}`, ...args)
202
+ };
203
+ var TransformerChainExecutor = class {
204
+ logger;
205
+ constructor(logger) {
206
+ this.logger = logger ?? defaultLogger;
207
+ }
208
+ /**
209
+ * Execute the request transformation chain
210
+ *
211
+ * @param request - Original request body
212
+ * @param provider - LLM provider configuration
213
+ * @param chain - Resolved transformer chain
214
+ * @param options - Execution options
215
+ * @returns Transformed request result
216
+ */
217
+ async executeRequestChain(request, provider, chain, options = {}) {
218
+ const { endpointTransformer, headers, extendedContext } = options;
219
+ const context = {
220
+ logger: this.logger,
221
+ providerName: provider.name
222
+ };
223
+ let requestBody = request;
224
+ let config = {};
225
+ let bypass = false;
226
+ bypass = this.shouldBypassTransformers(
227
+ chain,
228
+ endpointTransformer,
229
+ requestBody
230
+ );
231
+ if (bypass) {
232
+ if (headers) {
233
+ const cleanHeaders = this.cleanHeaders(headers);
234
+ config.headers = cleanHeaders;
235
+ }
236
+ this.logger.debug("Bypass mode enabled - skipping transformations");
237
+ }
238
+ if (!bypass && endpointTransformer?.transformRequestOut) {
239
+ this.logger.debug("Executing transformRequestOut");
240
+ try {
241
+ const transformOut = await endpointTransformer.transformRequestOut(requestBody, context);
242
+ if (transformOut && typeof transformOut === "object") {
243
+ if ("body" in transformOut) {
244
+ requestBody = transformOut.body;
245
+ config = transformOut.config ?? {};
246
+ } else {
247
+ requestBody = transformOut;
248
+ }
249
+ }
250
+ } catch (error) {
251
+ this.logger.error(`transformRequestOut error: ${this.getErrorMessage(error)}`);
252
+ throw error;
253
+ }
254
+ }
255
+ if (!bypass && chain.providerTransformers.length > 0) {
256
+ this.logger.debug(`Executing ${chain.providerTransformers.length} provider transformers`);
257
+ for (const transformer of chain.providerTransformers) {
258
+ if (transformer.transformRequestIn) {
259
+ try {
260
+ const transformIn = await transformer.transformRequestIn(
261
+ requestBody,
262
+ provider,
263
+ context
264
+ );
265
+ if (transformIn && typeof transformIn === "object") {
266
+ if ("body" in transformIn) {
267
+ requestBody = transformIn.body;
268
+ config = { ...config, ...transformIn.config };
269
+ } else {
270
+ requestBody = transformIn;
271
+ }
272
+ }
273
+ } catch (error) {
274
+ this.logger.error(
275
+ `Provider transformer ${transformer.name} error: ${this.getErrorMessage(error)}`
276
+ );
277
+ throw error;
278
+ }
279
+ }
280
+ }
281
+ }
282
+ if (!bypass && chain.modelTransformers.length > 0) {
283
+ this.logger.debug(`Executing ${chain.modelTransformers.length} model transformers`);
284
+ for (const transformer of chain.modelTransformers) {
285
+ if (transformer.transformRequestIn) {
286
+ try {
287
+ const result = await transformer.transformRequestIn(
288
+ requestBody,
289
+ provider,
290
+ context
291
+ );
292
+ requestBody = result;
293
+ } catch (error) {
294
+ this.logger.error(
295
+ `Model transformer ${transformer.name} error: ${this.getErrorMessage(error)}`
296
+ );
297
+ throw error;
298
+ }
299
+ }
300
+ }
301
+ }
302
+ if (extendedContext?.enabled) {
303
+ if (!config.headers || typeof config.headers !== "object") {
304
+ config.headers = {};
305
+ }
306
+ injectExtendedContextBeta(
307
+ config.headers,
308
+ extendedContext.model,
309
+ true
310
+ );
311
+ }
312
+ return { requestBody, config, bypass };
313
+ }
314
+ /**
315
+ * Execute the response transformation chain
316
+ *
317
+ * @param request - Original request (for context)
318
+ * @param response - Response from provider
319
+ * @param provider - LLM provider configuration
320
+ * @param chain - Resolved transformer chain
321
+ * @param options - Execution options
322
+ * @returns Transformed response
323
+ */
324
+ async executeResponseChain(request, response, provider, chain, options = {}) {
325
+ const { endpointTransformer } = options;
326
+ const context = {
327
+ logger: this.logger,
328
+ providerName: provider.name
329
+ };
330
+ let finalResponse = response;
331
+ const bypass = this.shouldBypassTransformers(chain, endpointTransformer, request);
332
+ if (bypass) {
333
+ this.logger.debug("Bypass mode - skipping response transformations");
334
+ return finalResponse;
335
+ }
336
+ if (chain.modelTransformers.length > 0) {
337
+ const reversedModelTransformers = [...chain.modelTransformers].reverse();
338
+ this.logger.debug(
339
+ `Executing ${reversedModelTransformers.length} model response transformers (reversed)`
340
+ );
341
+ for (const transformer of reversedModelTransformers) {
342
+ if (transformer.transformResponseOut) {
343
+ try {
344
+ finalResponse = await transformer.transformResponseOut(finalResponse, context);
345
+ } catch (error) {
346
+ this.logger.error(
347
+ `Model transformer ${transformer.name} response error: ${this.getErrorMessage(error)}`
348
+ );
349
+ throw error;
350
+ }
351
+ }
352
+ }
353
+ }
354
+ if (chain.providerTransformers.length > 0) {
355
+ const reversedProviderTransformers = [...chain.providerTransformers].reverse();
356
+ this.logger.debug(
357
+ `Executing ${reversedProviderTransformers.length} provider response transformers (reversed)`
358
+ );
359
+ for (const transformer of reversedProviderTransformers) {
360
+ if (transformer.transformResponseOut) {
361
+ try {
362
+ finalResponse = await transformer.transformResponseOut(finalResponse, context);
363
+ } catch (error) {
364
+ this.logger.error(
365
+ `Provider transformer ${transformer.name} response error: ${this.getErrorMessage(error)}`
366
+ );
367
+ throw error;
368
+ }
369
+ }
370
+ }
371
+ }
372
+ if (endpointTransformer?.transformResponseIn) {
373
+ this.logger.debug("Executing transformResponseIn");
374
+ try {
375
+ finalResponse = await endpointTransformer.transformResponseIn(finalResponse, context);
376
+ } catch (error) {
377
+ this.logger.error(`transformResponseIn error: ${this.getErrorMessage(error)}`);
378
+ throw error;
379
+ }
380
+ }
381
+ return finalResponse;
382
+ }
383
+ /**
384
+ * Execute authentication handler if available
385
+ *
386
+ * @param request - Request body
387
+ * @param provider - LLM provider
388
+ * @param endpointTransformer - Endpoint transformer with auth handler
389
+ * @param context - Transformer context
390
+ * @returns Auth result with potentially modified request and config
391
+ */
392
+ async executeAuth(request, provider, endpointTransformer, context) {
393
+ let requestBody = request;
394
+ let config = {};
395
+ if (endpointTransformer?.auth) {
396
+ this.logger.debug("Executing auth handler");
397
+ try {
398
+ const auth = await endpointTransformer.auth(requestBody, provider, context);
399
+ if (auth && typeof auth === "object") {
400
+ if ("body" in auth) {
401
+ requestBody = auth.body;
402
+ const authConfig = auth.config;
403
+ if (authConfig) {
404
+ const headers = { ...config.headers ?? {}, ...authConfig.headers ?? {} };
405
+ delete headers["host"];
406
+ config = { ...config, ...authConfig, headers };
407
+ }
408
+ } else {
409
+ requestBody = auth;
410
+ }
411
+ }
412
+ } catch (error) {
413
+ this.logger.error(`Auth handler error: ${this.getErrorMessage(error)}`);
414
+ throw error;
415
+ }
416
+ }
417
+ return { requestBody, config };
418
+ }
419
+ /**
420
+ * Check if transformers should be bypassed (optimization)
421
+ *
422
+ * Bypass is enabled when:
423
+ * - Provider has only one transformer that matches the endpoint transformer
424
+ * - Model has no specific transformers or only the same endpoint transformer
425
+ */
426
+ shouldBypassTransformers(chain, endpointTransformer, _request) {
427
+ if (!endpointTransformer?.name) {
428
+ return false;
429
+ }
430
+ const providerHasOnlyEndpoint = chain.providerTransformers.length === 1 && chain.providerTransformers[0]?.name === endpointTransformer.name;
431
+ const modelHasNoTransformers = chain.modelTransformers.length === 0;
432
+ const modelHasOnlyEndpoint = chain.modelTransformers.length === 1 && chain.modelTransformers[0]?.name === endpointTransformer.name;
433
+ return providerHasOnlyEndpoint && (modelHasNoTransformers || modelHasOnlyEndpoint);
434
+ }
435
+ /**
436
+ * Clean headers for pass-through
437
+ */
438
+ cleanHeaders(headers) {
439
+ const result = {};
440
+ if (headers instanceof Headers) {
441
+ headers.forEach((value, key) => {
442
+ if (key.toLowerCase() !== "content-length") {
443
+ result[key] = value;
444
+ }
445
+ });
446
+ } else {
447
+ for (const [key, value] of Object.entries(headers)) {
448
+ if (key.toLowerCase() !== "content-length") {
449
+ result[key] = value;
450
+ }
451
+ }
452
+ }
453
+ return result;
454
+ }
455
+ /**
456
+ * Get error message from unknown error
457
+ */
458
+ getErrorMessage(error) {
459
+ if (error instanceof Error) {
460
+ return error.message;
461
+ }
462
+ return String(error);
463
+ }
464
+ };
465
+
466
+ // src/transformer/transformers/AnthropicToolHandling.ts
467
+ function isServerSideTool(tool) {
468
+ const type = String(tool.type || "");
469
+ return type.startsWith("web_search_") || type.startsWith("code_execution_") || type.startsWith("text_editor_") || type.startsWith("memory_") || type.startsWith("web_fetch_") || type.startsWith("search_tool_");
470
+ }
471
+ function convertAnthropicToolsToOpenAI(tools) {
472
+ return tools.filter((tool) => !isServerSideTool(tool)).map((tool) => ({
473
+ type: "function",
474
+ function: {
475
+ name: String(tool.name),
476
+ description: String(tool.description || ""),
477
+ parameters: tool.input_schema
478
+ }
479
+ }));
480
+ }
481
+
482
+ // src/transformer/transformers/AnthropicTypes.ts
483
+ function getThinkLevel(budgetTokens) {
484
+ if (!budgetTokens || budgetTokens <= 0) return "none";
485
+ if (budgetTokens < 4096) return "low";
486
+ if (budgetTokens < 16384) return "medium";
487
+ return "high";
488
+ }
489
+ function formatBase64(data, mediaType) {
490
+ if (data.startsWith("data:")) return data;
491
+ return `data:${mediaType || "image/png"};base64,${data}`;
492
+ }
493
+
494
+ // src/transformer/transformers/AnthropicRequestBuilder.ts
495
+ function buildAnthropicRequestBody(request) {
496
+ let systemContent;
497
+ const anthropicMessages = [];
498
+ for (let i = 0; i < request.messages.length; i++) {
499
+ const msg = request.messages[i];
500
+ if (msg.role === "system") {
501
+ if (typeof msg.content === "string") {
502
+ systemContent = msg.content;
503
+ } else if (Array.isArray(msg.content)) {
504
+ systemContent = msg.content.filter((c) => c.type === "text").map((c) => ({
505
+ type: "text",
506
+ text: c.text,
507
+ ...c.cache_control ? { cache_control: c.cache_control } : {}
508
+ }));
509
+ }
510
+ continue;
511
+ }
512
+ if (msg.role === "assistant") {
513
+ const content = [];
514
+ if (msg.thinking?.content) {
515
+ const block = {
516
+ type: "thinking",
517
+ thinking: msg.thinking.content
518
+ };
519
+ if (msg.thinking.signature) {
520
+ block.signature = msg.thinking.signature;
521
+ }
522
+ content.push(block);
523
+ }
524
+ if (msg.content) {
525
+ const text = typeof msg.content === "string" ? msg.content : msg.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
526
+ if (text) {
527
+ content.push({ type: "text", text });
528
+ }
529
+ }
530
+ if (msg.tool_calls?.length) {
531
+ for (const tc of msg.tool_calls) {
532
+ let input;
533
+ try {
534
+ input = typeof tc.function.arguments === "string" ? JSON.parse(tc.function.arguments) : tc.function.arguments;
535
+ } catch {
536
+ input = { text: tc.function.arguments || "" };
537
+ }
538
+ content.push({
539
+ type: "tool_use",
540
+ id: tc.id,
541
+ name: tc.function.name,
542
+ input
543
+ });
544
+ }
545
+ }
546
+ anthropicMessages.push({
547
+ role: "assistant",
548
+ content: content.length > 0 ? content : ""
549
+ });
550
+ continue;
551
+ }
552
+ if (msg.role === "tool") {
553
+ const toolResults = [];
554
+ let j = i;
555
+ while (j < request.messages.length && request.messages[j].role === "tool") {
556
+ const t = request.messages[j];
557
+ toolResults.push({
558
+ type: "tool_result",
559
+ tool_use_id: t.tool_call_id || "",
560
+ content: typeof t.content === "string" ? t.content : JSON.stringify(t.content),
561
+ ...t.cache_control ? { cache_control: t.cache_control } : {}
562
+ });
563
+ j++;
564
+ }
565
+ anthropicMessages.push({ role: "user", content: toolResults });
566
+ i = j - 1;
567
+ continue;
568
+ }
569
+ if (typeof msg.content === "string") {
570
+ anthropicMessages.push({ role: "user", content: msg.content });
571
+ } else if (Array.isArray(msg.content)) {
572
+ const content = msg.content.map((part) => {
573
+ if (part.type === "image_url") {
574
+ const url = part.image_url.url;
575
+ if (url.startsWith("data:")) {
576
+ const match = url.match(/^data:([^;]+);base64,(.+)$/);
577
+ if (match) {
578
+ return {
579
+ type: "image",
580
+ source: { type: "base64", media_type: match[1], data: match[2] }
581
+ };
582
+ }
583
+ }
584
+ return {
585
+ type: "image",
586
+ source: { type: "url", url }
587
+ };
588
+ }
589
+ return { type: "text", text: part.text };
590
+ });
591
+ anthropicMessages.push({ role: "user", content });
592
+ }
593
+ }
594
+ const body = {
595
+ model: request.model,
596
+ messages: anthropicMessages,
597
+ max_tokens: request.max_tokens || 4096,
598
+ stream: request.stream ?? false
599
+ };
600
+ if (request.temperature !== void 0) {
601
+ body.temperature = request.temperature;
602
+ }
603
+ if (systemContent !== void 0) {
604
+ body.system = systemContent;
605
+ }
606
+ if (request.tools?.length) {
607
+ body.tools = request.tools.map((tool) => ({
608
+ name: tool.function.name,
609
+ description: tool.function.description || "",
610
+ input_schema: tool.function.parameters
611
+ }));
612
+ }
613
+ const serverSideTools = request._serverSideTools;
614
+ if (serverSideTools?.length) {
615
+ body.tools = [...body.tools || [], ...serverSideTools];
616
+ }
617
+ if (request.tool_choice) {
618
+ if (typeof request.tool_choice === "string") {
619
+ if (request.tool_choice === "required") {
620
+ body.tool_choice = { type: "any" };
621
+ } else if (request.tool_choice !== "none") {
622
+ body.tool_choice = { type: request.tool_choice };
623
+ }
624
+ } else if (typeof request.tool_choice === "object" && "function" in request.tool_choice) {
625
+ body.tool_choice = { type: "tool", name: request.tool_choice.function.name };
626
+ }
627
+ }
628
+ if (request.reasoning?.enabled) {
629
+ const budgetMap = { low: 2048, medium: 8192, high: 32768 };
630
+ const budget = request.reasoning.max_tokens || budgetMap[request.reasoning.effort || "medium"] || 8192;
631
+ body.thinking = { type: "enabled", budget_tokens: budget };
632
+ body.temperature = 1;
633
+ }
634
+ return body;
635
+ }
636
+
637
+ // src/transformer/transformers/AnthropicResponseConversion.ts
638
+ function convertAnthropicResponseToOpenAI(anthropicResponse) {
639
+ const content = anthropicResponse.content || [];
640
+ const textParts = content.filter((c) => c.type === "text").map((c) => c.text);
641
+ const toolUses = content.filter((c) => c.type === "tool_use" || c.type === "server_tool_use");
642
+ const thinkingBlock = content.find((c) => c.type === "thinking");
643
+ const searchResults = content.filter((c) => c.type === "web_search_tool_result");
644
+ for (const sr of searchResults) {
645
+ const searches = sr.content;
646
+ if (searches?.length) {
647
+ const formatted = searches.map((s) => `[${s.title}](${s.url}): ${s.page_content || s.snippet || ""}`).join("\n");
648
+ textParts.push(`
649
+
650
+ **Search Results:**
651
+ ${formatted}`);
652
+ }
653
+ }
654
+ const message = {
655
+ role: "assistant",
656
+ content: textParts.join("") || null
657
+ };
658
+ if (toolUses.length > 0) {
659
+ message.tool_calls = toolUses.map((tc) => ({
660
+ id: tc.id,
661
+ type: "function",
662
+ function: {
663
+ name: tc.name,
664
+ arguments: JSON.stringify(tc.input || {})
665
+ }
666
+ }));
667
+ }
668
+ if (thinkingBlock) {
669
+ message.thinking = {
670
+ content: thinkingBlock.thinking,
671
+ signature: thinkingBlock.signature
672
+ };
673
+ }
674
+ const stopReasonMapping = {
675
+ end_turn: "stop",
676
+ max_tokens: "length",
677
+ tool_use: "tool_calls",
678
+ stop_sequence: "stop"
679
+ };
680
+ const usage = anthropicResponse.usage;
681
+ return {
682
+ id: anthropicResponse.id || `chatcmpl-${Date.now()}`,
683
+ object: "chat.completion",
684
+ created: Math.floor(Date.now() / 1e3),
685
+ model: anthropicResponse.model || "unknown",
686
+ choices: [{
687
+ index: 0,
688
+ message,
689
+ finish_reason: stopReasonMapping[anthropicResponse.stop_reason] || "stop"
690
+ }],
691
+ usage: usage ? {
692
+ prompt_tokens: usage.input_tokens || 0,
693
+ completion_tokens: usage.output_tokens || 0,
694
+ total_tokens: (usage.input_tokens || 0) + (usage.output_tokens || 0)
695
+ } : void 0
696
+ };
697
+ }
698
+ function convertOpenAIResponseToAnthropic2(openaiResponse) {
699
+ const choice = openaiResponse.choices?.[0];
700
+ if (!choice) {
701
+ throw new Error("No choices found in OpenAI response");
702
+ }
703
+ const message = choice.message;
704
+ const content = [];
705
+ if (message.content) {
706
+ content.push({
707
+ type: "text",
708
+ text: message.content
709
+ });
710
+ }
711
+ const toolCalls = message.tool_calls;
712
+ if (toolCalls?.length) {
713
+ for (const toolCall of toolCalls) {
714
+ const func = toolCall.function;
715
+ let parsedInput = {};
716
+ try {
717
+ const args = func.arguments;
718
+ parsedInput = typeof args === "string" ? JSON.parse(args) : args;
719
+ } catch {
720
+ parsedInput = { text: func.arguments || "" };
721
+ }
722
+ content.push({
723
+ type: "tool_use",
724
+ id: toolCall.id,
725
+ name: func.name,
726
+ input: parsedInput
727
+ });
728
+ }
729
+ }
730
+ const thinking = message.thinking;
731
+ if (thinking?.content) {
732
+ content.push({
733
+ type: "thinking",
734
+ thinking: thinking.content,
735
+ signature: thinking.signature
736
+ });
737
+ }
738
+ const finishReason = choice.finish_reason;
739
+ const stopReasonMapping = {
740
+ stop: "end_turn",
741
+ length: "max_tokens",
742
+ tool_calls: "tool_use",
743
+ content_filter: "stop_sequence"
744
+ };
745
+ const usage = openaiResponse.usage;
746
+ const usageDetails = usage?.prompt_tokens_details;
747
+ return {
748
+ id: openaiResponse.id,
749
+ type: "message",
750
+ role: "assistant",
751
+ model: openaiResponse.model,
752
+ content,
753
+ stop_reason: stopReasonMapping[finishReason] || "end_turn",
754
+ stop_sequence: null,
755
+ usage: {
756
+ input_tokens: (usage?.prompt_tokens || 0) - (usageDetails?.cached_tokens || 0),
757
+ output_tokens: usage?.completion_tokens || 0,
758
+ cache_read_input_tokens: usageDetails?.cached_tokens || 0
759
+ }
760
+ };
761
+ }
762
+
763
+ // src/transformer/transformers/AnthropicConversion.ts
764
+ function transformAnthropicRequestToUnified(request) {
765
+ const anthropicRequest = request;
766
+ const messages = [];
767
+ if (anthropicRequest.system) {
768
+ if (typeof anthropicRequest.system === "string") {
769
+ messages.push({
770
+ role: "system",
771
+ content: anthropicRequest.system
772
+ });
773
+ } else if (Array.isArray(anthropicRequest.system)) {
774
+ const textParts = anthropicRequest.system.filter((item) => item.type === "text" && item.text).map((item) => ({
775
+ type: "text",
776
+ text: item.text,
777
+ cache_control: item.cache_control
778
+ }));
779
+ if (textParts.length > 0) {
780
+ messages.push({
781
+ role: "system",
782
+ content: textParts
783
+ });
784
+ }
785
+ }
786
+ }
787
+ const requestMessages = JSON.parse(JSON.stringify(anthropicRequest.messages || []));
788
+ for (const msg of requestMessages) {
789
+ if (msg.role !== "user" && msg.role !== "assistant") continue;
790
+ if (typeof msg.content === "string") {
791
+ messages.push({
792
+ role: msg.role,
793
+ content: msg.content
794
+ });
795
+ continue;
796
+ }
797
+ if (Array.isArray(msg.content)) {
798
+ if (msg.role === "user") {
799
+ const toolParts = msg.content.filter(
800
+ (c) => c.type === "tool_result" && c.tool_use_id
801
+ );
802
+ for (const tool of toolParts) {
803
+ messages.push({
804
+ role: "tool",
805
+ content: typeof tool.content === "string" ? tool.content : JSON.stringify(tool.content),
806
+ tool_call_id: tool.tool_use_id,
807
+ cache_control: tool.cache_control
808
+ });
809
+ }
810
+ const textAndMediaParts = msg.content.filter(
811
+ (c) => c.type === "text" && c.text || c.type === "image" && c.source
812
+ );
813
+ if (textAndMediaParts.length > 0) {
814
+ messages.push({
815
+ role: "user",
816
+ content: textAndMediaParts.map((part) => {
817
+ if (part.type === "image") {
818
+ const imagePart = part;
819
+ return {
820
+ type: "image_url",
821
+ image_url: {
822
+ url: imagePart.source.type === "base64" ? formatBase64(imagePart.source.data || "", imagePart.source.media_type) : imagePart.source.url || ""
823
+ },
824
+ media_type: imagePart.source.media_type
825
+ };
826
+ }
827
+ return {
828
+ type: "text",
829
+ text: part.text
830
+ };
831
+ })
832
+ });
833
+ }
834
+ } else if (msg.role === "assistant") {
835
+ const assistantMessage = {
836
+ role: "assistant",
837
+ content: ""
838
+ };
839
+ const textParts = msg.content.filter(
840
+ (c) => c.type === "text" && c.text
841
+ );
842
+ if (textParts.length > 0) {
843
+ assistantMessage.content = textParts.map((t) => t.text).join("\n");
844
+ }
845
+ const toolCallParts = msg.content.filter(
846
+ (c) => c.type === "tool_use" && c.id
847
+ );
848
+ if (toolCallParts.length > 0) {
849
+ assistantMessage.tool_calls = toolCallParts.map((tool) => ({
850
+ id: tool.id,
851
+ type: "function",
852
+ function: {
853
+ name: tool.name,
854
+ arguments: JSON.stringify(tool.input || {})
855
+ }
856
+ }));
857
+ }
858
+ const thinkingPart = msg.content.find(
859
+ (c) => c.type === "thinking"
860
+ );
861
+ if (thinkingPart?.thinking) {
862
+ assistantMessage.thinking = {
863
+ content: thinkingPart.thinking,
864
+ signature: thinkingPart.signature
865
+ };
866
+ }
867
+ messages.push(assistantMessage);
868
+ }
869
+ }
870
+ }
871
+ const rawTools = anthropicRequest.tools || [];
872
+ const serverSideTools = rawTools.filter((t) => isServerSideTool(t));
873
+ const functionTools = rawTools.length > 0 ? convertAnthropicToolsToOpenAI(rawTools) : void 0;
874
+ const result = {
875
+ messages,
876
+ model: anthropicRequest.model,
877
+ max_tokens: anthropicRequest.max_tokens,
878
+ temperature: anthropicRequest.temperature,
879
+ stream: anthropicRequest.stream,
880
+ tools: functionTools?.length ? functionTools : void 0
881
+ };
882
+ if (serverSideTools.length > 0) {
883
+ result._serverSideTools = serverSideTools;
884
+ }
885
+ if (anthropicRequest.thinking) {
886
+ result.reasoning = {
887
+ effort: getThinkLevel(anthropicRequest.thinking.budget_tokens),
888
+ enabled: anthropicRequest.thinking.type === "enabled"
889
+ };
890
+ }
891
+ if (anthropicRequest.tool_choice) {
892
+ if (anthropicRequest.tool_choice.type === "tool" && anthropicRequest.tool_choice.name) {
893
+ result.tool_choice = {
894
+ type: "function",
895
+ function: { name: anthropicRequest.tool_choice.name }
896
+ };
897
+ } else {
898
+ result.tool_choice = anthropicRequest.tool_choice.type;
899
+ }
900
+ }
901
+ return result;
902
+ }
903
+
904
+ // src/transformer/transformers/AnthropicAnthropicToOpenAIStream.ts
905
+ function convertAnthropicStreamToOpenAI2(anthropicStream, logger) {
906
+ const decoder = new TextDecoder();
907
+ const encoder = new TextEncoder();
908
+ const activeToolCalls = /* @__PURE__ */ new Map();
909
+ let toolCallCounter = 0;
910
+ let model = "unknown";
911
+ let messageId = `chatcmpl-${Date.now()}`;
912
+ return new ReadableStream({
913
+ start: async (controller) => {
914
+ const reader = anthropicStream.getReader();
915
+ let buffer = "";
916
+ let isClosed = false;
917
+ const safeEnqueue = (data) => {
918
+ if (!isClosed) {
919
+ try {
920
+ controller.enqueue(encoder.encode(data));
921
+ } catch {
922
+ isClosed = true;
923
+ }
924
+ }
925
+ };
926
+ try {
927
+ while (true) {
928
+ const { done, value } = await reader.read();
929
+ if (done) break;
930
+ buffer += decoder.decode(value, { stream: true });
931
+ const lines = buffer.split("\n");
932
+ buffer = lines.pop() || "";
933
+ for (const line of lines) {
934
+ if (isClosed) break;
935
+ if (!line.startsWith("data:")) continue;
936
+ const data = line.slice(5).trim();
937
+ if (!data || data === "[DONE]") continue;
938
+ try {
939
+ const event = JSON.parse(data);
940
+ if (event.type === "message_start" && event.message) {
941
+ model = event.message.model || model;
942
+ messageId = event.message.id || messageId;
943
+ }
944
+ if (event.type === "content_block_delta" && event.delta) {
945
+ const chunk = {
946
+ id: messageId,
947
+ object: "chat.completion.chunk",
948
+ created: Math.floor(Date.now() / 1e3),
949
+ model,
950
+ choices: [{ index: 0, delta: {}, finish_reason: null }]
951
+ };
952
+ const delta = chunk.choices[0].delta;
953
+ if (event.delta.type === "text_delta") {
954
+ delta.content = event.delta.text;
955
+ } else if (event.delta.type === "input_json_delta") {
956
+ const toolInfo = activeToolCalls.get(event.index);
957
+ if (toolInfo) {
958
+ delta.tool_calls = [{
959
+ index: toolInfo.index,
960
+ function: { arguments: event.delta.partial_json }
961
+ }];
962
+ }
963
+ } else if (event.delta.type === "thinking_delta") {
964
+ delta.thinking = { content: event.delta.thinking };
965
+ } else if (event.delta.type === "signature_delta") {
966
+ delta.thinking = { signature: event.delta.signature };
967
+ } else {
968
+ continue;
969
+ }
970
+ safeEnqueue(`data: ${JSON.stringify(chunk)}
971
+
972
+ `);
973
+ }
974
+ if (event.type === "content_block_start" && event.content_block) {
975
+ if (event.content_block.type === "tool_use" || event.content_block.type === "server_tool_use") {
976
+ const tcIndex = toolCallCounter++;
977
+ activeToolCalls.set(event.index, {
978
+ id: event.content_block.id,
979
+ name: event.content_block.name,
980
+ index: tcIndex
981
+ });
982
+ const chunk = {
983
+ id: messageId,
984
+ object: "chat.completion.chunk",
985
+ created: Math.floor(Date.now() / 1e3),
986
+ model,
987
+ choices: [{
988
+ index: 0,
989
+ delta: {
990
+ tool_calls: [{
991
+ index: tcIndex,
992
+ id: event.content_block.id,
993
+ type: "function",
994
+ function: { name: event.content_block.name, arguments: "" }
995
+ }]
996
+ },
997
+ finish_reason: null
998
+ }]
999
+ };
1000
+ safeEnqueue(`data: ${JSON.stringify(chunk)}
1001
+
1002
+ `);
1003
+ } else if (event.content_block.type === "web_search_tool_result") {
1004
+ const searches = event.content_block.content;
1005
+ if (searches?.length) {
1006
+ const formatted = searches.map((s) => `[${s.title}](${s.url}): ${s.page_content || s.snippet || ""}`).join("\n");
1007
+ const chunk = {
1008
+ id: messageId,
1009
+ object: "chat.completion.chunk",
1010
+ created: Math.floor(Date.now() / 1e3),
1011
+ model,
1012
+ choices: [{
1013
+ index: 0,
1014
+ delta: { content: `
1015
+
1016
+ **Search Results:**
1017
+ ${formatted}` },
1018
+ finish_reason: null
1019
+ }]
1020
+ };
1021
+ safeEnqueue(`data: ${JSON.stringify(chunk)}
1022
+
1023
+ `);
1024
+ }
1025
+ }
1026
+ }
1027
+ if (event.type === "message_delta" && event.delta) {
1028
+ const stopReasonMapping = {
1029
+ end_turn: "stop",
1030
+ max_tokens: "length",
1031
+ tool_use: "tool_calls",
1032
+ stop_sequence: "stop"
1033
+ };
1034
+ const chunk = {
1035
+ id: messageId,
1036
+ object: "chat.completion.chunk",
1037
+ created: Math.floor(Date.now() / 1e3),
1038
+ model,
1039
+ choices: [{
1040
+ index: 0,
1041
+ delta: {},
1042
+ finish_reason: stopReasonMapping[event.delta.stop_reason] || "stop"
1043
+ }]
1044
+ };
1045
+ if (event.usage) {
1046
+ chunk.usage = {
1047
+ prompt_tokens: event.usage.input_tokens || 0,
1048
+ completion_tokens: event.usage.output_tokens || 0,
1049
+ total_tokens: (event.usage.input_tokens || 0) + (event.usage.output_tokens || 0)
1050
+ };
1051
+ }
1052
+ safeEnqueue(`data: ${JSON.stringify(chunk)}
1053
+
1054
+ `);
1055
+ }
1056
+ } catch (e) {
1057
+ logger?.error("Error parsing Anthropic stream event:", e);
1058
+ }
1059
+ }
1060
+ }
1061
+ } catch (e) {
1062
+ if (!isClosed) {
1063
+ controller.error(e);
1064
+ }
1065
+ } finally {
1066
+ safeEnqueue("data: [DONE]\n\n");
1067
+ if (!isClosed) {
1068
+ try {
1069
+ controller.close();
1070
+ } catch {
1071
+ }
1072
+ }
1073
+ reader.releaseLock();
1074
+ }
1075
+ }
1076
+ });
1077
+ }
1078
+
1079
+ // src/transformer/transformers/AnthropicOpenAIToAnthropicStream.ts
1080
+ function convertOpenAIStreamToAnthropic2(openaiStream, _context, logger) {
1081
+ const decoder = new TextDecoder();
1082
+ const encoder = new TextEncoder();
1083
+ let hasStarted = false;
1084
+ let hasTextContentStarted = false;
1085
+ let isThinkingStarted = false;
1086
+ let contentIndex = 0;
1087
+ let currentContentBlockIndex = -1;
1088
+ const toolCallIndexToContentBlockIndex = /* @__PURE__ */ new Map();
1089
+ return new ReadableStream({
1090
+ start: async (controller) => {
1091
+ const reader = openaiStream.getReader();
1092
+ let buffer = "";
1093
+ const messageId = `msg_${Date.now()}`;
1094
+ let model = "unknown";
1095
+ let isClosed = false;
1096
+ let stopReasonDelta = null;
1097
+ const safeEnqueue = (data) => {
1098
+ if (!isClosed) {
1099
+ try {
1100
+ controller.enqueue(encoder.encode(data));
1101
+ } catch (_e) {
1102
+ isClosed = true;
1103
+ }
1104
+ }
1105
+ };
1106
+ const assignContentBlockIndex = () => {
1107
+ return contentIndex++;
1108
+ };
1109
+ const safeClose = () => {
1110
+ if (isClosed) return;
1111
+ if (currentContentBlockIndex >= 0) {
1112
+ safeEnqueue(`event: content_block_stop
1113
+ data: ${JSON.stringify({
1114
+ type: "content_block_stop",
1115
+ index: currentContentBlockIndex
1116
+ })}
1117
+
1118
+ `);
1119
+ }
1120
+ if (stopReasonDelta) {
1121
+ safeEnqueue(`event: message_delta
1122
+ data: ${JSON.stringify(stopReasonDelta)}
1123
+
1124
+ `);
1125
+ } else {
1126
+ safeEnqueue(`event: message_delta
1127
+ data: ${JSON.stringify({
1128
+ type: "message_delta",
1129
+ delta: { stop_reason: "end_turn", stop_sequence: null },
1130
+ usage: { input_tokens: 0, output_tokens: 0 }
1131
+ })}
1132
+
1133
+ `);
1134
+ }
1135
+ safeEnqueue(`event: message_stop
1136
+ data: ${JSON.stringify({ type: "message_stop" })}
1137
+
1138
+ `);
1139
+ try {
1140
+ controller.close();
1141
+ } catch (_e) {
1142
+ }
1143
+ isClosed = true;
1144
+ };
1145
+ try {
1146
+ while (true) {
1147
+ const { done, value } = await reader.read();
1148
+ if (done) break;
1149
+ buffer += decoder.decode(value, { stream: true });
1150
+ const lines = buffer.split("\n");
1151
+ buffer = lines.pop() || "";
1152
+ for (const line of lines) {
1153
+ if (isClosed) break;
1154
+ if (!line.startsWith("data:")) continue;
1155
+ const data = line.slice(5).trim();
1156
+ if (data === "[DONE]") continue;
1157
+ try {
1158
+ const chunk = JSON.parse(data);
1159
+ if (chunk.error) {
1160
+ safeEnqueue(`event: error
1161
+ data: ${JSON.stringify({
1162
+ type: "error",
1163
+ message: { type: "api_error", message: JSON.stringify(chunk.error) }
1164
+ })}
1165
+
1166
+ `);
1167
+ continue;
1168
+ }
1169
+ model = chunk.model || model;
1170
+ if (!hasStarted) {
1171
+ hasStarted = true;
1172
+ safeEnqueue(`event: message_start
1173
+ data: ${JSON.stringify({
1174
+ type: "message_start",
1175
+ message: {
1176
+ id: messageId,
1177
+ type: "message",
1178
+ role: "assistant",
1179
+ content: [],
1180
+ model,
1181
+ stop_reason: null,
1182
+ stop_sequence: null,
1183
+ usage: { input_tokens: 0, output_tokens: 0 }
1184
+ }
1185
+ })}
1186
+
1187
+ `);
1188
+ }
1189
+ const choice = chunk.choices?.[0];
1190
+ if (!choice) continue;
1191
+ if (chunk.usage) {
1192
+ stopReasonDelta = {
1193
+ type: "message_delta",
1194
+ delta: { stop_reason: "end_turn", stop_sequence: null },
1195
+ usage: {
1196
+ input_tokens: (chunk.usage.prompt_tokens || 0) - (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
1197
+ output_tokens: chunk.usage.completion_tokens || 0,
1198
+ cache_read_input_tokens: chunk.usage.prompt_tokens_details?.cached_tokens || 0
1199
+ }
1200
+ };
1201
+ }
1202
+ if (choice.delta?.thinking) {
1203
+ if (!isThinkingStarted) {
1204
+ const thinkingBlockIndex = assignContentBlockIndex();
1205
+ safeEnqueue(`event: content_block_start
1206
+ data: ${JSON.stringify({
1207
+ type: "content_block_start",
1208
+ index: thinkingBlockIndex,
1209
+ content_block: { type: "thinking", thinking: "" }
1210
+ })}
1211
+
1212
+ `);
1213
+ currentContentBlockIndex = thinkingBlockIndex;
1214
+ isThinkingStarted = true;
1215
+ }
1216
+ if (choice.delta.thinking.signature) {
1217
+ safeEnqueue(`event: content_block_delta
1218
+ data: ${JSON.stringify({
1219
+ type: "content_block_delta",
1220
+ index: currentContentBlockIndex,
1221
+ delta: { type: "signature_delta", signature: choice.delta.thinking.signature }
1222
+ })}
1223
+
1224
+ `);
1225
+ safeEnqueue(`event: content_block_stop
1226
+ data: ${JSON.stringify({
1227
+ type: "content_block_stop",
1228
+ index: currentContentBlockIndex
1229
+ })}
1230
+
1231
+ `);
1232
+ currentContentBlockIndex = -1;
1233
+ } else if (choice.delta.thinking.content) {
1234
+ safeEnqueue(`event: content_block_delta
1235
+ data: ${JSON.stringify({
1236
+ type: "content_block_delta",
1237
+ index: currentContentBlockIndex,
1238
+ delta: { type: "thinking_delta", thinking: choice.delta.thinking.content }
1239
+ })}
1240
+
1241
+ `);
1242
+ }
1243
+ }
1244
+ if (choice.delta?.content) {
1245
+ if (!hasTextContentStarted) {
1246
+ if (currentContentBlockIndex >= 0 && isThinkingStarted) {
1247
+ safeEnqueue(`event: content_block_stop
1248
+ data: ${JSON.stringify({
1249
+ type: "content_block_stop",
1250
+ index: currentContentBlockIndex
1251
+ })}
1252
+
1253
+ `);
1254
+ }
1255
+ hasTextContentStarted = true;
1256
+ const textBlockIndex = assignContentBlockIndex();
1257
+ safeEnqueue(`event: content_block_start
1258
+ data: ${JSON.stringify({
1259
+ type: "content_block_start",
1260
+ index: textBlockIndex,
1261
+ content_block: { type: "text", text: "" }
1262
+ })}
1263
+
1264
+ `);
1265
+ currentContentBlockIndex = textBlockIndex;
1266
+ }
1267
+ safeEnqueue(`event: content_block_delta
1268
+ data: ${JSON.stringify({
1269
+ type: "content_block_delta",
1270
+ index: currentContentBlockIndex,
1271
+ delta: { type: "text_delta", text: choice.delta.content }
1272
+ })}
1273
+
1274
+ `);
1275
+ }
1276
+ if (choice.delta?.tool_calls) {
1277
+ for (const toolCall of choice.delta.tool_calls) {
1278
+ const toolCallIndex = toolCall.index ?? 0;
1279
+ if (!toolCallIndexToContentBlockIndex.has(toolCallIndex)) {
1280
+ if (currentContentBlockIndex >= 0) {
1281
+ safeEnqueue(`event: content_block_stop
1282
+ data: ${JSON.stringify({
1283
+ type: "content_block_stop",
1284
+ index: currentContentBlockIndex
1285
+ })}
1286
+
1287
+ `);
1288
+ hasTextContentStarted = false;
1289
+ }
1290
+ const newBlockIndex = assignContentBlockIndex();
1291
+ toolCallIndexToContentBlockIndex.set(toolCallIndex, newBlockIndex);
1292
+ safeEnqueue(`event: content_block_start
1293
+ data: ${JSON.stringify({
1294
+ type: "content_block_start",
1295
+ index: newBlockIndex,
1296
+ content_block: {
1297
+ type: "tool_use",
1298
+ id: toolCall.id || `call_${Date.now()}_${toolCallIndex}`,
1299
+ name: toolCall.function?.name || `tool_${toolCallIndex}`,
1300
+ input: {}
1301
+ }
1302
+ })}
1303
+
1304
+ `);
1305
+ currentContentBlockIndex = newBlockIndex;
1306
+ }
1307
+ if (toolCall.function?.arguments) {
1308
+ const blockIndex = toolCallIndexToContentBlockIndex.get(toolCallIndex);
1309
+ if (blockIndex !== void 0) {
1310
+ safeEnqueue(`event: content_block_delta
1311
+ data: ${JSON.stringify({
1312
+ type: "content_block_delta",
1313
+ index: blockIndex,
1314
+ delta: { type: "input_json_delta", partial_json: toolCall.function.arguments }
1315
+ })}
1316
+
1317
+ `);
1318
+ }
1319
+ }
1320
+ }
1321
+ }
1322
+ if (choice.finish_reason) {
1323
+ const stopReasonMapping = {
1324
+ stop: "end_turn",
1325
+ length: "max_tokens",
1326
+ tool_calls: "tool_use",
1327
+ content_filter: "stop_sequence"
1328
+ };
1329
+ stopReasonDelta = {
1330
+ type: "message_delta",
1331
+ delta: {
1332
+ stop_reason: stopReasonMapping[choice.finish_reason] || "end_turn",
1333
+ stop_sequence: null
1334
+ },
1335
+ usage: {
1336
+ input_tokens: (chunk.usage?.prompt_tokens || 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens || 0),
1337
+ output_tokens: chunk.usage?.completion_tokens || 0,
1338
+ cache_read_input_tokens: chunk.usage?.prompt_tokens_details?.cached_tokens || 0
1339
+ }
1340
+ };
1341
+ }
1342
+ } catch (e) {
1343
+ logger?.error("Error parsing stream chunk:", e);
1344
+ }
1345
+ }
1346
+ }
1347
+ } catch (e) {
1348
+ if (!isClosed) {
1349
+ controller.error(e);
1350
+ }
1351
+ } finally {
1352
+ safeClose();
1353
+ reader.releaseLock();
1354
+ }
1355
+ }
1356
+ });
1357
+ }
1358
+
1359
+ // src/transformer/transformers/AnthropicTransformer.ts
1360
+ var AnthropicTransformer = class {
1361
+ static TransformerName = "anthropic";
1362
+ name = "anthropic";
1363
+ logger;
1364
+ endPoint = "/v1/messages";
1365
+ useBearer;
1366
+ constructor(options) {
1367
+ this.useBearer = options?.UseBearer ?? false;
1368
+ }
1369
+ /**
1370
+ * Handle authentication - Anthropic uses x-api-key header
1371
+ */
1372
+ async auth(request, provider, _context) {
1373
+ const headers = {};
1374
+ if (this.useBearer) {
1375
+ headers["authorization"] = `Bearer ${provider.apiKey}`;
1376
+ headers["x-api-key"] = void 0;
1377
+ } else {
1378
+ headers["x-api-key"] = provider.apiKey;
1379
+ headers["authorization"] = void 0;
1380
+ }
1381
+ return {
1382
+ body: request,
1383
+ config: { headers }
1384
+ };
1385
+ }
1386
+ /**
1387
+ * Transform Anthropic request to unified format.
1388
+ */
1389
+ async transformRequestOut(request, _context) {
1390
+ return transformAnthropicRequestToUnified(request);
1391
+ }
1392
+ /**
1393
+ * Transform OpenAI/unified response back to Anthropic format
1394
+ * (auto-detects stream vs JSON via Content-Type).
1395
+ */
1396
+ async transformResponseIn(response, context) {
1397
+ const contentType = response.headers.get("Content-Type") ?? "";
1398
+ if (contentType.includes("text/event-stream")) {
1399
+ if (!response.body) {
1400
+ throw new Error("Stream response body is null");
1401
+ }
1402
+ const convertedStream = convertOpenAIStreamToAnthropic2(response.body, context, this.logger);
1403
+ return new Response(convertedStream, {
1404
+ headers: {
1405
+ "Content-Type": "text/event-stream",
1406
+ "Cache-Control": "no-cache",
1407
+ Connection: "keep-alive"
1408
+ }
1409
+ });
1410
+ } else {
1411
+ const data = await response.json();
1412
+ const anthropicResponse = convertOpenAIResponseToAnthropic2(data);
1413
+ return new Response(JSON.stringify(anthropicResponse), {
1414
+ headers: { "Content-Type": "application/json" }
1415
+ });
1416
+ }
1417
+ }
1418
+ /**
1419
+ * Transform unified request to Anthropic Messages API format.
1420
+ * This is the reverse of transformRequestOut — converts OpenAI/unified format
1421
+ * to Anthropic's expected request body structure.
1422
+ */
1423
+ async transformRequestIn(request, _provider, _context) {
1424
+ return buildAnthropicRequestBody(request);
1425
+ }
1426
+ /**
1427
+ * Transform Anthropic response to OpenAI/unified format
1428
+ * (auto-detects stream vs JSON via Content-Type).
1429
+ */
1430
+ async transformResponseOut(response, _context) {
1431
+ const contentType = response.headers.get("Content-Type") ?? "";
1432
+ if (contentType.includes("text/event-stream")) {
1433
+ if (!response.body) {
1434
+ throw new Error("Stream response body is null");
1435
+ }
1436
+ const convertedStream = convertAnthropicStreamToOpenAI2(response.body, this.logger);
1437
+ return new Response(convertedStream, {
1438
+ headers: {
1439
+ "Content-Type": "text/event-stream",
1440
+ "Cache-Control": "no-cache",
1441
+ Connection: "keep-alive"
1442
+ }
1443
+ });
1444
+ } else {
1445
+ const data = await response.json();
1446
+ const openaiResponse = convertAnthropicResponseToOpenAI(data);
1447
+ return new Response(JSON.stringify(openaiResponse), {
1448
+ headers: { "Content-Type": "application/json" }
1449
+ });
1450
+ }
1451
+ }
1452
+ };
1453
+
1454
+ // src/transformer/transformers/utils/gemini.response-in.ts
1455
+ var FINISH_REASON_TO_GEMINI = {
1456
+ stop: "STOP",
1457
+ length: "MAX_TOKENS",
1458
+ tool_calls: "STOP",
1459
+ content_filter: "SAFETY",
1460
+ // Already-lowercased Gemini reasons (pass-through from another transformer)
1461
+ max_tokens: "MAX_TOKENS",
1462
+ safety: "SAFETY"
1463
+ };
1464
+ function toGeminiFinishReason(openaiReason) {
1465
+ if (!openaiReason) return null;
1466
+ return FINISH_REASON_TO_GEMINI[openaiReason] || openaiReason.toUpperCase();
1467
+ }
1468
+ function convertOpenAIResponseToGemini(openaiData) {
1469
+ const choice = openaiData.choices?.[0];
1470
+ const message = choice?.message ?? {};
1471
+ const usage = openaiData.usage;
1472
+ const usagePromptDetails = usage?.prompt_tokens_details;
1473
+ const usageOutputDetails = usage?.output_tokens_details;
1474
+ const parts = [];
1475
+ const thinking = message.thinking;
1476
+ if (thinking?.content) {
1477
+ parts.push({ text: thinking.content, thought: true });
1478
+ }
1479
+ if (thinking?.signature) {
1480
+ parts.push({ thoughtSignature: thinking.signature });
1481
+ }
1482
+ if (message.content) {
1483
+ parts.push({ text: message.content });
1484
+ }
1485
+ const toolCalls = message.tool_calls;
1486
+ if (toolCalls?.length) {
1487
+ for (const tc of toolCalls) {
1488
+ const func = tc.function;
1489
+ let args = {};
1490
+ try {
1491
+ const raw = func.arguments;
1492
+ args = typeof raw === "string" ? JSON.parse(raw) : raw;
1493
+ } catch {
1494
+ args = {};
1495
+ }
1496
+ parts.push({
1497
+ functionCall: {
1498
+ id: tc.id,
1499
+ name: func.name,
1500
+ args
1501
+ }
1502
+ });
1503
+ }
1504
+ }
1505
+ if (parts.length === 0) {
1506
+ parts.push({ text: "" });
1507
+ }
1508
+ return {
1509
+ responseId: openaiData.id || "",
1510
+ modelVersion: openaiData.model || "",
1511
+ candidates: [{
1512
+ content: { parts },
1513
+ finishReason: toGeminiFinishReason(choice?.finish_reason)
1514
+ }],
1515
+ usageMetadata: usage ? {
1516
+ promptTokenCount: usage.prompt_tokens || 0,
1517
+ candidatesTokenCount: usage.completion_tokens || 0,
1518
+ totalTokenCount: usage.total_tokens || 0,
1519
+ cachedContentTokenCount: usagePromptDetails?.cached_tokens || 0,
1520
+ thoughtsTokenCount: usageOutputDetails?.reasoning_tokens || 0
1521
+ } : void 0
1522
+ };
1523
+ }
1524
+ function convertOpenAIStreamToGemini(openaiStream, logger) {
1525
+ const decoder = new TextDecoder();
1526
+ const encoder = new TextEncoder();
1527
+ const pendingToolCalls = /* @__PURE__ */ new Map();
1528
+ let model = "";
1529
+ let responseId = "";
1530
+ return new ReadableStream({
1531
+ start: async (controller) => {
1532
+ const reader = openaiStream.getReader();
1533
+ let buffer = "";
1534
+ let isClosed = false;
1535
+ const emit2 = (data) => {
1536
+ if (isClosed) return;
1537
+ try {
1538
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
1539
+
1540
+ `));
1541
+ } catch {
1542
+ isClosed = true;
1543
+ }
1544
+ };
1545
+ const flushToolCalls = () => {
1546
+ if (pendingToolCalls.size === 0) return;
1547
+ const parts = [];
1548
+ for (const tc of pendingToolCalls.values()) {
1549
+ let args = {};
1550
+ try {
1551
+ args = JSON.parse(tc.args || "{}");
1552
+ } catch {
1553
+ }
1554
+ parts.push({
1555
+ functionCall: { id: tc.id, name: tc.name, args }
1556
+ });
1557
+ }
1558
+ pendingToolCalls.clear();
1559
+ emit2({
1560
+ responseId,
1561
+ modelVersion: model,
1562
+ candidates: [{ content: { parts }, finishReason: null }]
1563
+ });
1564
+ };
1565
+ try {
1566
+ while (true) {
1567
+ const { done, value } = await reader.read();
1568
+ if (done) break;
1569
+ buffer += decoder.decode(value, { stream: true });
1570
+ const lines = buffer.split("\n");
1571
+ buffer = lines.pop() || "";
1572
+ for (const line of lines) {
1573
+ if (isClosed) break;
1574
+ if (!line.startsWith("data:")) continue;
1575
+ const data = line.slice(5).trim();
1576
+ if (!data || data === "[DONE]") continue;
1577
+ try {
1578
+ const chunk = JSON.parse(data);
1579
+ if (!responseId && chunk.id) responseId = chunk.id;
1580
+ if (!model && chunk.model) model = chunk.model;
1581
+ const choice = chunk.choices?.[0];
1582
+ if (!choice) continue;
1583
+ const delta = choice.delta ?? {};
1584
+ if (delta.thinking) {
1585
+ const parts = [];
1586
+ if (delta.thinking.content) {
1587
+ parts.push({ text: delta.thinking.content, thought: true });
1588
+ }
1589
+ if (delta.thinking.signature) {
1590
+ parts.push({ thoughtSignature: delta.thinking.signature });
1591
+ }
1592
+ if (parts.length > 0) {
1593
+ emit2({
1594
+ responseId,
1595
+ modelVersion: model,
1596
+ candidates: [{ content: { parts }, finishReason: null }]
1597
+ });
1598
+ }
1599
+ }
1600
+ if (delta.content) {
1601
+ emit2({
1602
+ responseId,
1603
+ modelVersion: model,
1604
+ candidates: [{
1605
+ content: { parts: [{ text: delta.content }] },
1606
+ finishReason: null
1607
+ }]
1608
+ });
1609
+ }
1610
+ if (delta.tool_calls) {
1611
+ for (const tc of delta.tool_calls) {
1612
+ const idx = tc.index ?? 0;
1613
+ const existing = pendingToolCalls.get(idx);
1614
+ const func = tc.function;
1615
+ if (existing) {
1616
+ if (func?.arguments) {
1617
+ existing.args += func.arguments;
1618
+ }
1619
+ } else {
1620
+ pendingToolCalls.set(idx, {
1621
+ id: tc.id || `tool_${Date.now()}_${idx}`,
1622
+ name: func?.name || "",
1623
+ args: func?.arguments || ""
1624
+ });
1625
+ }
1626
+ }
1627
+ }
1628
+ if (choice.finish_reason) {
1629
+ flushToolCalls();
1630
+ const geminiUsage = chunk.usage ? {
1631
+ promptTokenCount: chunk.usage.prompt_tokens || 0,
1632
+ candidatesTokenCount: chunk.usage.completion_tokens || 0,
1633
+ totalTokenCount: chunk.usage.total_tokens || 0,
1634
+ cachedContentTokenCount: chunk.usage.prompt_tokens_details?.cached_tokens || 0,
1635
+ thoughtsTokenCount: chunk.usage.output_tokens_details?.reasoning_tokens || 0
1636
+ } : void 0;
1637
+ emit2({
1638
+ responseId,
1639
+ modelVersion: model,
1640
+ candidates: [{
1641
+ content: { parts: [{ text: "" }] },
1642
+ finishReason: toGeminiFinishReason(choice.finish_reason) || "STOP"
1643
+ }],
1644
+ usageMetadata: geminiUsage
1645
+ });
1646
+ }
1647
+ } catch (e) {
1648
+ logger?.error(`Error parsing OpenAI stream chunk for Gemini conversion: ${e}`);
1649
+ }
1650
+ }
1651
+ }
1652
+ flushToolCalls();
1653
+ } catch (e) {
1654
+ if (!isClosed) controller.error(e);
1655
+ } finally {
1656
+ if (!isClosed) {
1657
+ try {
1658
+ controller.close();
1659
+ } catch {
1660
+ }
1661
+ }
1662
+ reader.releaseLock();
1663
+ }
1664
+ }
1665
+ });
1666
+ }
1667
+ async function transformResponseIn(response, logger) {
1668
+ const contentType = response.headers.get("Content-Type") ?? "";
1669
+ if (contentType.includes("text/event-stream")) {
1670
+ if (!response.body) {
1671
+ throw new Error("Stream response body is null");
1672
+ }
1673
+ const geminiStream = convertOpenAIStreamToGemini(response.body, logger);
1674
+ return new Response(geminiStream, {
1675
+ headers: {
1676
+ "Content-Type": "text/event-stream",
1677
+ "Cache-Control": "no-cache",
1678
+ Connection: "keep-alive"
1679
+ }
1680
+ });
1681
+ }
1682
+ const data = await response.json();
1683
+ const geminiResponse = convertOpenAIResponseToGemini(data);
1684
+ return new Response(JSON.stringify(geminiResponse), {
1685
+ status: response.status,
1686
+ statusText: response.statusText,
1687
+ headers: { "Content-Type": "application/json" }
1688
+ });
1689
+ }
1690
+
1691
+ // src/transformer/transformers/utils/gemini.stream.ts
1692
+ async function transformResponseOut(response, providerName, logger) {
1693
+ const contentType = response.headers.get("Content-Type") ?? "";
1694
+ if (contentType.includes("application/json")) {
1695
+ return handleJsonResponse(response, providerName, logger);
1696
+ } else if (contentType.includes("stream") || contentType.includes("text/event-stream")) {
1697
+ return handleStreamResponse(response, providerName, logger);
1698
+ }
1699
+ return response;
1700
+ }
1701
+ async function handleJsonResponse(response, providerName, logger) {
1702
+ const jsonResponse = await response.json();
1703
+ logger?.debug(`${providerName} JSON response received`);
1704
+ const parts = jsonResponse.candidates?.[0]?.content?.parts || [];
1705
+ let thinkingContent = "";
1706
+ let thinkingSignature = "";
1707
+ const nonThinkingParts = [];
1708
+ for (const part of parts) {
1709
+ if (part.text && part.thought === true) {
1710
+ thinkingContent += part.text;
1711
+ } else {
1712
+ nonThinkingParts.push(part);
1713
+ }
1714
+ }
1715
+ thinkingSignature = parts.find((part) => part.thoughtSignature)?.thoughtSignature ?? "";
1716
+ const toolCalls = nonThinkingParts.filter((part) => part.functionCall).map((part) => ({
1717
+ id: part.functionCall?.id || `tool_${Math.random().toString(36).substring(2, 15)}`,
1718
+ type: "function",
1719
+ function: {
1720
+ name: part.functionCall?.name ?? "",
1721
+ arguments: JSON.stringify(part.functionCall?.args || {})
1722
+ }
1723
+ }));
1724
+ const textContent = nonThinkingParts.filter((part) => part.text).map((part) => part.text).join("\n");
1725
+ const openAIResponse = {
1726
+ id: jsonResponse.responseId ?? "",
1727
+ choices: [
1728
+ {
1729
+ finish_reason: (jsonResponse.candidates?.[0]?.finishReason ?? "").toLowerCase() || null,
1730
+ index: 0,
1731
+ message: {
1732
+ content: textContent,
1733
+ role: "assistant",
1734
+ tool_calls: toolCalls.length > 0 ? toolCalls : void 0,
1735
+ ...thinkingSignature && {
1736
+ thinking: {
1737
+ content: thinkingContent || "(no content)",
1738
+ signature: thinkingSignature
1739
+ }
1740
+ }
1741
+ }
1742
+ }
1743
+ ],
1744
+ created: Math.floor(Date.now() / 1e3),
1745
+ model: jsonResponse.modelVersion ?? "",
1746
+ object: "chat.completion",
1747
+ usage: {
1748
+ completion_tokens: jsonResponse.usageMetadata?.candidatesTokenCount || 0,
1749
+ prompt_tokens: jsonResponse.usageMetadata?.promptTokenCount || 0,
1750
+ prompt_tokens_details: {
1751
+ cached_tokens: jsonResponse.usageMetadata?.cachedContentTokenCount || 0
1752
+ },
1753
+ total_tokens: jsonResponse.usageMetadata?.totalTokenCount || 0,
1754
+ output_tokens_details: {
1755
+ reasoning_tokens: jsonResponse.usageMetadata?.thoughtsTokenCount || 0
1756
+ }
1757
+ }
1758
+ };
1759
+ return new Response(JSON.stringify(openAIResponse), {
1760
+ status: response.status,
1761
+ statusText: response.statusText,
1762
+ headers: response.headers
1763
+ });
1764
+ }
1765
+ function handleStreamResponse(response, providerName, logger) {
1766
+ if (!response.body) {
1767
+ return response;
1768
+ }
1769
+ const decoder = new TextDecoder();
1770
+ const encoder = new TextEncoder();
1771
+ let signatureSent = false;
1772
+ let contentSent = false;
1773
+ let hasThinkingContent = false;
1774
+ let pendingContent = "";
1775
+ let contentIndex = 0;
1776
+ let toolCallIndex = -1;
1777
+ const stream = new ReadableStream({
1778
+ async start(controller) {
1779
+ const reader = response.body.getReader();
1780
+ let buffer = "";
1781
+ const processLine = async (line) => {
1782
+ if (!line.startsWith("data: ")) return;
1783
+ const chunkStr = line.slice(6).trim();
1784
+ if (!chunkStr) return;
1785
+ logger?.debug(`${providerName} chunk: ${chunkStr.substring(0, 100)}...`);
1786
+ try {
1787
+ const chunk = JSON.parse(chunkStr);
1788
+ if (!chunk.candidates?.[0]) {
1789
+ logger?.debug("Invalid chunk structure");
1790
+ return;
1791
+ }
1792
+ const candidate = chunk.candidates[0];
1793
+ const parts = candidate.content?.parts || [];
1794
+ parts.filter((part) => part.text && part.thought === true).forEach((part) => {
1795
+ hasThinkingContent = true;
1796
+ const thinkingChunk = createChunk({
1797
+ responseId: chunk.responseId,
1798
+ modelVersion: chunk.modelVersion,
1799
+ contentIndex,
1800
+ delta: {
1801
+ role: "assistant",
1802
+ content: null,
1803
+ thinking: { content: part.text }
1804
+ }
1805
+ });
1806
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(thinkingChunk)}
1807
+
1808
+ `));
1809
+ });
1810
+ const signature = parts.find((part) => part.thoughtSignature)?.thoughtSignature;
1811
+ if (signature && !signatureSent) {
1812
+ if (!hasThinkingContent) {
1813
+ const thinkingChunk = createChunk({
1814
+ responseId: chunk.responseId,
1815
+ modelVersion: chunk.modelVersion,
1816
+ contentIndex,
1817
+ delta: {
1818
+ role: "assistant",
1819
+ content: null,
1820
+ thinking: { content: "" }
1821
+ }
1822
+ });
1823
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(thinkingChunk)}
1824
+
1825
+ `));
1826
+ }
1827
+ const signatureChunk = createChunk({
1828
+ responseId: chunk.responseId,
1829
+ modelVersion: chunk.modelVersion,
1830
+ contentIndex,
1831
+ delta: {
1832
+ role: "assistant",
1833
+ content: null,
1834
+ thinking: { signature }
1835
+ }
1836
+ });
1837
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(signatureChunk)}
1838
+
1839
+ `));
1840
+ signatureSent = true;
1841
+ contentIndex++;
1842
+ if (pendingContent) {
1843
+ const pendingChunk = createChunk({
1844
+ responseId: chunk.responseId,
1845
+ modelVersion: chunk.modelVersion,
1846
+ contentIndex,
1847
+ delta: { role: "assistant", content: pendingContent }
1848
+ });
1849
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(pendingChunk)}
1850
+
1851
+ `));
1852
+ pendingContent = "";
1853
+ contentSent = true;
1854
+ }
1855
+ }
1856
+ const toolCalls = parts.filter((part) => part.functionCall).map((part) => ({
1857
+ id: part.functionCall?.id || `ccr_tool_${Math.random().toString(36).substring(2, 15)}`,
1858
+ type: "function",
1859
+ function: {
1860
+ name: part.functionCall?.name ?? "",
1861
+ arguments: JSON.stringify(part.functionCall?.args || {})
1862
+ }
1863
+ }));
1864
+ const textContent = parts.filter((part) => part.text && part.thought !== true).map((part) => part.text).join("\n");
1865
+ if (!textContent && signatureSent && !contentSent) {
1866
+ contentSent = true;
1867
+ }
1868
+ if (hasThinkingContent && textContent && !signatureSent) {
1869
+ if (chunk.modelVersion?.includes("3")) {
1870
+ pendingContent += textContent;
1871
+ return;
1872
+ } else {
1873
+ const signatureChunk = createChunk({
1874
+ responseId: chunk.responseId,
1875
+ modelVersion: chunk.modelVersion,
1876
+ contentIndex,
1877
+ delta: {
1878
+ role: "assistant",
1879
+ content: null,
1880
+ thinking: { signature: `ccr_${Date.now()}` }
1881
+ }
1882
+ });
1883
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(signatureChunk)}
1884
+
1885
+ `));
1886
+ signatureSent = true;
1887
+ }
1888
+ }
1889
+ if (textContent) {
1890
+ if (!pendingContent) contentIndex++;
1891
+ const contentChunk = createChunk({
1892
+ responseId: chunk.responseId,
1893
+ modelVersion: chunk.modelVersion,
1894
+ contentIndex,
1895
+ delta: { role: "assistant", content: textContent },
1896
+ finishReason: candidate.finishReason,
1897
+ usageMetadata: chunk.usageMetadata,
1898
+ groundingMetadata: candidate.groundingMetadata
1899
+ });
1900
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(contentChunk)}
1901
+
1902
+ `));
1903
+ contentSent = true;
1904
+ }
1905
+ if (toolCalls.length > 0) {
1906
+ for (const tool of toolCalls) {
1907
+ contentIndex++;
1908
+ toolCallIndex++;
1909
+ const toolChunk = createChunk({
1910
+ responseId: chunk.responseId,
1911
+ modelVersion: chunk.modelVersion,
1912
+ contentIndex,
1913
+ delta: {
1914
+ role: "assistant",
1915
+ tool_calls: [{ ...tool, index: toolCallIndex }]
1916
+ },
1917
+ finishReason: candidate.finishReason,
1918
+ usageMetadata: chunk.usageMetadata,
1919
+ groundingMetadata: candidate.groundingMetadata
1920
+ });
1921
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(toolChunk)}
1922
+
1923
+ `));
1924
+ }
1925
+ contentSent = true;
1926
+ }
1927
+ } catch (_error) {
1928
+ logger?.error(`Error parsing ${providerName} stream chunk: ${chunkStr}`);
1929
+ }
1930
+ };
1931
+ try {
1932
+ while (true) {
1933
+ const { done, value } = await reader.read();
1934
+ if (done) {
1935
+ if (buffer) await processLine(buffer);
1936
+ break;
1937
+ }
1938
+ buffer += decoder.decode(value, { stream: true });
1939
+ const lines = buffer.split("\n");
1940
+ buffer = lines.pop() || "";
1941
+ for (const line of lines) {
1942
+ await processLine(line);
1943
+ }
1944
+ }
1945
+ } catch (error) {
1946
+ controller.error(error);
1947
+ } finally {
1948
+ controller.close();
1949
+ }
1950
+ }
1951
+ });
1952
+ return new Response(stream, {
1953
+ status: response.status,
1954
+ statusText: response.statusText,
1955
+ headers: response.headers
1956
+ });
1957
+ }
1958
+ function createChunk(options) {
1959
+ const {
1960
+ responseId,
1961
+ modelVersion,
1962
+ contentIndex,
1963
+ delta,
1964
+ finishReason,
1965
+ usageMetadata,
1966
+ groundingMetadata
1967
+ } = options;
1968
+ const chunk = {
1969
+ choices: [
1970
+ {
1971
+ delta,
1972
+ finish_reason: finishReason?.toLowerCase() || null,
1973
+ index: contentIndex,
1974
+ logprobs: null
1975
+ }
1976
+ ],
1977
+ created: Math.floor(Date.now() / 1e3),
1978
+ id: responseId || "",
1979
+ model: modelVersion || "",
1980
+ object: "chat.completion.chunk",
1981
+ system_fingerprint: "fp_a49d71b8a1"
1982
+ };
1983
+ if (usageMetadata) {
1984
+ chunk.usage = {
1985
+ completion_tokens: usageMetadata.candidatesTokenCount || 0,
1986
+ prompt_tokens: usageMetadata.promptTokenCount || 0,
1987
+ prompt_tokens_details: {
1988
+ cached_tokens: usageMetadata.cachedContentTokenCount || 0
1989
+ },
1990
+ total_tokens: usageMetadata.totalTokenCount || 0,
1991
+ output_tokens_details: {
1992
+ reasoning_tokens: usageMetadata.thoughtsTokenCount || 0
1993
+ }
1994
+ };
1995
+ }
1996
+ if (groundingMetadata?.groundingChunks?.length) {
1997
+ const annotations = groundingMetadata.groundingChunks.map((groundingChunk, index) => {
1998
+ const support = groundingMetadata.groundingSupports?.find(
1999
+ (s) => s.groundingChunkIndices?.includes(index)
2000
+ );
2001
+ return {
2002
+ type: "url_citation",
2003
+ url_citation: {
2004
+ url: groundingChunk.web?.uri || "",
2005
+ title: groundingChunk.web?.title || "",
2006
+ content: support?.segment?.text || "",
2007
+ start_index: support?.segment?.startIndex || 0,
2008
+ end_index: support?.segment?.endIndex || 0
2009
+ }
2010
+ };
2011
+ });
2012
+ chunk.choices[0].delta.annotations = annotations;
2013
+ }
2014
+ return chunk;
2015
+ }
2016
+
2017
+ // src/transformer/transformers/utils/gemini.schema.ts
2018
+ var GeminiType = {
2019
+ TYPE_UNSPECIFIED: "TYPE_UNSPECIFIED",
2020
+ STRING: "STRING",
2021
+ NUMBER: "NUMBER",
2022
+ INTEGER: "INTEGER",
2023
+ BOOLEAN: "BOOLEAN",
2024
+ ARRAY: "ARRAY",
2025
+ OBJECT: "OBJECT",
2026
+ NULL: "NULL"
2027
+ };
2028
+ function flattenTypeArrayToAnyOf(typeList, resultingSchema) {
2029
+ if (typeList.includes("null")) {
2030
+ resultingSchema.nullable = true;
2031
+ }
2032
+ const listWithoutNull = typeList.filter((type) => type !== "null");
2033
+ if (listWithoutNull.length === 1) {
2034
+ const upperCaseType = listWithoutNull[0].toUpperCase();
2035
+ resultingSchema.type = Object.values(GeminiType).includes(upperCaseType) ? upperCaseType : GeminiType.TYPE_UNSPECIFIED;
2036
+ } else {
2037
+ resultingSchema.anyOf = listWithoutNull.map((typeName) => {
2038
+ const upperCaseType = typeName.toUpperCase();
2039
+ return {
2040
+ type: Object.values(GeminiType).includes(upperCaseType) ? upperCaseType : GeminiType.TYPE_UNSPECIFIED
2041
+ };
2042
+ });
2043
+ }
2044
+ }
2045
+ function processJsonSchema(jsonSchema) {
2046
+ const genAISchema = {};
2047
+ const schemaFieldNames = ["items"];
2048
+ const listSchemaFieldNames = ["anyOf"];
2049
+ const dictSchemaFieldNames = ["properties"];
2050
+ let workingSchema = jsonSchema;
2051
+ if (workingSchema.type && workingSchema.anyOf) {
2052
+ throw new Error("type and anyOf cannot be both populated.");
2053
+ }
2054
+ const incomingAnyOf = workingSchema.anyOf;
2055
+ if (incomingAnyOf && Array.isArray(incomingAnyOf) && incomingAnyOf.length === 2) {
2056
+ if (incomingAnyOf[0]?.type === "null") {
2057
+ genAISchema.nullable = true;
2058
+ workingSchema = incomingAnyOf[1];
2059
+ } else if (incomingAnyOf[1]?.type === "null") {
2060
+ genAISchema.nullable = true;
2061
+ workingSchema = incomingAnyOf[0];
2062
+ }
2063
+ }
2064
+ if (workingSchema.type && Array.isArray(workingSchema.type)) {
2065
+ flattenTypeArrayToAnyOf(workingSchema.type, genAISchema);
2066
+ }
2067
+ for (const [fieldName, fieldValue] of Object.entries(workingSchema)) {
2068
+ if (fieldValue == null) {
2069
+ continue;
2070
+ }
2071
+ if (fieldName === "type") {
2072
+ if (fieldValue === "null") {
2073
+ throw new Error("type: null cannot be the only possible type for the field.");
2074
+ }
2075
+ if (Array.isArray(fieldValue)) {
2076
+ continue;
2077
+ }
2078
+ const upperCaseValue = fieldValue.toUpperCase();
2079
+ genAISchema.type = Object.values(GeminiType).includes(upperCaseValue) ? upperCaseValue : GeminiType.TYPE_UNSPECIFIED;
2080
+ } else if (schemaFieldNames.includes(fieldName)) {
2081
+ genAISchema[fieldName] = processJsonSchema(fieldValue);
2082
+ } else if (listSchemaFieldNames.includes(fieldName)) {
2083
+ const listValue = [];
2084
+ for (const item of fieldValue) {
2085
+ if (item.type === "null") {
2086
+ genAISchema.nullable = true;
2087
+ continue;
2088
+ }
2089
+ listValue.push(processJsonSchema(item));
2090
+ }
2091
+ genAISchema[fieldName] = listValue;
2092
+ } else if (dictSchemaFieldNames.includes(fieldName)) {
2093
+ const dictValue = {};
2094
+ for (const [key, value] of Object.entries(fieldValue)) {
2095
+ dictValue[key] = processJsonSchema(value);
2096
+ }
2097
+ genAISchema[fieldName] = dictValue;
2098
+ } else {
2099
+ if (fieldName === "additionalProperties") {
2100
+ continue;
2101
+ }
2102
+ genAISchema[fieldName] = fieldValue;
2103
+ }
2104
+ }
2105
+ return genAISchema;
2106
+ }
2107
+ function transformTool(tool) {
2108
+ const functionDeclarations = tool.functionDeclarations;
2109
+ if (functionDeclarations) {
2110
+ for (const functionDeclaration of functionDeclarations) {
2111
+ if (functionDeclaration.parameters) {
2112
+ const params = functionDeclaration.parameters;
2113
+ if (!Object.keys(params).includes("$schema")) {
2114
+ functionDeclaration.parameters = processJsonSchema(params);
2115
+ } else {
2116
+ if (!functionDeclaration.parametersJsonSchema) {
2117
+ functionDeclaration.parametersJsonSchema = functionDeclaration.parameters;
2118
+ delete functionDeclaration.parameters;
2119
+ }
2120
+ }
2121
+ }
2122
+ if (functionDeclaration.response) {
2123
+ const response = functionDeclaration.response;
2124
+ if (!Object.keys(response).includes("$schema")) {
2125
+ functionDeclaration.response = processJsonSchema(response);
2126
+ } else {
2127
+ if (!functionDeclaration.responseJsonSchema) {
2128
+ functionDeclaration.responseJsonSchema = functionDeclaration.response;
2129
+ delete functionDeclaration.response;
2130
+ }
2131
+ }
2132
+ }
2133
+ }
2134
+ }
2135
+ return tool;
2136
+ }
2137
+
2138
+ // src/transformer/transformers/utils/gemini.util.ts
2139
+ function buildRequestBody(request) {
2140
+ const tools = [];
2141
+ const functionDeclarations = request.tools?.filter((tool) => tool.function.name !== "web_search")?.map((tool) => ({
2142
+ name: tool.function.name,
2143
+ description: tool.function.description,
2144
+ parametersJsonSchema: tool.function.parameters
2145
+ }));
2146
+ if (functionDeclarations?.length) {
2147
+ tools.push(
2148
+ transformTool({
2149
+ functionDeclarations
2150
+ })
2151
+ );
2152
+ }
2153
+ const webSearch = request.tools?.find((tool) => tool.function.name === "web_search");
2154
+ if (webSearch) {
2155
+ tools.push({ googleSearch: {} });
2156
+ }
2157
+ const contents = [];
2158
+ const toolResponses = request.messages.filter((item) => item.role === "tool");
2159
+ request.messages.filter((item) => item.role !== "tool").forEach((message) => {
2160
+ let role;
2161
+ if (message.role === "assistant") {
2162
+ role = "model";
2163
+ } else if (["user", "system"].includes(message.role)) {
2164
+ role = "user";
2165
+ } else {
2166
+ role = "user";
2167
+ }
2168
+ const parts = [];
2169
+ if (typeof message.content === "string") {
2170
+ const part = { text: message.content };
2171
+ if (message.thinking?.signature) {
2172
+ part.thoughtSignature = message.thinking.signature;
2173
+ }
2174
+ parts.push(part);
2175
+ } else if (Array.isArray(message.content)) {
2176
+ for (const content of message.content) {
2177
+ if (content.type === "text") {
2178
+ parts.push({ text: content.text || "" });
2179
+ } else if (content.type === "image_url") {
2180
+ const imageUrl = content.image_url?.url ?? "";
2181
+ if (imageUrl.startsWith("http")) {
2182
+ parts.push({
2183
+ file_data: {
2184
+ mime_type: content.media_type,
2185
+ file_uri: imageUrl
2186
+ }
2187
+ });
2188
+ } else {
2189
+ const data = imageUrl.split(",").pop() || imageUrl;
2190
+ parts.push({
2191
+ inlineData: {
2192
+ mime_type: content.media_type || "image/png",
2193
+ data
2194
+ }
2195
+ });
2196
+ }
2197
+ }
2198
+ }
2199
+ } else if (message.content && typeof message.content === "object") {
2200
+ const contentObj = message.content;
2201
+ if (contentObj.text) {
2202
+ parts.push({ text: contentObj.text });
2203
+ } else {
2204
+ parts.push({ text: JSON.stringify(message.content) });
2205
+ }
2206
+ }
2207
+ if (Array.isArray(message.tool_calls)) {
2208
+ for (let index = 0; index < message.tool_calls.length; index++) {
2209
+ const toolCall = message.tool_calls[index];
2210
+ const functionCallPart = {
2211
+ functionCall: {
2212
+ id: toolCall.id || `tool_${Math.random().toString(36).substring(2, 15)}`,
2213
+ name: toolCall.function.name,
2214
+ args: JSON.parse(toolCall.function.arguments || "{}")
2215
+ }
2216
+ };
2217
+ if (index === 0 && message.thinking?.signature) {
2218
+ functionCallPart.thoughtSignature = message.thinking.signature;
2219
+ }
2220
+ parts.push(functionCallPart);
2221
+ }
2222
+ }
2223
+ if (parts.length === 0) {
2224
+ parts.push({ text: "" });
2225
+ }
2226
+ contents.push({ role, parts });
2227
+ if (role === "model" && message.tool_calls) {
2228
+ const functionResponses = message.tool_calls.map(
2229
+ (tool) => {
2230
+ const response = toolResponses.find((item) => item.tool_call_id === tool.id);
2231
+ return {
2232
+ functionResponse: {
2233
+ name: tool.function?.name ?? "",
2234
+ response: { result: response?.content }
2235
+ }
2236
+ };
2237
+ }
2238
+ );
2239
+ contents.push({
2240
+ role: "user",
2241
+ parts: functionResponses
2242
+ });
2243
+ }
2244
+ });
2245
+ const generationConfig = {};
2246
+ if (request.reasoning?.effort && request.reasoning.effort !== "none") {
2247
+ generationConfig.thinkingConfig = {
2248
+ includeThoughts: true
2249
+ };
2250
+ if (request.model.includes("gemini-3")) {
2251
+ generationConfig.thinkingConfig.thinkingLevel = request.reasoning.effort;
2252
+ } else {
2253
+ const thinkingBudgets = request.model.includes("pro") ? [128, 32768] : [0, 24576];
2254
+ const maxTokens = request.reasoning.max_tokens;
2255
+ if (typeof maxTokens !== "undefined") {
2256
+ let thinkingBudget;
2257
+ if (maxTokens >= thinkingBudgets[0] && maxTokens <= thinkingBudgets[1]) {
2258
+ thinkingBudget = maxTokens;
2259
+ } else if (maxTokens < thinkingBudgets[0]) {
2260
+ thinkingBudget = thinkingBudgets[0];
2261
+ } else {
2262
+ thinkingBudget = thinkingBudgets[1];
2263
+ }
2264
+ generationConfig.thinkingConfig.thinkingBudget = thinkingBudget;
2265
+ }
2266
+ }
2267
+ }
2268
+ const body = {
2269
+ contents,
2270
+ tools: tools.length > 0 ? tools : void 0,
2271
+ generationConfig: Object.keys(generationConfig).length > 0 ? generationConfig : void 0
2272
+ };
2273
+ if (request.tool_choice) {
2274
+ const toolConfig = {
2275
+ functionCallingConfig: {}
2276
+ };
2277
+ if (request.tool_choice === "auto") {
2278
+ toolConfig.functionCallingConfig.mode = "auto";
2279
+ } else if (request.tool_choice === "none") {
2280
+ toolConfig.functionCallingConfig.mode = "none";
2281
+ } else if (request.tool_choice === "required") {
2282
+ toolConfig.functionCallingConfig.mode = "any";
2283
+ } else if (typeof request.tool_choice === "object" && request.tool_choice.function?.name) {
2284
+ toolConfig.functionCallingConfig.mode = "any";
2285
+ toolConfig.functionCallingConfig.allowedFunctionNames = [
2286
+ request.tool_choice.function.name
2287
+ ];
2288
+ }
2289
+ body.toolConfig = toolConfig;
2290
+ }
2291
+ return body;
2292
+ }
2293
+ function transformRequestOut(request) {
2294
+ const contents = request.contents;
2295
+ const tools = request.tools;
2296
+ const model = request.model;
2297
+ const maxTokens = request.max_tokens;
2298
+ const temperature = request.temperature;
2299
+ const stream = request.stream;
2300
+ const toolChoice = request.tool_choice;
2301
+ const unifiedRequest = {
2302
+ messages: [],
2303
+ model,
2304
+ max_tokens: maxTokens,
2305
+ temperature,
2306
+ stream,
2307
+ tool_choice: toolChoice
2308
+ };
2309
+ if (Array.isArray(contents)) {
2310
+ for (const content of contents) {
2311
+ if (typeof content === "string") {
2312
+ unifiedRequest.messages.push({
2313
+ role: "user",
2314
+ content
2315
+ });
2316
+ } else if ("text" in content && typeof content.text === "string") {
2317
+ unifiedRequest.messages.push({
2318
+ role: "user",
2319
+ content: content.text || null
2320
+ });
2321
+ } else if ("role" in content && content.role === "user") {
2322
+ const geminiContent = content;
2323
+ unifiedRequest.messages.push({
2324
+ role: "user",
2325
+ content: geminiContent.parts?.map((part) => ({
2326
+ type: "text",
2327
+ text: part.text || ""
2328
+ })) || []
2329
+ });
2330
+ } else if (content.role === "model") {
2331
+ unifiedRequest.messages.push({
2332
+ role: "assistant",
2333
+ content: content.parts?.map((part) => ({
2334
+ type: "text",
2335
+ text: part.text || ""
2336
+ })) || []
2337
+ });
2338
+ }
2339
+ }
2340
+ }
2341
+ if (Array.isArray(tools)) {
2342
+ unifiedRequest.tools = [];
2343
+ for (const tool of tools) {
2344
+ if (Array.isArray(tool.functionDeclarations)) {
2345
+ for (const funcDecl of tool.functionDeclarations) {
2346
+ unifiedRequest.tools.push({
2347
+ type: "function",
2348
+ function: {
2349
+ name: funcDecl.name,
2350
+ description: funcDecl.description ?? "",
2351
+ parameters: funcDecl.parameters ?? {}
2352
+ }
2353
+ });
2354
+ }
2355
+ }
2356
+ }
2357
+ }
2358
+ return unifiedRequest;
2359
+ }
2360
+
2361
+ // src/transformer/transformers/GeminiTransformer.ts
2362
+ var GeminiTransformer = class {
2363
+ static TransformerName = "gemini";
2364
+ name = "gemini";
2365
+ logger;
2366
+ /**
2367
+ * API endpoint pattern for Gemini
2368
+ * :modelAndAction will be replaced with actual model and action
2369
+ */
2370
+ endPoint = "/v1beta/models/:modelAndAction";
2371
+ /** Use Bearer token instead of x-goog-api-key (for relay providers) */
2372
+ useBearer;
2373
+ constructor(options) {
2374
+ this.useBearer = options?.UseBearer ?? false;
2375
+ }
2376
+ /**
2377
+ * Handle authentication
2378
+ * - Official Gemini: x-goog-api-key header
2379
+ * - Relay providers: Authorization: Bearer header
2380
+ */
2381
+ async auth(request, provider, _context) {
2382
+ const headers = {};
2383
+ if (this.useBearer) {
2384
+ headers["authorization"] = `Bearer ${provider.apiKey}`;
2385
+ headers["x-goog-api-key"] = void 0;
2386
+ } else {
2387
+ headers["x-goog-api-key"] = provider.apiKey;
2388
+ headers["authorization"] = void 0;
2389
+ }
2390
+ return {
2391
+ body: request,
2392
+ config: { headers }
2393
+ };
2394
+ }
2395
+ /**
2396
+ * Transform request from unified format to Gemini format
2397
+ * Also builds the correct URL for the Gemini API
2398
+ */
2399
+ async transformRequestIn(request, provider, _context) {
2400
+ const body = buildRequestBody(request);
2401
+ const action = request.stream ? "streamGenerateContent?alt=sse" : "generateContent";
2402
+ const url = new URL(`./${request.model}:${action}`, provider.baseUrl);
2403
+ const headers = {};
2404
+ if (this.useBearer) {
2405
+ headers["authorization"] = `Bearer ${provider.apiKey}`;
2406
+ headers["x-goog-api-key"] = void 0;
2407
+ } else {
2408
+ headers["x-goog-api-key"] = provider.apiKey;
2409
+ headers["Authorization"] = void 0;
2410
+ }
2411
+ return {
2412
+ body,
2413
+ config: { url, headers }
2414
+ };
2415
+ }
2416
+ /**
2417
+ * Transform incoming request to unified format
2418
+ * (For requests coming into the Gemini endpoint)
2419
+ */
2420
+ async transformRequestOut(request, _context) {
2421
+ return transformRequestOut(request);
2422
+ }
2423
+ /**
2424
+ * Transform Gemini response to OpenAI-compatible format
2425
+ */
2426
+ async transformResponseOut(response, _context) {
2427
+ return transformResponseOut(response, this.name, this.logger);
2428
+ }
2429
+ /**
2430
+ * Transform OpenAI-compatible response back to Gemini format
2431
+ * (For endpoint mode — returning Gemini-format responses to the client)
2432
+ */
2433
+ async transformResponseIn(response, _context) {
2434
+ return transformResponseIn(response, this.logger);
2435
+ }
2436
+ };
2437
+
2438
+ // src/transformer/transformers/OpenAIResponseTransformer.ts
2439
+ var OpenAIResponseTransformer = class {
2440
+ static TransformerName = "openai-response";
2441
+ name = "openai-response";
2442
+ endPoint = "/v1/responses";
2443
+ logger;
2444
+ /**
2445
+ * Handle authentication - Bearer token
2446
+ */
2447
+ async auth(request, provider, _context) {
2448
+ return {
2449
+ body: request,
2450
+ config: {
2451
+ headers: {
2452
+ Authorization: `Bearer ${provider.apiKey}`,
2453
+ "Content-Type": "application/json"
2454
+ }
2455
+ }
2456
+ };
2457
+ }
2458
+ /**
2459
+ * Transform unified request → Response API format
2460
+ */
2461
+ async transformRequestIn(request, provider, _context) {
2462
+ const input = [];
2463
+ for (const msg of request.messages) {
2464
+ if (msg.role === "system") {
2465
+ input.push({
2466
+ role: "developer",
2467
+ content: typeof msg.content === "string" ? msg.content : flattenContent(msg.content)
2468
+ });
2469
+ } else if (msg.role === "tool") {
2470
+ input.push({
2471
+ type: "function_call_output",
2472
+ call_id: msg.tool_call_id,
2473
+ output: typeof msg.content === "string" ? msg.content : ""
2474
+ });
2475
+ } else {
2476
+ const entry = {
2477
+ role: msg.role,
2478
+ content: typeof msg.content === "string" ? msg.content : flattenContent(msg.content)
2479
+ };
2480
+ if (msg.role === "assistant" && msg.tool_calls?.length) {
2481
+ input.push(entry);
2482
+ for (const tc of msg.tool_calls) {
2483
+ input.push({
2484
+ type: "function_call",
2485
+ id: tc.id,
2486
+ call_id: tc.id,
2487
+ name: tc.function.name,
2488
+ arguments: tc.function.arguments
2489
+ });
2490
+ }
2491
+ continue;
2492
+ }
2493
+ input.push(entry);
2494
+ }
2495
+ }
2496
+ const body = {
2497
+ model: request.model,
2498
+ input,
2499
+ stream: request.stream ?? false,
2500
+ ...request.max_tokens ? { max_output_tokens: request.max_tokens } : {},
2501
+ ...request.temperature !== void 0 ? { temperature: request.temperature } : {}
2502
+ };
2503
+ if (request.reasoning?.effort && request.reasoning.effort !== "none") {
2504
+ body.reasoning = { effort: request.reasoning.effort, summary: "auto" };
2505
+ }
2506
+ if (request.tools?.length) {
2507
+ body.tools = request.tools.map((tool) => ({
2508
+ type: "function",
2509
+ name: tool.function.name,
2510
+ description: tool.function.description,
2511
+ parameters: tool.function.parameters
2512
+ }));
2513
+ }
2514
+ if (request.tool_choice) {
2515
+ if (typeof request.tool_choice === "string") {
2516
+ body.tool_choice = request.tool_choice;
2517
+ } else if (typeof request.tool_choice === "object" && "function" in request.tool_choice) {
2518
+ body.tool_choice = { type: "function", name: request.tool_choice.function.name };
2519
+ }
2520
+ }
2521
+ const url = new URL("/v1/responses", provider.baseUrl);
2522
+ return { body, config: { url } };
2523
+ }
2524
+ /**
2525
+ * Transform Response API request → unified format
2526
+ */
2527
+ async transformRequestOut(request, _context) {
2528
+ const req = request;
2529
+ const messages = [];
2530
+ if (req.input) {
2531
+ for (const item of req.input) {
2532
+ const entry = item;
2533
+ if (entry.type === "function_call_output") {
2534
+ messages.push({
2535
+ role: "tool",
2536
+ content: entry.output || "",
2537
+ tool_call_id: entry.call_id || void 0
2538
+ });
2539
+ continue;
2540
+ }
2541
+ if (entry.type === "function_call") {
2542
+ const toolCall = {
2543
+ id: (entry.call_id ?? entry.id) || "",
2544
+ type: "function",
2545
+ function: {
2546
+ name: entry.name || "",
2547
+ arguments: typeof entry.arguments === "string" ? entry.arguments : ""
2548
+ }
2549
+ };
2550
+ const last = messages[messages.length - 1];
2551
+ if (last && last.role === "assistant") {
2552
+ (last.tool_calls ??= []).push(toolCall);
2553
+ } else {
2554
+ messages.push({ role: "assistant", content: null, tool_calls: [toolCall] });
2555
+ }
2556
+ continue;
2557
+ }
2558
+ const role = entry.role;
2559
+ if (role === "developer") {
2560
+ messages.push({
2561
+ role: "system",
2562
+ content: typeof entry.content === "string" ? entry.content : JSON.stringify(entry.content)
2563
+ });
2564
+ } else if (role === "user" || role === "assistant") {
2565
+ messages.push({
2566
+ role,
2567
+ content: typeof entry.content === "string" ? entry.content : JSON.stringify(entry.content)
2568
+ });
2569
+ }
2570
+ }
2571
+ }
2572
+ const result = {
2573
+ messages,
2574
+ model: req.model,
2575
+ max_tokens: req.max_output_tokens,
2576
+ temperature: req.temperature,
2577
+ stream: req.stream
2578
+ };
2579
+ if (req.reasoning?.effort) {
2580
+ result.reasoning = {
2581
+ effort: req.reasoning.effort,
2582
+ enabled: true
2583
+ };
2584
+ }
2585
+ if (req.tools?.length) {
2586
+ result.tools = req.tools.filter((t) => t.type === "function").map((t) => ({
2587
+ type: "function",
2588
+ function: {
2589
+ name: t.name || "",
2590
+ description: t.description || "",
2591
+ parameters: t.parameters || {}
2592
+ }
2593
+ }));
2594
+ }
2595
+ return result;
2596
+ }
2597
+ /**
2598
+ * Transform Response API response → unified (OpenAI CC) format
2599
+ */
2600
+ async transformResponseOut(response, _context) {
2601
+ const contentType = response.headers.get("Content-Type") ?? "";
2602
+ if (contentType.includes("text/event-stream")) {
2603
+ if (!response.body) {
2604
+ throw new Error("Stream response body is null");
2605
+ }
2606
+ return new Response(convertResponseApiStreamToOpenAI(response.body), {
2607
+ headers: {
2608
+ "Content-Type": "text/event-stream",
2609
+ "Cache-Control": "no-cache",
2610
+ Connection: "keep-alive"
2611
+ }
2612
+ });
2613
+ }
2614
+ const data = await response.json();
2615
+ return new Response(JSON.stringify(convertResponseApiJsonToOpenAI(data)), {
2616
+ headers: { "Content-Type": "application/json" }
2617
+ });
2618
+ }
2619
+ /**
2620
+ * Transform OpenAI CC response → Response API format
2621
+ */
2622
+ async transformResponseIn(response, _context) {
2623
+ const contentType = response.headers.get("Content-Type") ?? "";
2624
+ if (contentType.includes("text/event-stream")) {
2625
+ if (!response.body) {
2626
+ throw new Error("Stream response body is null");
2627
+ }
2628
+ return new Response(convertOpenAIStreamToResponseApi(response.body), {
2629
+ headers: {
2630
+ "Content-Type": "text/event-stream",
2631
+ "Cache-Control": "no-cache",
2632
+ Connection: "keep-alive"
2633
+ }
2634
+ });
2635
+ }
2636
+ const data = await response.json();
2637
+ return new Response(JSON.stringify(convertOpenAIJsonToResponseApi(data)), {
2638
+ headers: { "Content-Type": "application/json" }
2639
+ });
2640
+ }
2641
+ };
2642
+ function flattenContent(content) {
2643
+ if (typeof content === "string") return content;
2644
+ if (!content) return "";
2645
+ if (Array.isArray(content)) {
2646
+ return content.filter((c) => c.type === "text").map((c) => c.text || "").join("\n");
2647
+ }
2648
+ return "";
2649
+ }
2650
+ function convertResponseApiJsonToOpenAI(data) {
2651
+ let textContent = "";
2652
+ const toolCalls = [];
2653
+ const output = data.output;
2654
+ if (output) {
2655
+ for (const item of output) {
2656
+ if (item.type === "message") {
2657
+ const content = item.content;
2658
+ if (content) {
2659
+ for (const part of content) {
2660
+ if (part.type === "output_text" && typeof part.text === "string") {
2661
+ textContent += part.text;
2662
+ }
2663
+ }
2664
+ }
2665
+ } else if (item.type === "function_call") {
2666
+ toolCalls.push({
2667
+ id: item.call_id || item.id || `call_${Date.now()}`,
2668
+ type: "function",
2669
+ function: {
2670
+ name: item.name,
2671
+ arguments: typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments || {})
2672
+ }
2673
+ });
2674
+ }
2675
+ }
2676
+ }
2677
+ const usage = data.usage;
2678
+ const message = {
2679
+ role: "assistant",
2680
+ content: textContent || null
2681
+ };
2682
+ if (toolCalls.length > 0) {
2683
+ message.tool_calls = toolCalls;
2684
+ }
2685
+ return {
2686
+ id: data.id || `chatcmpl-${Date.now()}`,
2687
+ object: "chat.completion",
2688
+ created: Math.floor(Date.now() / 1e3),
2689
+ model: data.model || "unknown",
2690
+ choices: [
2691
+ {
2692
+ index: 0,
2693
+ message,
2694
+ finish_reason: toolCalls.length > 0 ? "tool_calls" : "stop"
2695
+ }
2696
+ ],
2697
+ usage: usage ? {
2698
+ prompt_tokens: usage.input_tokens || 0,
2699
+ completion_tokens: usage.output_tokens || 0,
2700
+ total_tokens: (usage.input_tokens || 0) + (usage.output_tokens || 0)
2701
+ } : void 0
2702
+ };
2703
+ }
2704
+ function convertOpenAIJsonToResponseApi(data) {
2705
+ const choices = data.choices;
2706
+ const message = choices?.[0]?.message;
2707
+ const output = [];
2708
+ if (message) {
2709
+ const contentParts = [];
2710
+ if (message.content) {
2711
+ contentParts.push({ type: "output_text", text: message.content });
2712
+ }
2713
+ if (contentParts.length > 0) {
2714
+ output.push({ type: "message", role: "assistant", content: contentParts });
2715
+ }
2716
+ const toolCalls = message.tool_calls;
2717
+ if (toolCalls?.length) {
2718
+ for (const tc of toolCalls) {
2719
+ const func = tc.function;
2720
+ output.push({
2721
+ type: "function_call",
2722
+ id: tc.id,
2723
+ call_id: tc.id,
2724
+ name: func?.name,
2725
+ arguments: func?.arguments
2726
+ });
2727
+ }
2728
+ }
2729
+ }
2730
+ const usage = data.usage;
2731
+ return {
2732
+ id: data.id || `resp_${Date.now()}`,
2733
+ object: "response",
2734
+ status: "completed",
2735
+ model: data.model || "unknown",
2736
+ output,
2737
+ usage: usage ? {
2738
+ input_tokens: usage.prompt_tokens || 0,
2739
+ output_tokens: usage.completion_tokens || 0,
2740
+ total_tokens: usage.total_tokens || (usage.prompt_tokens || 0) + (usage.completion_tokens || 0)
2741
+ } : void 0
2742
+ };
2743
+ }
2744
+ function convertResponseApiStreamToOpenAI(responseApiStream) {
2745
+ const decoder = new TextDecoder();
2746
+ const encoder = new TextEncoder();
2747
+ return new ReadableStream({
2748
+ start: async (controller) => {
2749
+ const reader = responseApiStream.getReader();
2750
+ let buffer = "";
2751
+ let isClosed = false;
2752
+ const messageId = `chatcmpl-${Date.now()}`;
2753
+ let model = "unknown";
2754
+ let hasEmittedRole = false;
2755
+ const safeEnqueue = (str) => {
2756
+ if (!isClosed) {
2757
+ try {
2758
+ controller.enqueue(encoder.encode(str));
2759
+ } catch {
2760
+ isClosed = true;
2761
+ }
2762
+ }
2763
+ };
2764
+ const emitChunk = (choices, usage) => {
2765
+ const chunk = {
2766
+ id: messageId,
2767
+ object: "chat.completion.chunk",
2768
+ created: Math.floor(Date.now() / 1e3),
2769
+ model,
2770
+ choices
2771
+ };
2772
+ if (usage) chunk.usage = usage;
2773
+ safeEnqueue(`data: ${JSON.stringify(chunk)}
2774
+
2775
+ `);
2776
+ };
2777
+ try {
2778
+ while (true) {
2779
+ const { done, value } = await reader.read();
2780
+ if (done) break;
2781
+ buffer += decoder.decode(value, { stream: true });
2782
+ const lines = buffer.split("\n");
2783
+ buffer = lines.pop() || "";
2784
+ for (const line of lines) {
2785
+ if (isClosed) break;
2786
+ if (!line.startsWith("data:")) continue;
2787
+ const data = line.slice(5).trim();
2788
+ if (data === "[DONE]") continue;
2789
+ try {
2790
+ const event = JSON.parse(data);
2791
+ model = event.model || event.response?.model || model;
2792
+ switch (event.type) {
2793
+ case "response.output_text.delta":
2794
+ if (event.delta) {
2795
+ if (!hasEmittedRole) {
2796
+ emitChunk([{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }]);
2797
+ hasEmittedRole = true;
2798
+ }
2799
+ emitChunk([{ index: 0, delta: { content: event.delta }, finish_reason: null }]);
2800
+ }
2801
+ break;
2802
+ case "response.reasoning_summary_text.delta":
2803
+ if (event.delta) {
2804
+ emitChunk([{
2805
+ index: 0,
2806
+ delta: { thinking: { content: event.delta } },
2807
+ finish_reason: null
2808
+ }]);
2809
+ }
2810
+ break;
2811
+ case "response.completed": {
2812
+ const resp = event.response;
2813
+ const respUsage = resp?.usage;
2814
+ const usage = respUsage ? {
2815
+ prompt_tokens: respUsage.input_tokens || 0,
2816
+ completion_tokens: respUsage.output_tokens || 0,
2817
+ total_tokens: (respUsage.input_tokens || 0) + (respUsage.output_tokens || 0)
2818
+ } : void 0;
2819
+ emitChunk([{ index: 0, delta: {}, finish_reason: "stop" }], usage);
2820
+ safeEnqueue("data: [DONE]\n\n");
2821
+ break;
2822
+ }
2823
+ case "error":
2824
+ emitChunk([{
2825
+ index: 0,
2826
+ delta: { content: `[Error: ${event.error?.message || "Unknown error"}]` },
2827
+ finish_reason: "stop"
2828
+ }]);
2829
+ break;
2830
+ default:
2831
+ break;
2832
+ }
2833
+ } catch {
2834
+ }
2835
+ }
2836
+ }
2837
+ } catch (e) {
2838
+ if (!isClosed) controller.error(e);
2839
+ } finally {
2840
+ if (!isClosed) {
2841
+ try {
2842
+ controller.close();
2843
+ } catch {
2844
+ }
2845
+ }
2846
+ reader.releaseLock();
2847
+ }
2848
+ }
2849
+ });
2850
+ }
2851
+ function convertOpenAIStreamToResponseApi(openaiStream) {
2852
+ const decoder = new TextDecoder();
2853
+ const encoder = new TextEncoder();
2854
+ return new ReadableStream({
2855
+ start: async (controller) => {
2856
+ const reader = openaiStream.getReader();
2857
+ let buffer = "";
2858
+ let isClosed = false;
2859
+ let accumulatedContent = "";
2860
+ let model = "unknown";
2861
+ const responseId = `resp_${Date.now()}`;
2862
+ const safeEnqueue = (str) => {
2863
+ if (!isClosed) {
2864
+ try {
2865
+ controller.enqueue(encoder.encode(str));
2866
+ } catch {
2867
+ isClosed = true;
2868
+ }
2869
+ }
2870
+ };
2871
+ const emitEvent = (event) => {
2872
+ safeEnqueue(`data: ${JSON.stringify(event)}
2873
+
2874
+ `);
2875
+ };
2876
+ emitEvent({
2877
+ type: "response.created",
2878
+ response: { id: responseId, status: "in_progress" }
2879
+ });
2880
+ try {
2881
+ while (true) {
2882
+ const { done, value } = await reader.read();
2883
+ if (done) break;
2884
+ buffer += decoder.decode(value, { stream: true });
2885
+ const lines = buffer.split("\n");
2886
+ buffer = lines.pop() || "";
2887
+ for (const line of lines) {
2888
+ if (isClosed) break;
2889
+ if (!line.startsWith("data:")) continue;
2890
+ const data = line.slice(5).trim();
2891
+ if (data === "[DONE]") continue;
2892
+ try {
2893
+ const chunk = JSON.parse(data);
2894
+ const choice = chunk.choices?.[0];
2895
+ model = chunk.model || model;
2896
+ if (!choice) continue;
2897
+ if (choice.delta?.content) {
2898
+ accumulatedContent += choice.delta.content;
2899
+ emitEvent({ type: "response.output_text.delta", delta: choice.delta.content });
2900
+ }
2901
+ if (choice.delta?.thinking?.content) {
2902
+ emitEvent({
2903
+ type: "response.reasoning_summary_text.delta",
2904
+ delta: choice.delta.thinking.content
2905
+ });
2906
+ }
2907
+ if (choice.finish_reason) {
2908
+ emitEvent({ type: "response.output_text.done", text: accumulatedContent });
2909
+ emitEvent({
2910
+ type: "response.completed",
2911
+ response: {
2912
+ id: responseId,
2913
+ status: "completed",
2914
+ model,
2915
+ output: [
2916
+ {
2917
+ type: "message",
2918
+ role: "assistant",
2919
+ content: [{ type: "output_text", text: accumulatedContent }]
2920
+ }
2921
+ ],
2922
+ usage: chunk.usage ? {
2923
+ input_tokens: chunk.usage.prompt_tokens || 0,
2924
+ output_tokens: chunk.usage.completion_tokens || 0,
2925
+ total_tokens: chunk.usage.total_tokens || 0
2926
+ } : void 0
2927
+ }
2928
+ });
2929
+ }
2930
+ } catch {
2931
+ }
2932
+ }
2933
+ }
2934
+ } catch (e) {
2935
+ if (!isClosed) controller.error(e);
2936
+ } finally {
2937
+ if (!isClosed) {
2938
+ try {
2939
+ controller.close();
2940
+ } catch {
2941
+ }
2942
+ }
2943
+ reader.releaseLock();
2944
+ }
2945
+ }
2946
+ });
2947
+ }
2948
+
2949
+ // src/transformer/transformers/ReasoningTransformer.ts
2950
+ import {
2951
+ buildAnthropicThinking,
2952
+ buildQwenThinkingConfig,
2953
+ calculateThinkingBudget,
2954
+ getOpenAIReasoningEffort
2955
+ } from "@omnicross/contracts/thinking-config";
2956
+
2957
+ // src/completion/StreamHandler.ts
2958
+ import { buildAnthropicThinking as buildAnthropicThinking2, getOpenAIReasoningEffort as getOpenAIReasoningEffort3 } from "@omnicross/contracts/thinking-config";
2959
+
2960
+ // src/completion/ThinkingResolver.ts
2961
+ import {
2962
+ buildAnthropicThinking as buildAnthropicThinking3,
2963
+ calculateThinkingBudget as calculateThinkingBudget2,
2964
+ DEFAULT_MAX_TOKENS,
2965
+ getClaudeMaxTokens,
2966
+ isReasoningModel
2967
+ } from "@omnicross/contracts/thinking-config";
2968
+
2969
+ // src/completion/url-builder.ts
2970
+ import { resolveProviderEndpoint as resolveProviderEndpointShared } from "@omnicross/contracts/endpoint-resolver";
2971
+ function resolveApiFormat(provider) {
2972
+ if (provider.apiFormat) {
2973
+ return provider.apiFormat;
2974
+ }
2975
+ if (provider.chatApiFormat) {
2976
+ return provider.chatApiFormat;
2977
+ }
2978
+ if (provider.apiType === "claudecode" || provider.apiType === "anthropic") {
2979
+ return "anthropic";
2980
+ }
2981
+ if (provider.apiType === "google") {
2982
+ return "google";
2983
+ }
2984
+ return "openai";
2985
+ }
2986
+ function buildOpenAIApiUrl(baseUrl) {
2987
+ const url = baseUrl.replace(/\/+$/, "");
2988
+ if (url.endsWith("/chat/completions")) {
2989
+ return url;
2990
+ }
2991
+ if (/\/v\d+$/.test(url)) {
2992
+ return url + "/chat/completions";
2993
+ }
2994
+ if (url.includes("/chat/completions")) {
2995
+ return url;
2996
+ }
2997
+ return url + "/v1/chat/completions";
2998
+ }
2999
+ function buildAnthropicApiUrl(baseUrl) {
3000
+ const url = baseUrl.replace(/\/+$/, "");
3001
+ if (url.endsWith("/messages")) {
3002
+ return url;
3003
+ }
3004
+ if (url.includes("/messages")) {
3005
+ return url;
3006
+ }
3007
+ if (/\/v\d+$/.test(url)) {
3008
+ return url + "/messages";
3009
+ }
3010
+ return url + "/v1/messages";
3011
+ }
3012
+ function buildGeminiModelActionUrl(baseUrl, model, action) {
3013
+ let url = baseUrl.replace(/\/+$/, "");
3014
+ if (url.includes("/models/")) {
3015
+ return url;
3016
+ }
3017
+ if (!/\/v\d+(?:beta)?(?:$|\/)/.test(url)) {
3018
+ url += "/v1beta";
3019
+ }
3020
+ return `${url}/models/${model}:${action}`;
3021
+ }
3022
+ function buildGeminiApiUrl(baseUrl, model, stream) {
3023
+ const action = stream ? "streamGenerateContent?alt=sse" : "generateContent";
3024
+ return buildGeminiModelActionUrl(baseUrl, model, action);
3025
+ }
3026
+ function normalizeAzureEndpoint(endpoint) {
3027
+ return endpoint.replace(/\/+$/, "").replace(/\/openai(\/v\d+)?$/, "");
3028
+ }
3029
+ function buildAzureOpenAIApiUrl(baseUrl, model, apiVersion) {
3030
+ const endpoint = normalizeAzureEndpoint(baseUrl);
3031
+ return `${endpoint}/openai/deployments/${model}/chat/completions?api-version=${apiVersion}`;
3032
+ }
3033
+ function buildOpenAIResponseApiUrl(baseUrl) {
3034
+ const url = baseUrl.replace(/\/+$/, "");
3035
+ if (url.endsWith("/responses")) {
3036
+ return url;
3037
+ }
3038
+ if (/\/v\d+$/.test(url)) {
3039
+ return url + "/responses";
3040
+ }
3041
+ return url + "/v1/responses";
3042
+ }
3043
+ var resolveProviderEndpoint = resolveProviderEndpointShared;
3044
+ function buildProviderApiUrl(provider, options = {}) {
3045
+ const format = resolveApiFormat(provider);
3046
+ const { baseUrl } = resolveProviderEndpoint(provider);
3047
+ switch (format) {
3048
+ case "anthropic":
3049
+ return buildAnthropicApiUrl(baseUrl);
3050
+ case "google":
3051
+ return buildGeminiApiUrl(baseUrl, options.model || "", options.stream || false);
3052
+ case "azure-openai":
3053
+ return buildAzureOpenAIApiUrl(baseUrl, options.model || "", provider.apiVersion || "2024-08-01-preview");
3054
+ case "openai-response":
3055
+ return buildOpenAIResponseApiUrl(baseUrl);
3056
+ case "openai":
3057
+ default:
3058
+ return buildOpenAIApiUrl(baseUrl);
3059
+ }
3060
+ }
3061
+
3062
+ // src/pipeline/executeProviderCall.ts
3063
+ async function executeProviderCall(ctx) {
3064
+ const {
3065
+ executor,
3066
+ request,
3067
+ provider,
3068
+ chain,
3069
+ endpointTransformer,
3070
+ extendedContext,
3071
+ resolveUrl,
3072
+ buildHeaders,
3073
+ prepareBody,
3074
+ fetchFn,
3075
+ runResponseChain = false,
3076
+ responseChainRequest
3077
+ } = ctx;
3078
+ const { requestBody, config } = await executor.executeRequestChain(
3079
+ request,
3080
+ provider,
3081
+ chain,
3082
+ { endpointTransformer, extendedContext }
3083
+ );
3084
+ const finalBody = prepareBody ? await prepareBody(requestBody, config) : requestBody;
3085
+ const url = resolveUrl(config);
3086
+ const headers = buildHeaders(config);
3087
+ const fetched = await fetchFn(url, headers, finalBody);
3088
+ let response = fetched;
3089
+ if (runResponseChain) {
3090
+ response = await executor.executeResponseChain(
3091
+ // Preserve the EXACT first arg each site passed: proxy → requestBody,
3092
+ // unified ingresses → their pre-transform request. The caller supplies
3093
+ // it via `responseChainRequest`; fall back to `requestBody` (the proxy
3094
+ // shape) so the contract is never silently `undefined`.
3095
+ responseChainRequest ?? requestBody,
3096
+ fetched,
3097
+ provider,
3098
+ chain,
3099
+ { endpointTransformer }
3100
+ );
3101
+ }
3102
+ return { response, requestBody, finalBody, config, url, headers };
3103
+ }
3104
+
3105
+ // src/pipeline/resolveProviderChain.ts
3106
+ async function resolveProviderChain(llmConfig, providerId, model) {
3107
+ const cachedChain = await llmConfig.resolveTransformerChain(providerId, model);
3108
+ const mainTransformer = await llmConfig.getMainTransformer(providerId);
3109
+ const chain = {
3110
+ providerTransformers: [...cachedChain.providerTransformers],
3111
+ modelTransformers: [...cachedChain.modelTransformers]
3112
+ };
3113
+ if (mainTransformer) {
3114
+ const alreadyInChain = chain.providerTransformers.some(
3115
+ (t) => t.name === mainTransformer.name
3116
+ );
3117
+ if (!alreadyInChain) {
3118
+ chain.providerTransformers.unshift(mainTransformer);
3119
+ }
3120
+ }
3121
+ const hasTransformers = chain.providerTransformers.length > 0 || chain.modelTransformers.length > 0;
3122
+ return { chain, mainTransformer, hasTransformers };
3123
+ }
3124
+
3125
+ // src/completion/header-builder.ts
3126
+ function getProviderHeaders(provider, apiKey) {
3127
+ const format = resolveApiFormat(provider);
3128
+ let headers;
3129
+ switch (format) {
3130
+ case "anthropic":
3131
+ headers = {
3132
+ "Content-Type": "application/json",
3133
+ "x-api-key": apiKey,
3134
+ "anthropic-version": "2025-01-10"
3135
+ // Required for extended thinking feature
3136
+ };
3137
+ break;
3138
+ case "google":
3139
+ headers = {
3140
+ "Content-Type": "application/json",
3141
+ "x-goog-api-key": apiKey
3142
+ };
3143
+ break;
3144
+ case "azure-openai":
3145
+ headers = {
3146
+ "Content-Type": "application/json",
3147
+ "api-key": apiKey
3148
+ };
3149
+ break;
3150
+ case "openai":
3151
+ case "openai-response":
3152
+ default:
3153
+ headers = {
3154
+ "Content-Type": "application/json",
3155
+ "Authorization": `Bearer ${apiKey}`
3156
+ };
3157
+ break;
3158
+ }
3159
+ if (isOpenRouterProvider(provider)) {
3160
+ return { ...headers, ...OPENROUTER_APP_HEADERS };
3161
+ }
3162
+ return headers;
3163
+ }
3164
+
3165
+ // src/completion/message-converter.ts
3166
+ var MAX_INLINE_VIDEO_BYTES = 25 * 1024 * 1024;
3167
+
3168
+ // src/pipeline/LlmConfigProviderAuth.ts
3169
+ function isReportableStatus(status) {
3170
+ return status === 429 || status === 529 || status === 401 || status === 403;
3171
+ }
3172
+ var LlmConfigProviderAuth = class {
3173
+ provider;
3174
+ apiKeyPool;
3175
+ providerId;
3176
+ sessionId;
3177
+ /**
3178
+ * The current resolved key. Mutable so a Phase-3 caller can read the
3179
+ * rotated key after `onResult`; this phase it is only set at construction
3180
+ * and updated inside `onResult` (mirroring the inline re-point in
3181
+ * `callWithPoolReporting`).
3182
+ */
3183
+ apiKey;
3184
+ constructor(opts) {
3185
+ this.provider = opts.provider;
3186
+ this.apiKey = opts.apiKey;
3187
+ this.apiKeyPool = opts.apiKeyPool ?? null;
3188
+ this.providerId = opts.providerId;
3189
+ this.sessionId = opts.sessionId;
3190
+ }
3191
+ /**
3192
+ * Merge the provider auth headers (and content-type / OpenRouter app
3193
+ * headers) into `headers`, exactly as a direct `getProviderHeaders` call
3194
+ * would produce them. `hints` are accepted for contract symmetry but the
3195
+ * provider-key path does not vary headers by URL/model.
3196
+ */
3197
+ applyHeaders(headers, _hints) {
3198
+ const authHeaders = getProviderHeaders(this.provider, this.apiKey);
3199
+ for (const [k, v] of Object.entries(authHeaders)) {
3200
+ headers[k] = v;
3201
+ }
3202
+ }
3203
+ /**
3204
+ * React to the final HTTP status — the pool-rotation seam (design D5).
3205
+ *
3206
+ * Reproduces `callWithPoolReporting`'s reportable + success branches:
3207
+ * - reportable status (429/529/401/403) on a pool-backed request →
3208
+ * `reportError(providerId, sessionId, status)`; when it hands back a
3209
+ * `newKey` the session was re-bound, so we adopt it (`this.apiKey`) and
3210
+ * return `{ rebound: true, newKey }`. When no key is available it returns
3211
+ * `{ rebound: false }` (the session is still cooled/disabled by the report).
3212
+ * - success / any 2xx → `reportSuccess(sessionId)`; `{ rebound: false }`.
3213
+ * - non-reportable, non-success, or non-pool request → no-op
3214
+ * `{ rebound: false }`.
3215
+ *
3216
+ * NOTE: no caller invokes this in Phase 2; it is unit-tested in isolation.
3217
+ */
3218
+ async onResult(status) {
3219
+ const pool = this.apiKeyPool;
3220
+ const providerId = this.providerId;
3221
+ const sessionId = this.sessionId;
3222
+ if (!pool || !providerId || !sessionId) {
3223
+ return { rebound: false };
3224
+ }
3225
+ if (isReportableStatus(status)) {
3226
+ const newKey = await pool.reportError(providerId, sessionId, status);
3227
+ if (newKey) {
3228
+ this.apiKey = newKey;
3229
+ return { rebound: true, newKey };
3230
+ }
3231
+ return { rebound: false };
3232
+ }
3233
+ if (status !== null && status >= 200 && status < 300) {
3234
+ pool.reportSuccess(sessionId);
3235
+ }
3236
+ return { rebound: false };
3237
+ }
3238
+ };
3239
+
3240
+ // src/provider-proxy/usage/recordAnthropicUsage.ts
3241
+ function readAnthropicUsage(usage) {
3242
+ if (!usage) return null;
3243
+ const inputTokens = Number(usage.input_tokens) || 0;
3244
+ const outputTokens = Number(usage.output_tokens) || 0;
3245
+ const cacheReadTokens = Number(usage.cache_read_input_tokens) || 0;
3246
+ const cacheCreationTokens = Number(usage.cache_creation_input_tokens) || 0;
3247
+ return {
3248
+ inputTokens,
3249
+ outputTokens,
3250
+ cacheReadTokens,
3251
+ cacheCreationTokens,
3252
+ // The Anthropic wire carries no separate reasoning-token field; thinking
3253
+ // tokens are folded into `output_tokens`.
3254
+ reasoningTokens: 0
3255
+ };
3256
+ }
3257
+ function recordAnthropicNonStreamUsage(recorder, bodyText, attribution) {
3258
+ try {
3259
+ const parsed = JSON.parse(bodyText);
3260
+ const tapped = readAnthropicUsage(parsed.usage);
3261
+ if (!tapped) return;
3262
+ recorder.record({
3263
+ messageId: null,
3264
+ parentMessageId: null,
3265
+ sessionId: attribution.sessionId,
3266
+ providerId: attribution.providerId,
3267
+ model: attribution.model,
3268
+ apiKeyId: null,
3269
+ engineOrigin: "codex-ingress",
3270
+ usage: tapped,
3271
+ rawUsage: parsed.usage
3272
+ });
3273
+ } catch {
3274
+ }
3275
+ }
3276
+
3277
+ // src/pipeline/resolveSubscriptionChain.ts
3278
+ function resolveOne(transformerService, name) {
3279
+ const t = transformerService.getTransformer(name);
3280
+ if (!t) return null;
3281
+ return typeof t === "function" ? new t() : t;
3282
+ }
3283
+ function resolveSubscriptionChain(profile, transformerService, fallbackEndpoint, overrideNames) {
3284
+ const providerNames = overrideNames ?? profile.providerTransformerNames ?? [];
3285
+ const modelNames = profile.modelTransformerNames ?? [];
3286
+ if (providerNames.length === 0) {
3287
+ return { providerTransformers: [fallbackEndpoint], modelTransformers: [] };
3288
+ }
3289
+ if (!transformerService) {
3290
+ throw new Error(
3291
+ "[resolveSubscriptionChain] profile declares providerTransformerNames but no TransformerService is wired"
3292
+ );
3293
+ }
3294
+ const providerTransformers = [];
3295
+ for (const name of providerNames) {
3296
+ const instance = resolveOne(transformerService, name);
3297
+ if (!instance) {
3298
+ console.warn(`[resolveSubscriptionChain] Transformer not registered: ${name}`);
3299
+ continue;
3300
+ }
3301
+ providerTransformers.push(instance);
3302
+ }
3303
+ const modelTransformers = [];
3304
+ for (const name of modelNames) {
3305
+ const instance = resolveOne(transformerService, name);
3306
+ if (!instance) {
3307
+ console.warn(`[resolveSubscriptionChain] Model transformer not registered: ${name}`);
3308
+ continue;
3309
+ }
3310
+ modelTransformers.push(instance);
3311
+ }
3312
+ if (providerTransformers.length === 0) {
3313
+ return { providerTransformers: [fallbackEndpoint], modelTransformers };
3314
+ }
3315
+ return { providerTransformers, modelTransformers };
3316
+ }
3317
+
3318
+ // src/pipeline/SubscriptionAuthSource.ts
3319
+ function stripAuthHeaders(headers) {
3320
+ delete headers.authorization;
3321
+ delete headers.Authorization;
3322
+ delete headers["x-api-key"];
3323
+ delete headers["X-Api-Key"];
3324
+ delete headers["x-goog-api-key"];
3325
+ delete headers["X-Goog-Api-Key"];
3326
+ }
3327
+ var SubscriptionAuthSource = class {
3328
+ constructor(profile) {
3329
+ this.profile = profile;
3330
+ }
3331
+ profile;
3332
+ /**
3333
+ * Strip any transformer-set auth headers, then delegate to the bound
3334
+ * strategy. Mirrors `SubscriptionDispatcher`'s
3335
+ * `stripAuthHeaders(...)` + `applyHeadersWithRetry(...)` sequence, including
3336
+ * the best-effort swallow-and-warn around a throwing `applyHeaders`.
3337
+ *
3338
+ * The strategy's looser `AuthApplyHints` (optional `upstreamUrl` /
3339
+ * `resolvedModel`) is fed from the pipeline's required `{ upstreamUrl,
3340
+ * model }` at the boundary.
3341
+ */
3342
+ async applyHeaders(headers, hints) {
3343
+ stripAuthHeaders(headers);
3344
+ try {
3345
+ await this.profile.authStrategy.applyHeaders(headers, {
3346
+ upstreamUrl: hints.upstreamUrl,
3347
+ resolvedModel: hints.model
3348
+ });
3349
+ } catch (err) {
3350
+ console.warn("[SubscriptionAuthSource] authStrategy.applyHeaders threw:", serializeError(err));
3351
+ }
3352
+ }
3353
+ /** Delegate the 401-refresh decision to the bound strategy. */
3354
+ async onUnauthorized() {
3355
+ return this.profile.authStrategy.onUnauthorized();
3356
+ }
3357
+ /** Resolve the upstream URL from the profile, when it provides one. */
3358
+ resolveUpstreamUrl(model) {
3359
+ return this.profile.resolveUpstreamUrl?.(model);
3360
+ }
3361
+ };
3362
+
3363
+ // src/provider-proxy/matchText.ts
3364
+ var MATCH_TEXT_PER_MESSAGE_CAP = 8192;
3365
+ var MATCH_TEXT_RECENT_MESSAGES = 6;
3366
+ function flattenMatchText(value) {
3367
+ if (typeof value === "string") return value;
3368
+ if (Array.isArray(value)) {
3369
+ const parts = [];
3370
+ for (const item of value) {
3371
+ const text = flattenMatchText(item);
3372
+ if (text) parts.push(text);
3373
+ }
3374
+ return parts.join("\n");
3375
+ }
3376
+ if (value && typeof value === "object") {
3377
+ const obj = value;
3378
+ if (obj.type === "tool_result" && obj.content !== void 0) {
3379
+ return flattenMatchText(obj.content);
3380
+ }
3381
+ if (typeof obj.text === "string") return obj.text;
3382
+ }
3383
+ return "";
3384
+ }
3385
+ function collectMatchText(anthropicBody) {
3386
+ const messages = Array.isArray(anthropicBody.messages) ? anthropicBody.messages : [];
3387
+ const slices = [];
3388
+ const sys = flattenMatchText(anthropicBody.system).trim();
3389
+ if (sys) slices.push(sys.slice(0, MATCH_TEXT_PER_MESSAGE_CAP));
3390
+ const recent = [];
3391
+ for (let i = messages.length - 1; i >= 0 && recent.length < MATCH_TEXT_RECENT_MESSAGES; i--) {
3392
+ const message = messages[i];
3393
+ if (!message || typeof message !== "object") continue;
3394
+ const role = message.role;
3395
+ if (role !== "user" && role !== "system") continue;
3396
+ const text = flattenMatchText(message.content).trim();
3397
+ if (text) recent.push(text.slice(0, MATCH_TEXT_PER_MESSAGE_CAP));
3398
+ }
3399
+ for (let i = recent.length - 1; i >= 0; i--) slices.push(recent[i]);
3400
+ return slices;
3401
+ }
3402
+
3403
+ // src/provider-proxy/ingress/providerProxyShared.ts
3404
+ var sharedExecutor = null;
3405
+ function getSharedExecutor() {
3406
+ if (!sharedExecutor) {
3407
+ sharedExecutor = new TransformerChainExecutor();
3408
+ }
3409
+ return sharedExecutor;
3410
+ }
3411
+ var sharedResponses = null;
3412
+ function getResponsesEndpointTransformer() {
3413
+ if (!sharedResponses) sharedResponses = new OpenAIResponseTransformer();
3414
+ return sharedResponses;
3415
+ }
3416
+ var sharedAnthropic = null;
3417
+ function getAnthropicEndpointTransformer() {
3418
+ if (!sharedAnthropic) sharedAnthropic = new AnthropicTransformer();
3419
+ return sharedAnthropic;
3420
+ }
3421
+ var sharedGemini = null;
3422
+ function getGeminiEndpointTransformer() {
3423
+ if (!sharedGemini) sharedGemini = new GeminiTransformer();
3424
+ return sharedGemini;
3425
+ }
3426
+ function readBody(req) {
3427
+ return new Promise((resolve, reject) => {
3428
+ const chunks = [];
3429
+ req.on("data", (chunk) => chunks.push(chunk));
3430
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
3431
+ req.on("error", reject);
3432
+ });
3433
+ }
3434
+ async function relayResponse(res, providerResponse, isStream) {
3435
+ const contentType = providerResponse.headers.get("Content-Type") ?? "";
3436
+ const status = providerResponse.status && providerResponse.status >= 100 ? providerResponse.status : 200;
3437
+ if (isStream || contentType.includes("text/event-stream")) {
3438
+ res.writeHead(status, {
3439
+ "Content-Type": "text/event-stream",
3440
+ "Cache-Control": "no-cache",
3441
+ Connection: "keep-alive"
3442
+ });
3443
+ if (!providerResponse.body) {
3444
+ res.end();
3445
+ return null;
3446
+ }
3447
+ const reader = providerResponse.body.getReader();
3448
+ try {
3449
+ for (; ; ) {
3450
+ const { done, value } = await reader.read();
3451
+ if (done) break;
3452
+ res.write(value);
3453
+ }
3454
+ } finally {
3455
+ reader.releaseLock();
3456
+ res.end();
3457
+ }
3458
+ return null;
3459
+ }
3460
+ const bodyText = await providerResponse.text();
3461
+ res.writeHead(status, { "Content-Type": contentType.includes("json") ? contentType : "application/json" });
3462
+ res.end(bodyText);
3463
+ return bodyText;
3464
+ }
3465
+ function writeError(res, status, message) {
3466
+ if (res.headersSent) return;
3467
+ res.writeHead(status, { "Content-Type": "application/json" });
3468
+ res.end(JSON.stringify({ error: { type: "provider_proxy_error", message } }));
3469
+ }
3470
+ function resolveApiKey(apiKey) {
3471
+ if (!apiKey) return "";
3472
+ if (apiKey.startsWith("$")) {
3473
+ return process.env[apiKey.slice(1)] || "";
3474
+ }
3475
+ return apiKey;
3476
+ }
3477
+ async function resolvePoolBoundKey(deps, providerId, provider, sessionId) {
3478
+ if (deps.apiKeyPool && sessionId) {
3479
+ const poolKey = await deps.apiKeyPool.getKeyForSession(providerId, sessionId);
3480
+ if (poolKey) return poolKey;
3481
+ }
3482
+ return resolveApiKey(resolveProviderEndpoint(provider).apiKey);
3483
+ }
3484
+
3485
+ // src/provider-proxy/ingress/anthropicSubscriptionPlan.ts
3486
+ async function buildSubscriptionPlan(res, route, deps, anthropicBody, routeModel, isStream) {
3487
+ const profile = route.subscriptionProfile;
3488
+ if (!profile) {
3489
+ writeError(res, 502, "Subscription mode requires an Anthropic subscription profile");
3490
+ return null;
3491
+ }
3492
+ let resolvedModel = routeModel;
3493
+ let scenario;
3494
+ if (profile.modelMapper) {
3495
+ const summary = buildSubscriptionRequestSummary(anthropicBody);
3496
+ const mapped = profile.modelMapper(routeModel, summary, route.subscriptionConfig);
3497
+ resolvedModel = mapped.resolvedModel;
3498
+ scenario = mapped.scenario;
3499
+ }
3500
+ const plan = buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isStream, scenario);
3501
+ if (!plan) {
3502
+ writeError(res, 502, "Subscription profile is missing resolveUpstreamUrl");
3503
+ return null;
3504
+ }
3505
+ return plan;
3506
+ }
3507
+ function buildSubscriptionIterationPlan(profile, route, deps, resolvedModel, isStream, scenario) {
3508
+ const upstreamUrl = profile.resolveUpstreamUrl?.(resolvedModel, route.subscriptionConfig);
3509
+ if (!upstreamUrl) return null;
3510
+ const auth = route.auth ?? new SubscriptionAuthSource(profile);
3511
+ const sameFormat = profile.mode === "pass-through" || upstreamUrl.endsWith("/v1/messages");
3512
+ const overrideNames = profile.resolveProviderTransformerNames?.(
3513
+ resolvedModel,
3514
+ route.subscriptionConfig
3515
+ );
3516
+ const chain = sameFormat ? { providerTransformers: [], modelTransformers: [] } : resolveSubscriptionChain(
3517
+ profile,
3518
+ deps.llmConfig.getTransformerService(),
3519
+ getAnthropicEndpointTransformer(),
3520
+ overrideNames
3521
+ );
3522
+ const transformerProvider = {
3523
+ name: profile.authStrategy.providerId,
3524
+ baseUrl: upstreamUrl,
3525
+ apiKey: "",
3526
+ models: [resolvedModel]
3527
+ };
3528
+ return {
3529
+ auth,
3530
+ chain,
3531
+ transformerProvider,
3532
+ resolvedModel,
3533
+ isStream,
3534
+ // EXCEPTION (the Responses chain) (Phase 3 task 3.3): the `openai-response`
3535
+ // provider transformer emits an ABSOLUTE-path `config.url`
3536
+ // (`new URL('/v1/responses', baseUrl)`) that DISCARDS any base PATH prefix — so
3537
+ // for a path-prefixed base (zen `https://opencode.ai/zen/v1/responses` → drops
3538
+ // `/zen/`; codex `https://chatgpt.com/backend-api/codex/responses` → drops
3539
+ // `/backend-api/codex/`). On the core `/v1/messages` plan the endpoint
3540
+ // transformer is `anthropic` and the codex/zen-responses chain is
3541
+ // `['openai-response']` (endpoint != provider), so `transformRequestIn` RUNS
3542
+ // and emits the lossy URL. The guard makes this safe REGARDLESS of whether it
3543
+ // ran: `usesResponsesChain(['openai-response'])` captures BOTH codex and zen
3544
+ // responses, so the profile's complete `upstreamUrl` always wins (codex → its
3545
+ // backend-api/codex/responses; zen → its `/zen/...` endpoint). The gemini
3546
+ // chain's `config.url` is RELATIVE (`./{model}:{action}`) and PRESERVES the
3547
+ // base path, so it correctly wins. The bypass-by-name optimization only applies
3548
+ // on the core OpenAI-Responses ingress (endpoint == `openai-response`), NOT
3549
+ // here. `// UNVERIFIED (no live zen key)`.
3550
+ resolveUrl: usesResponsesChain(overrideNames ?? profile.providerTransformerNames) ? () => upstreamUrl : (config) => config.url instanceof URL ? config.url.toString() : typeof config.url === "string" ? config.url : upstreamUrl,
3551
+ upstreamUrl,
3552
+ sameFormat,
3553
+ isSubscription: true,
3554
+ scenario
3555
+ };
3556
+ }
3557
+ function usesResponsesChain(names) {
3558
+ return !!names && names.includes("openai-response");
3559
+ }
3560
+ function buildSubscriptionRequestSummary(anthropicBody) {
3561
+ const messages = Array.isArray(anthropicBody.messages) ? anthropicBody.messages : [];
3562
+ let totalChars = collectText(anthropicBody.system);
3563
+ for (const message of messages) {
3564
+ if (message && typeof message === "object") {
3565
+ totalChars += collectText(message.content);
3566
+ }
3567
+ }
3568
+ return {
3569
+ messageCount: messages.length,
3570
+ estimatedInputTokens: Math.ceil(totalChars / 4),
3571
+ // Single source of truth (shared with the dispatcher builder) so both
3572
+ // ingress paths yield identical `matchText` for the same body.
3573
+ matchText: collectMatchText(anthropicBody)
3574
+ };
3575
+ }
3576
+ function collectText(value) {
3577
+ if (typeof value === "string") return value.length;
3578
+ if (Array.isArray(value)) {
3579
+ let sum = 0;
3580
+ for (const item of value) sum += collectText(item);
3581
+ return sum;
3582
+ }
3583
+ if (value && typeof value === "object") {
3584
+ const text = value.text;
3585
+ if (typeof text === "string") return text.length;
3586
+ }
3587
+ return 0;
3588
+ }
3589
+ async function runPipeline(anthropicBody, plan) {
3590
+ const executor = getSharedExecutor();
3591
+ const endpointTransformer = getAnthropicEndpointTransformer();
3592
+ const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
3593
+ const authHeaders = {};
3594
+ await auth.applyHeaders(authHeaders, { upstreamUrl, model: resolvedModel });
3595
+ let rawStatus = null;
3596
+ const { response } = await executeProviderCall({
3597
+ executor,
3598
+ request: anthropicBody,
3599
+ provider: transformerProvider,
3600
+ chain,
3601
+ endpointTransformer,
3602
+ resolveUrl,
3603
+ buildHeaders: (config) => {
3604
+ const headers = { ...authHeaders };
3605
+ if (config.headers) {
3606
+ for (const [key, value] of Object.entries(config.headers)) {
3607
+ if (value !== void 0 && !(key in headers)) headers[key] = value;
3608
+ }
3609
+ }
3610
+ return headers;
3611
+ },
3612
+ fetchFn: (url, headers, body) => {
3613
+ console.info(`[ProviderProxy:anthropic] -> ${url} model=${resolvedModel} stream=${isStream}`);
3614
+ return fetch(url, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => {
3615
+ rawStatus = r.status;
3616
+ return r;
3617
+ });
3618
+ },
3619
+ runResponseChain: true
3620
+ });
3621
+ return { response, rawStatus };
3622
+ }
3623
+ async function runSubscriptionSameFormatFetch(rawBody, plan) {
3624
+ const headers = { "content-type": "application/json" };
3625
+ await plan.auth.applyHeaders(headers, {
3626
+ upstreamUrl: plan.upstreamUrl,
3627
+ model: plan.resolvedModel
3628
+ });
3629
+ console.info(
3630
+ `[ProviderProxy:anthropic] (subscription same-format) -> ${plan.upstreamUrl} model=${plan.resolvedModel} stream=${plan.isStream}`
3631
+ );
3632
+ const response = await fetch(plan.upstreamUrl, { method: "POST", headers, body: rawBody });
3633
+ return { response, rawStatus: response.status };
3634
+ }
3635
+ async function runSubscriptionAttemptWith401Retry(anthropicBody, relayBody, plan) {
3636
+ const runOnce = () => plan.sameFormat ? runSubscriptionSameFormatFetch(relayBody, plan) : runPipeline(anthropicBody, plan);
3637
+ const first = await runOnce();
3638
+ if (first.rawStatus !== 401) return first;
3639
+ const refreshed = await plan.auth.onUnauthorized?.();
3640
+ if (!refreshed) {
3641
+ console.warn("[ProviderProxy:anthropic] 401 not recoverable (onUnauthorized returned false)");
3642
+ return first;
3643
+ }
3644
+ console.info("[ProviderProxy:anthropic] 401 \u2192 token refreshed; retrying once");
3645
+ return runOnce();
3646
+ }
3647
+ function isFallbackEligibleStatus(rawStatus) {
3648
+ return rawStatus === null || rawStatus >= 500 || rawStatus === 429;
3649
+ }
3650
+ function breakerOutcome(rawStatus) {
3651
+ if (isFallbackEligibleStatus(rawStatus)) return "failure";
3652
+ if (rawStatus !== null && rawStatus >= 200 && rawStatus < 300) return "success";
3653
+ return "neutral";
3654
+ }
3655
+ function recordBreakerOutcome(profile, modelId, rawStatus) {
3656
+ const outcome = breakerOutcome(rawStatus);
3657
+ if (outcome === "neutral") return;
3658
+ profile?.recordModelOutcome?.(modelId, outcome === "success");
3659
+ }
3660
+ async function runSubscriptionAttemptOutcome(anthropicBody, relayBody, plan) {
3661
+ try {
3662
+ return { kind: "result", result: await runSubscriptionAttemptWith401Retry(anthropicBody, relayBody, plan) };
3663
+ } catch (error) {
3664
+ return { kind: "thrown", error };
3665
+ }
3666
+ }
3667
+ function outcomeStatus(outcome) {
3668
+ return outcome.kind === "thrown" ? null : outcome.result.rawStatus;
3669
+ }
3670
+ function settleOutcome(outcome) {
3671
+ if (outcome.kind === "thrown") throw outcome.error;
3672
+ return outcome.result;
3673
+ }
3674
+ async function runPipelineWithSubscriptionRetry(anthropicBody, rawBody, initialPlan, route, deps) {
3675
+ const profile = route.subscriptionProfile;
3676
+ const scenario = initialPlan.scenario;
3677
+ if (!profile?.nextFallback || scenario === void 0) {
3678
+ const loneOutcome = await runSubscriptionAttemptOutcome(anthropicBody, rawBody, initialPlan);
3679
+ recordBreakerOutcome(profile, initialPlan.resolvedModel, outcomeStatus(loneOutcome));
3680
+ return settleOutcome(loneOutcome);
3681
+ }
3682
+ const attempted = [];
3683
+ let plan = initialPlan;
3684
+ let firstRelayBody = rawBody;
3685
+ let firstBodyObj = anthropicBody;
3686
+ if (profile.allowModel && !profile.allowModel(initialPlan.resolvedModel)) {
3687
+ attempted.push(initialPlan.resolvedModel);
3688
+ const firstAdmitting = profile.nextFallback(scenario, attempted, route.subscriptionConfig);
3689
+ const gatedPlan = firstAdmitting ? buildSubscriptionIterationPlan(profile, route, deps, firstAdmitting.modelId, initialPlan.isStream, scenario) : null;
3690
+ if (firstAdmitting && gatedPlan) {
3691
+ console.warn(
3692
+ `[ProviderProxy:anthropic] subscription primary ${initialPlan.resolvedModel} circuit open -> first admitting fallback ${firstAdmitting.modelId}`
3693
+ );
3694
+ plan = gatedPlan;
3695
+ firstBodyObj = { ...anthropicBody, model: firstAdmitting.modelId };
3696
+ firstRelayBody = gatedPlan.sameFormat ? JSON.stringify(firstBodyObj) : rawBody;
3697
+ } else {
3698
+ attempted.length = 0;
3699
+ console.warn(
3700
+ `[ProviderProxy:anthropic] subscription all opencodego circuits open -> fail open to primary ${initialPlan.resolvedModel}`
3701
+ );
3702
+ }
3703
+ }
3704
+ let outcome = await runSubscriptionAttemptOutcome(firstBodyObj, firstRelayBody, plan);
3705
+ recordBreakerOutcome(profile, plan.resolvedModel, outcomeStatus(outcome));
3706
+ if (!attempted.includes(plan.resolvedModel)) attempted.push(plan.resolvedModel);
3707
+ while (attempted.length < MAX_FALLBACK_ATTEMPTS) {
3708
+ if (!isFallbackEligibleStatus(outcomeStatus(outcome))) return settleOutcome(outcome);
3709
+ const next = profile.nextFallback(scenario, attempted, route.subscriptionConfig);
3710
+ if (!next) return settleOutcome(outcome);
3711
+ const nextPlan = buildSubscriptionIterationPlan(
3712
+ profile,
3713
+ route,
3714
+ deps,
3715
+ next.modelId,
3716
+ plan.isStream,
3717
+ scenario
3718
+ );
3719
+ if (!nextPlan) return settleOutcome(outcome);
3720
+ const sinceLabel = outcome.kind === "thrown" ? "thrown error" : `status ${String(outcome.result.rawStatus)}`;
3721
+ console.warn(
3722
+ `[ProviderProxy:anthropic] subscription fallback ${plan.resolvedModel} -> ${next.modelId} after ${sinceLabel}`
3723
+ );
3724
+ const fallbackBodyObj = { ...anthropicBody, model: next.modelId };
3725
+ const fallbackRelayBody = nextPlan.sameFormat ? JSON.stringify(fallbackBodyObj) : rawBody;
3726
+ plan = nextPlan;
3727
+ attempted.push(next.modelId);
3728
+ outcome = await runSubscriptionAttemptOutcome(fallbackBodyObj, fallbackRelayBody, plan);
3729
+ recordBreakerOutcome(profile, plan.resolvedModel, outcomeStatus(outcome));
3730
+ }
3731
+ return settleOutcome(outcome);
3732
+ }
3733
+ var MAX_FALLBACK_ATTEMPTS = 3;
3734
+
3735
+ // src/provider-proxy/ingress/anthropicMessagesByo.ts
3736
+ async function handleAnthropicMessagesByo(res, rawBody, route, deps, options = {}) {
3737
+ let anthropicBody;
3738
+ try {
3739
+ anthropicBody = JSON.parse(rawBody);
3740
+ } catch {
3741
+ writeError(res, 400, "Invalid JSON in request body");
3742
+ return;
3743
+ }
3744
+ const isStream = anthropicBody.stream === true;
3745
+ const resolvedModel = route.model;
3746
+ anthropicBody.model = resolvedModel;
3747
+ try {
3748
+ const plan = route.authMode === "subscription" ? await buildSubscriptionPlan(res, route, deps, anthropicBody, resolvedModel, isStream) : await buildByoPlan(res, route, deps, resolvedModel, isStream);
3749
+ if (!plan) return;
3750
+ anthropicBody.model = plan.resolvedModel;
3751
+ const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry(anthropicBody, rawBody, plan, route, deps) : await runPipelineWithPoolReporting(anthropicBody, rawBody, plan, options);
3752
+ const bodyText = await relayResponse(res, providerResponse.response, isStream);
3753
+ if (bodyText && deps.usageRecorder) {
3754
+ recordAnthropicNonStreamUsage(deps.usageRecorder, bodyText, {
3755
+ sessionId: route.sessionId,
3756
+ providerId: route.providerId ?? "anthropic",
3757
+ model: plan.resolvedModel
3758
+ });
3759
+ }
3760
+ } catch (err) {
3761
+ const errMsg = serializeError(err);
3762
+ console.error("[ProviderProxy:anthropic] Pipeline error:", errMsg);
3763
+ writeError(res, 502, errMsg);
3764
+ }
3765
+ }
3766
+ async function buildByoPlan(res, route, deps, resolvedModel, isStream) {
3767
+ const providerId = route.providerId;
3768
+ if (!providerId) {
3769
+ writeError(res, 502, "BYO route is missing a providerId");
3770
+ return null;
3771
+ }
3772
+ const provider = await deps.llmConfig.getProvider(providerId);
3773
+ if (!provider) {
3774
+ writeError(res, 502, `Provider not found: ${providerId}`);
3775
+ return null;
3776
+ }
3777
+ const apiKey = await resolvePoolBoundKey(deps, providerId, provider, route.sessionId);
3778
+ if (!apiKey) {
3779
+ writeError(res, 502, "API key not configured");
3780
+ return null;
3781
+ }
3782
+ const auth = route.auth ?? new LlmConfigProviderAuth({
3783
+ provider,
3784
+ apiKey,
3785
+ apiKeyPool: deps.apiKeyPool ?? null,
3786
+ providerId,
3787
+ sessionId: route.sessionId
3788
+ });
3789
+ const { chain } = await resolveProviderChain(deps.llmConfig, providerId, resolvedModel);
3790
+ const transformerProvider = {
3791
+ name: provider.name,
3792
+ baseUrl: provider.api_base_url,
3793
+ apiKey,
3794
+ models: provider.models || []
3795
+ };
3796
+ const byoUrl = buildProviderApiUrl(provider, { model: resolvedModel, stream: isStream });
3797
+ const sameFormat = route.targetProviderFormat === "anthropic" || resolveApiFormat(provider) === "anthropic";
3798
+ return {
3799
+ auth,
3800
+ chain,
3801
+ transformerProvider,
3802
+ resolvedModel,
3803
+ isStream,
3804
+ resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : byoUrl,
3805
+ upstreamUrl: byoUrl,
3806
+ sameFormat,
3807
+ isSubscription: false,
3808
+ provider,
3809
+ apiKey,
3810
+ extendedContextEnabled: route.anthropicSdkHints?.extendedContext?.enabled ?? false
3811
+ };
3812
+ }
3813
+ async function runSameFormatFetch(rawBody, plan, options, keyOverride) {
3814
+ const { provider, apiKey, resolvedModel, isStream, extendedContextEnabled } = plan;
3815
+ if (!provider) {
3816
+ throw new Error("[ProviderProxy:anthropic] same-format BYO path requires a provider row");
3817
+ }
3818
+ const effectiveKey = keyOverride ?? apiKey ?? "";
3819
+ const headers = getProviderHeaders(provider, effectiveKey);
3820
+ const callerBeta = options.callerAnthropicBeta?.trim();
3821
+ if (callerBeta) {
3822
+ const existing = headers["anthropic-beta"] ?? headers["Anthropic-Beta"];
3823
+ const parts = new Set(
3824
+ [existing, callerBeta].filter((v) => typeof v === "string" && v.length > 0).flatMap((v) => v.split(",").map((s) => s.trim()).filter((s) => s.length > 0))
3825
+ );
3826
+ if (parts.size > 0) {
3827
+ delete headers["Anthropic-Beta"];
3828
+ headers["anthropic-beta"] = [...parts].join(",");
3829
+ }
3830
+ }
3831
+ injectExtendedContextBeta(headers, resolvedModel, extendedContextEnabled ?? false);
3832
+ const url = buildProviderApiUrl(provider, { model: resolvedModel, stream: isStream });
3833
+ console.info(`[ProviderProxy:anthropic] (same-format) -> ${url} model=${resolvedModel} stream=${isStream}`);
3834
+ const response = await fetch(url, { method: "POST", headers, body: rawBody });
3835
+ return { response, rawStatus: response.status };
3836
+ }
3837
+ async function runPipelineWithPoolReporting(anthropicBody, rawBody, plan, options) {
3838
+ const runOnce = (keyOverride) => plan.sameFormat ? runSameFormatFetch(rawBody, plan, options, keyOverride) : runPipeline(anthropicBody, plan);
3839
+ const first = await runOnce();
3840
+ const outcome = await plan.auth.onResult?.(first.rawStatus);
3841
+ if (outcome?.rebound) {
3842
+ console.info(
3843
+ "[ProviderProxy:anthropic] pool re-bound key after status",
3844
+ first.rawStatus,
3845
+ "\u2192 retrying once"
3846
+ );
3847
+ return runOnce(outcome.newKey);
3848
+ }
3849
+ return first;
3850
+ }
3851
+
3852
+ // src/provider-proxy/ingress/anthropicMessagesIngress.ts
3853
+ function isAnthropicMessagesRequest(method, url) {
3854
+ return method === "POST" && !!url && url.includes("/v1/messages");
3855
+ }
3856
+ async function handleAnthropicMessagesRequest(req, res, route, deps) {
3857
+ const handlerFactory = deps.anthropicIngressHandlerFactory;
3858
+ if (!handlerFactory) {
3859
+ const rawBody = await readBody(req);
3860
+ const callerBetaRaw = req.headers["anthropic-beta"];
3861
+ const callerAnthropicBeta = Array.isArray(callerBetaRaw) ? callerBetaRaw.join(",") : callerBetaRaw;
3862
+ await handleAnthropicMessagesByo(res, rawBody, route, deps, { callerAnthropicBeta });
3863
+ return;
3864
+ }
3865
+ const hints = route.anthropicSdkHints;
3866
+ if (!hints) {
3867
+ writeError(res, 502, "Anthropic route is missing anthropicSdkHints");
3868
+ return;
3869
+ }
3870
+ if (!hints.passThrough && !route.providerId) {
3871
+ writeError(res, 502, "Anthropic route is missing a providerId");
3872
+ return;
3873
+ }
3874
+ const handler = handlerFactory({
3875
+ llmConfig: deps.llmConfig,
3876
+ providerId: route.providerId ?? "",
3877
+ model: route.model,
3878
+ apiKey: hints.apiKey,
3879
+ backgroundTaskModel: route.backgroundTaskModel,
3880
+ isOfficialProvider: hints.isOfficialProvider,
3881
+ thinkingLevel: hints.thinkingLevel,
3882
+ extendedContext: hints.extendedContext ?? null,
3883
+ passThrough: hints.passThrough,
3884
+ passThroughAuthToken: hints.passThroughAuthToken ?? null,
3885
+ resolvePassThroughAuthToken: hints.resolvePassThroughAuthToken ?? null,
3886
+ subscriptionProfile: hints.subscriptionProfile ?? null,
3887
+ maxConcurrency: hints.maxConcurrency,
3888
+ webSearchService: hints.webSearchService ?? null,
3889
+ onRetry: hints.onRetry,
3890
+ onStreamEvent: hints.onStreamEvent,
3891
+ usageRecorder: hints.usageRecorder ?? null,
3892
+ attribution: hints.attribution ?? null
3893
+ });
3894
+ await handler.handle(req, res);
3895
+ }
3896
+
3897
+ // src/provider-proxy/usage/recordGeminiUsage.ts
3898
+ function readGeminiUsage(usage) {
3899
+ if (!usage) return null;
3900
+ const promptTokens = Number(usage.promptTokenCount) || 0;
3901
+ const candidatesTokens = Number(usage.candidatesTokenCount) || 0;
3902
+ const cacheReadTokens = Number(usage.cachedContentTokenCount) || 0;
3903
+ const reasoningTokens = Number(usage.thoughtsTokenCount) || 0;
3904
+ const billableInput = Math.max(0, promptTokens - cacheReadTokens);
3905
+ return {
3906
+ inputTokens: billableInput,
3907
+ outputTokens: candidatesTokens,
3908
+ cacheReadTokens,
3909
+ cacheCreationTokens: 0,
3910
+ reasoningTokens
3911
+ };
3912
+ }
3913
+ function recordGeminiNonStreamUsage(recorder, bodyText, attribution) {
3914
+ try {
3915
+ const parsed = JSON.parse(bodyText);
3916
+ const tapped = readGeminiUsage(parsed.usageMetadata);
3917
+ if (!tapped) return;
3918
+ recorder.record({
3919
+ messageId: null,
3920
+ parentMessageId: null,
3921
+ sessionId: attribution.sessionId,
3922
+ providerId: attribution.providerId,
3923
+ model: attribution.model,
3924
+ apiKeyId: null,
3925
+ engineOrigin: "codex-ingress",
3926
+ usage: tapped,
3927
+ rawUsage: parsed.usageMetadata
3928
+ });
3929
+ } catch {
3930
+ }
3931
+ }
3932
+
3933
+ // src/provider-proxy/ingress/geminiGenerateContentIngress.ts
3934
+ var GENERATE_CONTENT_ACTION = ":generateContent";
3935
+ var STREAM_GENERATE_CONTENT_ACTION = ":streamGenerateContent";
3936
+ function isGeminiGenerateContentRequest(method, url) {
3937
+ if (method !== "POST" || !url) return false;
3938
+ const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
3939
+ const lastSegment = path.split("/").pop() ?? "";
3940
+ return lastSegment.includes(STREAM_GENERATE_CONTENT_ACTION) || lastSegment.includes(GENERATE_CONTENT_ACTION);
3941
+ }
3942
+ function isGeminiStreamRequest(url) {
3943
+ if (!url) return false;
3944
+ const [path, query = ""] = url.split("?");
3945
+ const lastSegment = (path?.replace(/\/+$/, "") ?? "").split("/").pop() ?? "";
3946
+ if (lastSegment.includes(STREAM_GENERATE_CONTENT_ACTION)) return true;
3947
+ return /(^|&)alt=sse(&|$)/.test(query);
3948
+ }
3949
+ async function handleGeminiGenerateContentRequest(res, rawBody, url, route, deps) {
3950
+ let geminiBody;
3951
+ try {
3952
+ geminiBody = JSON.parse(rawBody);
3953
+ } catch {
3954
+ writeError(res, 400, "Invalid JSON in request body");
3955
+ return;
3956
+ }
3957
+ if (route.authMode === "subscription") {
3958
+ writeError(res, 502, "Gemini generateContent ingress does not support subscription auth");
3959
+ return;
3960
+ }
3961
+ const isStream = isGeminiStreamRequest(url);
3962
+ geminiBody.stream = isStream;
3963
+ const resolvedModel = route.model;
3964
+ geminiBody.model = resolvedModel;
3965
+ try {
3966
+ const plan = await buildByoPlan2(res, route, deps, resolvedModel, isStream);
3967
+ if (!plan) return;
3968
+ const providerResponse = await runPipelineWithPoolReporting2(geminiBody, plan);
3969
+ const bodyText = await relayResponse(res, providerResponse.response, isStream);
3970
+ if (bodyText && deps.usageRecorder) {
3971
+ recordGeminiNonStreamUsage(deps.usageRecorder, bodyText, {
3972
+ sessionId: route.sessionId,
3973
+ providerId: route.providerId ?? "gemini",
3974
+ model: resolvedModel
3975
+ });
3976
+ }
3977
+ } catch (err) {
3978
+ const errMsg = serializeError(err);
3979
+ console.error("[ProviderProxy:gemini] Pipeline error:", errMsg);
3980
+ writeError(res, 502, errMsg);
3981
+ }
3982
+ }
3983
+ async function buildByoPlan2(res, route, deps, resolvedModel, isStream) {
3984
+ const providerId = route.providerId;
3985
+ if (!providerId) {
3986
+ writeError(res, 502, "BYO route is missing a providerId");
3987
+ return null;
3988
+ }
3989
+ const provider = await deps.llmConfig.getProvider(providerId);
3990
+ if (!provider) {
3991
+ writeError(res, 502, `Provider not found: ${providerId}`);
3992
+ return null;
3993
+ }
3994
+ const apiKey = await resolvePoolBoundKey(deps, providerId, provider, route.sessionId);
3995
+ if (!apiKey) {
3996
+ writeError(res, 502, "API key not configured");
3997
+ return null;
3998
+ }
3999
+ const auth = route.auth ?? new LlmConfigProviderAuth({
4000
+ provider,
4001
+ apiKey,
4002
+ apiKeyPool: deps.apiKeyPool ?? null,
4003
+ providerId,
4004
+ sessionId: route.sessionId
4005
+ });
4006
+ const { chain } = await resolveProviderChain(deps.llmConfig, providerId, resolvedModel);
4007
+ const transformerProvider = {
4008
+ name: provider.name,
4009
+ baseUrl: provider.api_base_url,
4010
+ apiKey,
4011
+ models: provider.models || []
4012
+ };
4013
+ const byoUrl = buildProviderApiUrl(provider, { model: resolvedModel, stream: isStream });
4014
+ return {
4015
+ auth,
4016
+ chain,
4017
+ transformerProvider,
4018
+ resolvedModel,
4019
+ isStream,
4020
+ resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : byoUrl,
4021
+ upstreamUrl: byoUrl
4022
+ };
4023
+ }
4024
+ async function runPipeline2(geminiBody, plan) {
4025
+ const executor = getSharedExecutor();
4026
+ const endpointTransformer = getGeminiEndpointTransformer();
4027
+ const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
4028
+ const authHeaders = {};
4029
+ await auth.applyHeaders(authHeaders, { upstreamUrl, model: resolvedModel });
4030
+ let rawStatus = null;
4031
+ const { response } = await executeProviderCall({
4032
+ executor,
4033
+ request: geminiBody,
4034
+ provider: transformerProvider,
4035
+ chain,
4036
+ endpointTransformer,
4037
+ resolveUrl,
4038
+ buildHeaders: (config) => {
4039
+ const headers = { ...authHeaders };
4040
+ if (config.headers) {
4041
+ for (const [key, value] of Object.entries(config.headers)) {
4042
+ if (value !== void 0 && !(key in headers)) headers[key] = value;
4043
+ }
4044
+ }
4045
+ return headers;
4046
+ },
4047
+ fetchFn: (url, headers, body) => {
4048
+ console.log(`[ProviderProxy:gemini] -> ${url} model=${resolvedModel} stream=${isStream}`);
4049
+ return fetch(url, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => {
4050
+ rawStatus = r.status;
4051
+ return r;
4052
+ });
4053
+ },
4054
+ runResponseChain: true
4055
+ });
4056
+ return { response, rawStatus };
4057
+ }
4058
+ async function runPipelineWithPoolReporting2(geminiBody, plan) {
4059
+ const first = await runPipeline2(geminiBody, plan);
4060
+ const outcome = await plan.auth.onResult?.(first.rawStatus);
4061
+ if (outcome?.rebound) {
4062
+ console.log(
4063
+ "[ProviderProxy:gemini] pool re-bound key after status",
4064
+ first.rawStatus,
4065
+ "\u2192 retrying once"
4066
+ );
4067
+ return runPipeline2(geminiBody, plan);
4068
+ }
4069
+ return first;
4070
+ }
4071
+
4072
+ // src/provider-proxy/usage/recordChatCompletionsUsage.ts
4073
+ function readChatCompletionsUsage(usage) {
4074
+ if (!usage) return null;
4075
+ const promptTokens = Number(usage.prompt_tokens) || 0;
4076
+ const completionTokens = Number(usage.completion_tokens) || 0;
4077
+ const promptDetails = usage.prompt_tokens_details ?? {};
4078
+ const cacheReadTokens = Number(promptDetails.cached_tokens) || 0;
4079
+ const completionDetails = usage.completion_tokens_details ?? {};
4080
+ const reasoningTokens = Number(completionDetails.reasoning_tokens) || 0;
4081
+ const billableInput = Math.max(0, promptTokens - cacheReadTokens);
4082
+ return {
4083
+ inputTokens: billableInput,
4084
+ outputTokens: completionTokens,
4085
+ cacheReadTokens,
4086
+ cacheCreationTokens: 0,
4087
+ reasoningTokens
4088
+ };
4089
+ }
4090
+ function recordChatCompletionsNonStreamUsage(recorder, bodyText, attribution) {
4091
+ try {
4092
+ const parsed = JSON.parse(bodyText);
4093
+ const tapped = readChatCompletionsUsage(parsed.usage);
4094
+ if (!tapped) return;
4095
+ recorder.record({
4096
+ messageId: null,
4097
+ parentMessageId: null,
4098
+ sessionId: attribution.sessionId,
4099
+ providerId: attribution.providerId,
4100
+ model: attribution.model,
4101
+ apiKeyId: null,
4102
+ engineOrigin: "codex-ingress",
4103
+ usage: tapped,
4104
+ rawUsage: parsed.usage
4105
+ });
4106
+ } catch {
4107
+ }
4108
+ }
4109
+
4110
+ // src/provider-proxy/ingress/openaiChatIngress.ts
4111
+ function isOpenAIChatRequest(method, url) {
4112
+ if (method !== "POST" || !url) return false;
4113
+ const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
4114
+ return path.endsWith("/chat/completions");
4115
+ }
4116
+ async function handleOpenAIChatRequest(res, rawBody, route, deps) {
4117
+ let chatBody;
4118
+ try {
4119
+ chatBody = JSON.parse(rawBody);
4120
+ } catch {
4121
+ writeError(res, 400, "Invalid JSON in request body");
4122
+ return;
4123
+ }
4124
+ if (route.authMode === "subscription") {
4125
+ writeError(res, 502, "OpenAI Chat Completions ingress does not support subscription auth");
4126
+ return;
4127
+ }
4128
+ const isStream = chatBody.stream === true;
4129
+ const resolvedModel = route.model;
4130
+ chatBody.model = resolvedModel;
4131
+ try {
4132
+ const plan = await buildByoPlan3(res, route, deps, resolvedModel, isStream);
4133
+ if (!plan) return;
4134
+ const providerResponse = await runPipelineWithPoolReporting3(chatBody, plan);
4135
+ const bodyText = await relayResponse(res, providerResponse.response, isStream);
4136
+ if (bodyText && deps.usageRecorder) {
4137
+ recordChatCompletionsNonStreamUsage(deps.usageRecorder, bodyText, {
4138
+ sessionId: route.sessionId,
4139
+ providerId: route.providerId ?? "openai",
4140
+ model: resolvedModel
4141
+ });
4142
+ }
4143
+ } catch (err) {
4144
+ const errMsg = serializeError(err);
4145
+ console.error("[ProviderProxy:chat] Pipeline error:", errMsg);
4146
+ writeError(res, 502, errMsg);
4147
+ }
4148
+ }
4149
+ async function buildByoPlan3(res, route, deps, resolvedModel, isStream) {
4150
+ const providerId = route.providerId;
4151
+ if (!providerId) {
4152
+ writeError(res, 502, "BYO route is missing a providerId");
4153
+ return null;
4154
+ }
4155
+ const provider = await deps.llmConfig.getProvider(providerId);
4156
+ if (!provider) {
4157
+ writeError(res, 502, `Provider not found: ${providerId}`);
4158
+ return null;
4159
+ }
4160
+ const apiKey = await resolvePoolBoundKey(deps, providerId, provider, route.sessionId);
4161
+ if (!apiKey) {
4162
+ writeError(res, 502, "API key not configured");
4163
+ return null;
4164
+ }
4165
+ const auth = route.auth ?? new LlmConfigProviderAuth({
4166
+ provider,
4167
+ apiKey,
4168
+ apiKeyPool: deps.apiKeyPool ?? null,
4169
+ providerId,
4170
+ sessionId: route.sessionId
4171
+ });
4172
+ const { chain } = await resolveProviderChain(deps.llmConfig, providerId, resolvedModel);
4173
+ const transformerProvider = {
4174
+ name: provider.name,
4175
+ baseUrl: provider.api_base_url,
4176
+ apiKey,
4177
+ models: provider.models || []
4178
+ };
4179
+ const byoUrl = buildProviderApiUrl(provider, { model: resolvedModel, stream: isStream });
4180
+ return {
4181
+ auth,
4182
+ chain,
4183
+ transformerProvider,
4184
+ resolvedModel,
4185
+ isStream,
4186
+ resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : byoUrl,
4187
+ upstreamUrl: byoUrl
4188
+ };
4189
+ }
4190
+ async function runPipeline3(chatBody, plan) {
4191
+ const executor = getSharedExecutor();
4192
+ const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
4193
+ const authHeaders = {};
4194
+ await auth.applyHeaders(authHeaders, { upstreamUrl, model: resolvedModel });
4195
+ let rawStatus = null;
4196
+ const { response } = await executeProviderCall({
4197
+ executor,
4198
+ request: chatBody,
4199
+ provider: transformerProvider,
4200
+ chain,
4201
+ resolveUrl,
4202
+ buildHeaders: (config) => {
4203
+ const headers = { ...authHeaders };
4204
+ if (config.headers) {
4205
+ for (const [key, value] of Object.entries(config.headers)) {
4206
+ if (value !== void 0 && !(key in headers)) headers[key] = value;
4207
+ }
4208
+ }
4209
+ return headers;
4210
+ },
4211
+ fetchFn: (url, headers, body) => {
4212
+ console.log(`[ProviderProxy:chat] -> ${url} model=${resolvedModel} stream=${isStream}`);
4213
+ return fetch(url, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => {
4214
+ rawStatus = r.status;
4215
+ return r;
4216
+ });
4217
+ },
4218
+ runResponseChain: true
4219
+ });
4220
+ return { response, rawStatus };
4221
+ }
4222
+ async function runPipelineWithPoolReporting3(chatBody, plan) {
4223
+ const first = await runPipeline3(chatBody, plan);
4224
+ const outcome = await plan.auth.onResult?.(first.rawStatus);
4225
+ if (outcome?.rebound) {
4226
+ console.log(
4227
+ "[ProviderProxy:chat] pool re-bound key after status",
4228
+ first.rawStatus,
4229
+ "\u2192 retrying once"
4230
+ );
4231
+ return runPipeline3(chatBody, plan);
4232
+ }
4233
+ return first;
4234
+ }
4235
+
4236
+ // src/ports/gemini-code-assist-resolver.ts
4237
+ var resolver = null;
4238
+ function getGeminiCodeAssistResolver() {
4239
+ return resolver;
4240
+ }
4241
+
4242
+ // src/provider-proxy/usage/recordResponsesUsage.ts
4243
+ function readResponsesUsage(usage) {
4244
+ if (!usage) return null;
4245
+ const inputTokens = Number(usage.input_tokens) || 0;
4246
+ const outputTokens = Number(usage.output_tokens) || 0;
4247
+ const inputDetails = usage.input_tokens_details ?? {};
4248
+ const cacheReadTokens = Number(inputDetails.cached_tokens) || 0;
4249
+ const outputDetails = usage.output_tokens_details ?? {};
4250
+ const reasoningTokens = Number(outputDetails.reasoning_tokens) || 0;
4251
+ const billableInput = Math.max(0, inputTokens - cacheReadTokens);
4252
+ return {
4253
+ inputTokens: billableInput,
4254
+ outputTokens,
4255
+ cacheReadTokens,
4256
+ cacheCreationTokens: 0,
4257
+ reasoningTokens
4258
+ };
4259
+ }
4260
+ function recordResponsesNonStreamUsage(recorder, bodyText, attribution) {
4261
+ try {
4262
+ const parsed = JSON.parse(bodyText);
4263
+ const tapped = readResponsesUsage(parsed.usage);
4264
+ if (!tapped) return;
4265
+ recorder.record({
4266
+ messageId: null,
4267
+ parentMessageId: null,
4268
+ sessionId: attribution.sessionId,
4269
+ providerId: attribution.providerId,
4270
+ model: attribution.model,
4271
+ apiKeyId: null,
4272
+ engineOrigin: "codex-ingress",
4273
+ usage: tapped,
4274
+ rawUsage: parsed.usage
4275
+ });
4276
+ } catch {
4277
+ }
4278
+ }
4279
+
4280
+ // src/provider-proxy/ingress/openaiResponsesIngress.ts
4281
+ function isOpenAIResponsesRequest(method, url) {
4282
+ if (method !== "POST" || !url) return false;
4283
+ const path = url.split("?")[0]?.replace(/\/+$/, "") ?? "";
4284
+ return path.endsWith("/responses");
4285
+ }
4286
+ async function handleOpenAIResponsesRequest(res, rawBody, route, deps) {
4287
+ let responsesBody;
4288
+ try {
4289
+ responsesBody = JSON.parse(rawBody);
4290
+ } catch {
4291
+ writeError(res, 400, "Invalid JSON in request body");
4292
+ return;
4293
+ }
4294
+ const isStream = responsesBody.stream === true;
4295
+ const resolvedModel = route.model;
4296
+ responsesBody.model = resolvedModel;
4297
+ try {
4298
+ const plan = route.authMode === "subscription" ? await buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) : await buildByoPlan4(res, route, deps, resolvedModel, isStream);
4299
+ if (!plan) return;
4300
+ const providerResponse = route.authMode === "subscription" ? await runPipelineWithSubscriptionRetry2(responsesBody, plan) : await runPipelineWithPoolReporting4(responsesBody, plan);
4301
+ const bodyText = await relayResponse(res, providerResponse.response, isStream);
4302
+ if (bodyText && deps.usageRecorder) {
4303
+ recordResponsesNonStreamUsage(deps.usageRecorder, bodyText, {
4304
+ sessionId: route.sessionId,
4305
+ providerId: route.providerId ?? "codex",
4306
+ model: resolvedModel
4307
+ });
4308
+ }
4309
+ } catch (err) {
4310
+ const errMsg = serializeError(err);
4311
+ console.error("[ProviderProxy:responses] Pipeline error:", errMsg);
4312
+ writeError(res, 502, errMsg);
4313
+ }
4314
+ }
4315
+ async function buildByoPlan4(res, route, deps, resolvedModel, isStream) {
4316
+ const providerId = route.providerId;
4317
+ if (!providerId) {
4318
+ writeError(res, 502, "BYO route is missing a providerId");
4319
+ return null;
4320
+ }
4321
+ const provider = await deps.llmConfig.getProvider(providerId);
4322
+ if (!provider) {
4323
+ writeError(res, 502, `Provider not found: ${providerId}`);
4324
+ return null;
4325
+ }
4326
+ const apiKey = await resolvePoolBoundKey(deps, providerId, provider, route.sessionId);
4327
+ if (!apiKey) {
4328
+ writeError(res, 502, "API key not configured");
4329
+ return null;
4330
+ }
4331
+ const auth = route.auth ?? new LlmConfigProviderAuth({
4332
+ provider,
4333
+ apiKey,
4334
+ apiKeyPool: deps.apiKeyPool ?? null,
4335
+ providerId,
4336
+ sessionId: route.sessionId
4337
+ });
4338
+ const { chain } = await resolveProviderChain(deps.llmConfig, providerId, resolvedModel);
4339
+ const transformerProvider = {
4340
+ name: provider.name,
4341
+ baseUrl: provider.api_base_url,
4342
+ apiKey,
4343
+ models: provider.models || []
4344
+ };
4345
+ const byoUrl = buildProviderApiUrl(provider, { model: resolvedModel, stream: isStream });
4346
+ return {
4347
+ auth,
4348
+ chain,
4349
+ transformerProvider,
4350
+ resolvedModel,
4351
+ isStream,
4352
+ resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : byoUrl,
4353
+ upstreamUrl: byoUrl
4354
+ };
4355
+ }
4356
+ async function buildSubscriptionPlan2(res, route, deps, resolvedModel, isStream) {
4357
+ const profile = route.subscriptionProfile;
4358
+ if (!profile) {
4359
+ writeError(res, 502, "Subscription mode requires a codex subscription profile");
4360
+ return null;
4361
+ }
4362
+ const upstreamUrl = profile.resolveUpstreamUrl?.(resolvedModel);
4363
+ if (!upstreamUrl) {
4364
+ writeError(res, 502, "Subscription profile is missing resolveUpstreamUrl");
4365
+ return null;
4366
+ }
4367
+ const auth = route.auth ?? new SubscriptionAuthSource(profile);
4368
+ const chain = resolveSubscriptionChain(
4369
+ profile,
4370
+ deps.llmConfig.getTransformerService(),
4371
+ getResponsesEndpointTransformer()
4372
+ );
4373
+ const transformerProvider = {
4374
+ name: profile.authStrategy.providerId,
4375
+ baseUrl: upstreamUrl,
4376
+ apiKey: "",
4377
+ models: [resolvedModel]
4378
+ };
4379
+ if (profile.authStrategy.providerId === "gemini") {
4380
+ try {
4381
+ transformerProvider.geminiProject = await resolveGeminiCodeAssistProject(profile);
4382
+ } catch (err) {
4383
+ writeError(res, 502, serializeError(err));
4384
+ return null;
4385
+ }
4386
+ }
4387
+ return {
4388
+ auth,
4389
+ chain,
4390
+ transformerProvider,
4391
+ resolvedModel,
4392
+ isStream,
4393
+ // Prefer the transformer-supplied URL (Code Assist carries the correct
4394
+ // stream vs non-stream colon-method URL in `config.url`); fall back to the
4395
+ // profile's `resolveUpstreamUrl` for non-Code-Assist subscriptions.
4396
+ resolveUrl: (config) => config.url instanceof URL ? config.url.toString() : typeof config.url === "string" ? config.url : upstreamUrl,
4397
+ upstreamUrl
4398
+ };
4399
+ }
4400
+ async function resolveGeminiCodeAssistProject(profile) {
4401
+ const probe = {};
4402
+ await profile.authStrategy.applyHeaders(probe);
4403
+ const bearer = probe.Authorization ?? probe.authorization ?? "";
4404
+ const accessToken = bearer.replace(/^Bearer\s+/i, "").trim();
4405
+ if (!accessToken) return void 0;
4406
+ const resolver2 = getGeminiCodeAssistResolver();
4407
+ if (!resolver2) return void 0;
4408
+ return resolver2.resolveProject(accessToken);
4409
+ }
4410
+ async function runPipeline4(responsesBody, plan) {
4411
+ const executor = getSharedExecutor();
4412
+ const endpointTransformer = getResponsesEndpointTransformer();
4413
+ const { auth, chain, transformerProvider, resolvedModel, isStream, resolveUrl, upstreamUrl } = plan;
4414
+ const authHeaders = {};
4415
+ await auth.applyHeaders(authHeaders, { upstreamUrl, model: resolvedModel });
4416
+ let rawStatus = null;
4417
+ const { response } = await executeProviderCall({
4418
+ executor,
4419
+ request: responsesBody,
4420
+ provider: transformerProvider,
4421
+ chain,
4422
+ endpointTransformer,
4423
+ resolveUrl,
4424
+ buildHeaders: (config) => {
4425
+ const headers = { ...authHeaders };
4426
+ if (config.headers) {
4427
+ for (const [key, value] of Object.entries(config.headers)) {
4428
+ if (value !== void 0 && !(key in headers)) headers[key] = value;
4429
+ }
4430
+ }
4431
+ return headers;
4432
+ },
4433
+ fetchFn: (url, headers, body) => {
4434
+ console.log(`[ProviderProxy:responses] -> ${url} model=${resolvedModel} stream=${isStream}`);
4435
+ return fetch(url, { method: "POST", headers, body: JSON.stringify(body) }).then((r) => {
4436
+ rawStatus = r.status;
4437
+ return r;
4438
+ });
4439
+ },
4440
+ runResponseChain: true
4441
+ });
4442
+ return { response, rawStatus };
4443
+ }
4444
+ async function runPipelineWithPoolReporting4(responsesBody, plan) {
4445
+ const first = await runPipeline4(responsesBody, plan);
4446
+ const outcome = await plan.auth.onResult?.(first.rawStatus);
4447
+ if (outcome?.rebound) {
4448
+ console.log(
4449
+ "[ProviderProxy:responses] pool re-bound key after status",
4450
+ first.rawStatus,
4451
+ "\u2192 retrying once"
4452
+ );
4453
+ return runPipeline4(responsesBody, plan);
4454
+ }
4455
+ return first;
4456
+ }
4457
+ async function runPipelineWithSubscriptionRetry2(responsesBody, plan) {
4458
+ const first = await runPipeline4(responsesBody, plan);
4459
+ if (first.rawStatus !== 401) return first;
4460
+ const refreshed = await plan.auth.onUnauthorized?.();
4461
+ if (!refreshed) {
4462
+ console.warn("[ProviderProxy:responses] 401 not recoverable (onUnauthorized returned false)");
4463
+ return first;
4464
+ }
4465
+ console.log("[ProviderProxy:responses] 401 \u2192 token refreshed; retrying once");
4466
+ return runPipeline4(responsesBody, plan);
4467
+ }
4468
+
4469
+ // src/provider-proxy/providerProxyRouter.ts
4470
+ function extractRouteToken(authHeader) {
4471
+ if (!authHeader) return void 0;
4472
+ const trimmed = authHeader.trim();
4473
+ if (!trimmed) return void 0;
4474
+ const m = /^Bearer\s+(.+)$/i.exec(trimmed);
4475
+ return (m ? m[1] : trimmed).trim() || void 0;
4476
+ }
4477
+ function resolveRouteToken(req) {
4478
+ const fromAuth = extractRouteToken(req.headers["authorization"]);
4479
+ if (fromAuth) return fromAuth;
4480
+ const googKey = req.headers["x-goog-api-key"];
4481
+ const value = Array.isArray(googKey) ? googKey[0] : googKey;
4482
+ return value?.trim() || void 0;
4483
+ }
4484
+ async function routeRequest(req, res, routes, deps) {
4485
+ const token = resolveRouteToken(req);
4486
+ const route = routes.lookup(token);
4487
+ if (!route) {
4488
+ writeError(res, 401, "Invalid or expired route token");
4489
+ return;
4490
+ }
4491
+ const method = req.method;
4492
+ const url = req.url;
4493
+ if (isAnthropicMessagesRequest(method, url)) {
4494
+ await handleAnthropicMessagesRequest(req, res, route, deps);
4495
+ return;
4496
+ }
4497
+ if (isOpenAIResponsesRequest(method, url)) {
4498
+ const rawBody = await readBody(req);
4499
+ await handleOpenAIResponsesRequest(res, rawBody, route, deps);
4500
+ return;
4501
+ }
4502
+ if (isOpenAIChatRequest(method, url)) {
4503
+ const rawBody = await readBody(req);
4504
+ await handleOpenAIChatRequest(res, rawBody, route, deps);
4505
+ return;
4506
+ }
4507
+ if (isGeminiGenerateContentRequest(method, url)) {
4508
+ const rawBody = await readBody(req);
4509
+ await handleGeminiGenerateContentRequest(res, rawBody, url, route, deps);
4510
+ return;
4511
+ }
4512
+ writeError(res, 404, `Unsupported: ${method} ${url}`);
4513
+ }
4514
+
4515
+ // src/provider-proxy/ProviderProxy.ts
4516
+ function isLoopbackAddress(addr) {
4517
+ if (!addr) return false;
4518
+ const normalized = addr.startsWith("::ffff:") ? addr.slice("::ffff:".length) : addr;
4519
+ return normalized === "127.0.0.1" || normalized.startsWith("127.") || normalized === "::1" || addr === "::1";
4520
+ }
4521
+ var ProviderProxy = class {
4522
+ constructor(deps, routes) {
4523
+ this.deps = deps;
4524
+ this.routes = routes ?? new ProviderProxyRouteMap();
4525
+ }
4526
+ deps;
4527
+ server = null;
4528
+ port = 0;
4529
+ routes;
4530
+ /**
4531
+ * Start the resident listener on a stable port on 127.0.0.1. Idempotent —
4532
+ * a second `start()` returns the already-bound port.
4533
+ */
4534
+ async start() {
4535
+ if (this.server) return this.port;
4536
+ return new Promise((resolve, reject) => {
4537
+ const server = http2.createServer((req, res) => {
4538
+ const peer = req.socket.remoteAddress;
4539
+ if (!isLoopbackAddress(peer)) {
4540
+ res.writeHead(403, { "Content-Type": "application/json" });
4541
+ res.end(JSON.stringify({ error: { type: "provider_proxy_error", message: "Loopback only" } }));
4542
+ req.socket.destroy();
4543
+ return;
4544
+ }
4545
+ routeRequest(req, res, this.routes, this.deps).catch((err) => {
4546
+ const errMsg = serializeError(err);
4547
+ console.error("[ProviderProxy] Unhandled error:", errMsg);
4548
+ if (!res.headersSent) {
4549
+ res.writeHead(500, { "Content-Type": "application/json" });
4550
+ res.end(JSON.stringify({ error: { type: "provider_proxy_error", message: errMsg } }));
4551
+ }
4552
+ });
4553
+ });
4554
+ server.listen(0, "127.0.0.1", () => {
4555
+ const addr = server.address();
4556
+ if (addr && typeof addr === "object") {
4557
+ this.port = addr.port;
4558
+ this.server = server;
4559
+ console.log(`[ProviderProxy] Listening on 127.0.0.1:${this.port}`);
4560
+ resolve(this.port);
4561
+ } else {
4562
+ reject(new Error("Failed to get server address"));
4563
+ }
4564
+ });
4565
+ server.on("error", reject);
4566
+ });
4567
+ }
4568
+ /** Stop the listener, clear all routes, and release the port. */
4569
+ async stop() {
4570
+ this.routes.clear();
4571
+ const server = this.server;
4572
+ if (!server) return;
4573
+ return new Promise((resolve) => {
4574
+ server.close(() => {
4575
+ console.log(`[ProviderProxy] Stopped (port=${this.port})`);
4576
+ this.server = null;
4577
+ this.port = 0;
4578
+ resolve();
4579
+ });
4580
+ });
4581
+ }
4582
+ /** Base URL for injector wiring (`ANTHROPIC_BASE_URL` / codex `base_url`). */
4583
+ getBaseUrl() {
4584
+ return `http://127.0.0.1:${this.port}`;
4585
+ }
4586
+ /**
4587
+ * The SHARED route map. Exposed so the outbound API server
4588
+ * (`outbound-api-server`) can mint per-request routes on the SAME map and
4589
+ * delegate to the existing `routeRequest()` dispatch — guaranteeing a single
4590
+ * conversion stack. Not used by the resident per-run flow.
4591
+ */
4592
+ getRouteMap() {
4593
+ return this.routes;
4594
+ }
4595
+ /**
4596
+ * The app-session deps the proxy services all routes with. Exposed so the
4597
+ * outbound server can pass them verbatim into `routeRequest()`.
4598
+ */
4599
+ getDeps() {
4600
+ return this.deps;
4601
+ }
4602
+ /** Register a route for one run; returns the crypto route token (task 2.2). */
4603
+ addRoute(context, idleMs) {
4604
+ return this.routes.addRoute(context, idleMs);
4605
+ }
4606
+ /** Remove a route at run end. Returns true if an entry existed. */
4607
+ removeRoute(token) {
4608
+ return this.routes.removeRoute(token);
4609
+ }
4610
+ /** Live-route count (diagnostics / tests). */
4611
+ routeCount() {
4612
+ return this.routes.size();
4613
+ }
4614
+ };
4615
+ export {
4616
+ ProviderProxy,
4617
+ isLoopbackAddress
4618
+ };