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