nexus-agents 10.1.0 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/{ast-rule-runner-AAOBENF2.js → ast-rule-runner-RI7ZJ4ZI.js} +3 -3
  2. package/dist/{child-mcp-config-FRWGQIHP.js → child-mcp-config-73BX4OTR.js} +2 -2
  3. package/dist/{chunk-NUXSGWDS.js → chunk-23QA6GQD.js} +5 -5
  4. package/dist/{chunk-EGUISJBB.js → chunk-2DYCHHWW.js} +4 -4
  5. package/dist/{chunk-TLITBKMZ.js → chunk-2TA6KTRF.js} +15 -15
  6. package/dist/{chunk-YHY7GMAL.js → chunk-4JJI34RV.js} +2 -2
  7. package/dist/{chunk-KPGWNBDA.js → chunk-7EV3QAKO.js} +2 -2
  8. package/dist/{chunk-SCQ5YAYO.js → chunk-D6E3LJGP.js} +4 -4
  9. package/dist/{chunk-XYYXVJSO.js → chunk-EJ5EBTAV.js} +2 -2
  10. package/dist/{chunk-BJLP4E6Q.js → chunk-FNHVTDHZ.js} +3 -3
  11. package/dist/{chunk-VGCRYAEH.js → chunk-FWQSOPBA.js} +4 -4
  12. package/dist/{chunk-RBFHY2LF.js → chunk-GAX75XCV.js} +2 -2
  13. package/dist/{chunk-O46IOND3.js → chunk-GX5ZHZR2.js} +7 -7
  14. package/dist/{chunk-33C22IY4.js → chunk-J44RKRGQ.js} +3 -3
  15. package/dist/{chunk-L4P4A7Z2.js → chunk-JGY5HMWI.js} +2 -2
  16. package/dist/{chunk-WCHN7B2Q.js → chunk-JUUGLXSN.js} +8 -10
  17. package/dist/{chunk-WCHN7B2Q.js.map → chunk-JUUGLXSN.js.map} +1 -1
  18. package/dist/{chunk-BK6Q3VQK.js → chunk-KASBBKAQ.js} +2 -2
  19. package/dist/chunk-KASBBKAQ.js.map +1 -0
  20. package/dist/chunk-KLSNDPU6.js +2412 -0
  21. package/dist/chunk-KLSNDPU6.js.map +1 -0
  22. package/dist/{chunk-HQWQ7MR5.js → chunk-LP5ISXKM.js} +4 -4
  23. package/dist/{chunk-QPU7QKPT.js → chunk-LXYT2P2Y.js} +3 -3
  24. package/dist/{chunk-C6MM6Z65.js → chunk-OMSKBBL7.js} +45 -27
  25. package/dist/chunk-OMSKBBL7.js.map +1 -0
  26. package/dist/{chunk-M4IJ3H6M.js → chunk-PXYSAG5B.js} +2 -2
  27. package/dist/{chunk-52Q72CPP.js → chunk-QG3VDSWD.js} +3 -3
  28. package/dist/{chunk-IRFKJ2KR.js → chunk-QM5W3T3O.js} +2 -2
  29. package/dist/{chunk-O3VPGING.js → chunk-QRC3QLUQ.js} +92 -95
  30. package/dist/chunk-QRC3QLUQ.js.map +1 -0
  31. package/dist/{chunk-MCWIMVG2.js → chunk-QS5HGPCN.js} +2 -2
  32. package/dist/{chunk-O5OUJ3PX.js → chunk-SS53PZO6.js} +5 -5
  33. package/dist/{chunk-VHOOJ4FG.js → chunk-SX2XWCGA.js} +2 -2
  34. package/dist/{chunk-2VD5OQQR.js → chunk-TDUXUVUU.js} +2 -2
  35. package/dist/{chunk-XFTTKOHK.js → chunk-U37AITDA.js} +2 -2
  36. package/dist/{chunk-V5FGXVSX.js → chunk-UZJ6IPJR.js} +5 -5
  37. package/dist/{chunk-MYM2TPNL.js → chunk-V26YQQK4.js} +2 -2
  38. package/dist/{chunk-HVZVVDFN.js → chunk-VLOPXV47.js} +5 -5
  39. package/dist/{chunk-AMK4BTAV.js → chunk-WD4DENPC.js} +3 -3
  40. package/dist/{chunk-WI73OZAU.js → chunk-WKFPKPEM.js} +13 -13
  41. package/dist/chunk-WKFPKPEM.js.map +1 -0
  42. package/dist/{chunk-IYEZVAJT.js → chunk-XDUTAUK3.js} +40 -42
  43. package/dist/{chunk-IYEZVAJT.js.map → chunk-XDUTAUK3.js.map} +1 -1
  44. package/dist/{cli-circuit-breaker-AM425BRL.js → cli-circuit-breaker-62YOQQT3.js} +3 -3
  45. package/dist/{cli-readiness-LWOMHZ34.js → cli-readiness-O6EOQV4L.js} +7 -7
  46. package/dist/cli.js +61 -56
  47. package/dist/cli.js.map +1 -1
  48. package/dist/{composite-router-VXQFXNET.js → composite-router-YPZ3D5Y6.js} +2 -2
  49. package/dist/{consensus-vote-V2JQHILD.js → consensus-vote-RNQQUBXO.js} +16 -17
  50. package/dist/{context-retriever-GEQIPCVL.js → context-retriever-HVPLM6DH.js} +8 -8
  51. package/dist/{doctor-deep-DSOZGB5J.js → doctor-deep-UZQTD65D.js} +3 -3
  52. package/dist/{doctor-gateway-report-RJOSOHGA.js → doctor-gateway-report-LRBPPJD4.js} +6 -7
  53. package/dist/{doctor-live-7LVRGOS5.js → doctor-live-QFLT5SDB.js} +11 -11
  54. package/dist/{expert-bridge-5A4ADUBO.js → expert-bridge-O3UZXLGB.js} +4 -4
  55. package/dist/{factory-UBAUIA2M.js → factory-KCKQQAEH.js} +10 -10
  56. package/dist/{factory-4RZLP7QV.js → factory-LUKUIB6C.js} +5 -5
  57. package/dist/{improvement-review-UIYSAI36.js → improvement-review-7IABFAU3.js} +6 -6
  58. package/dist/index.d.ts +20 -22
  59. package/dist/index.js +35 -37
  60. package/dist/index.js.map +1 -1
  61. package/dist/{init-opencode-WUN3CTBM.js → init-opencode-RYPBCM4P.js} +5 -6
  62. package/dist/{init-opencode-WUN3CTBM.js.map → init-opencode-RYPBCM4P.js.map} +1 -1
  63. package/dist/{issue-triage-VKEHVHO4.js → issue-triage-YBVJVAUU.js} +6 -6
  64. package/dist/{model-config-helpers-GYPIIXOP.js → model-config-helpers-QNV3KD2W.js} +2 -2
  65. package/dist/{openai-compat-adapter-GSDOJ7TQ.js → openai-compat-adapter-GPZDIR3D.js} +11 -8
  66. package/dist/{outcome-store-adapter-PXT7AL6T.js → outcome-store-adapter-WRONSTUD.js} +2 -2
  67. package/dist/{pr-reviewer-helpers-GUUT7G43.js → pr-reviewer-helpers-7SMNNDFJ.js} +4 -4
  68. package/dist/{registry-command-4G3WGKS5.js → registry-command-U7K6HIBK.js} +2 -2
  69. package/dist/{repo-security-plan-QO6ACTPZ.js → repo-security-plan-LFWLVEL2.js} +3 -3
  70. package/dist/{research-helpers-synthesize-MPEJHPYR.js → research-helpers-synthesize-X6MVAAKT.js} +4 -4
  71. package/dist/{routing-memory-VHQDJ6XZ.js → routing-memory-OCIVXKOU.js} +2 -2
  72. package/dist/{session-memory-M3CQFFL6.js → session-memory-4LMEWO3X.js} +3 -3
  73. package/dist/{setup-config-VGEBCEKX.js → setup-config-YNNNR4AE.js} +3 -3
  74. package/dist/{setup-custom-api-DX7CNYKT.js → setup-custom-api-QEPUCHOX.js} +3 -3
  75. package/dist/setup-data-dir-XM5DWH7R.js +35 -0
  76. package/dist/{tool-memory-7QUPGCWV.js → tool-memory-EFGFXF65.js} +5 -5
  77. package/dist/{unified-registry-CLZK3H64.js → unified-registry-KLNC24HA.js} +11 -11
  78. package/dist/{weather-report-OKMXF73V.js → weather-report-PMYQ3WBW.js} +2 -2
  79. package/package.json +1 -1
  80. package/dist/chunk-BK6Q3VQK.js.map +0 -1
  81. package/dist/chunk-C6MM6Z65.js.map +0 -1
  82. package/dist/chunk-K6M2ZWX5.js +0 -1116
  83. package/dist/chunk-K6M2ZWX5.js.map +0 -1
  84. package/dist/chunk-O3VPGING.js.map +0 -1
  85. package/dist/chunk-WI73OZAU.js.map +0 -1
  86. package/dist/chunk-XQMQGUJT.js +0 -995
  87. package/dist/chunk-XQMQGUJT.js.map +0 -1
  88. package/dist/setup-data-dir-75Z5PSSB.js +0 -36
  89. /package/dist/{ast-rule-runner-AAOBENF2.js.map → ast-rule-runner-RI7ZJ4ZI.js.map} +0 -0
  90. /package/dist/{child-mcp-config-FRWGQIHP.js.map → child-mcp-config-73BX4OTR.js.map} +0 -0
  91. /package/dist/{chunk-NUXSGWDS.js.map → chunk-23QA6GQD.js.map} +0 -0
  92. /package/dist/{chunk-EGUISJBB.js.map → chunk-2DYCHHWW.js.map} +0 -0
  93. /package/dist/{chunk-TLITBKMZ.js.map → chunk-2TA6KTRF.js.map} +0 -0
  94. /package/dist/{chunk-YHY7GMAL.js.map → chunk-4JJI34RV.js.map} +0 -0
  95. /package/dist/{chunk-KPGWNBDA.js.map → chunk-7EV3QAKO.js.map} +0 -0
  96. /package/dist/{chunk-SCQ5YAYO.js.map → chunk-D6E3LJGP.js.map} +0 -0
  97. /package/dist/{chunk-XYYXVJSO.js.map → chunk-EJ5EBTAV.js.map} +0 -0
  98. /package/dist/{chunk-BJLP4E6Q.js.map → chunk-FNHVTDHZ.js.map} +0 -0
  99. /package/dist/{chunk-VGCRYAEH.js.map → chunk-FWQSOPBA.js.map} +0 -0
  100. /package/dist/{chunk-RBFHY2LF.js.map → chunk-GAX75XCV.js.map} +0 -0
  101. /package/dist/{chunk-O46IOND3.js.map → chunk-GX5ZHZR2.js.map} +0 -0
  102. /package/dist/{chunk-33C22IY4.js.map → chunk-J44RKRGQ.js.map} +0 -0
  103. /package/dist/{chunk-L4P4A7Z2.js.map → chunk-JGY5HMWI.js.map} +0 -0
  104. /package/dist/{chunk-HQWQ7MR5.js.map → chunk-LP5ISXKM.js.map} +0 -0
  105. /package/dist/{chunk-QPU7QKPT.js.map → chunk-LXYT2P2Y.js.map} +0 -0
  106. /package/dist/{chunk-M4IJ3H6M.js.map → chunk-PXYSAG5B.js.map} +0 -0
  107. /package/dist/{chunk-52Q72CPP.js.map → chunk-QG3VDSWD.js.map} +0 -0
  108. /package/dist/{chunk-IRFKJ2KR.js.map → chunk-QM5W3T3O.js.map} +0 -0
  109. /package/dist/{chunk-MCWIMVG2.js.map → chunk-QS5HGPCN.js.map} +0 -0
  110. /package/dist/{chunk-O5OUJ3PX.js.map → chunk-SS53PZO6.js.map} +0 -0
  111. /package/dist/{chunk-VHOOJ4FG.js.map → chunk-SX2XWCGA.js.map} +0 -0
  112. /package/dist/{chunk-2VD5OQQR.js.map → chunk-TDUXUVUU.js.map} +0 -0
  113. /package/dist/{chunk-XFTTKOHK.js.map → chunk-U37AITDA.js.map} +0 -0
  114. /package/dist/{chunk-V5FGXVSX.js.map → chunk-UZJ6IPJR.js.map} +0 -0
  115. /package/dist/{chunk-MYM2TPNL.js.map → chunk-V26YQQK4.js.map} +0 -0
  116. /package/dist/{chunk-HVZVVDFN.js.map → chunk-VLOPXV47.js.map} +0 -0
  117. /package/dist/{chunk-AMK4BTAV.js.map → chunk-WD4DENPC.js.map} +0 -0
  118. /package/dist/{cli-circuit-breaker-AM425BRL.js.map → cli-circuit-breaker-62YOQQT3.js.map} +0 -0
  119. /package/dist/{cli-readiness-LWOMHZ34.js.map → cli-readiness-O6EOQV4L.js.map} +0 -0
  120. /package/dist/{composite-router-VXQFXNET.js.map → composite-router-YPZ3D5Y6.js.map} +0 -0
  121. /package/dist/{consensus-vote-V2JQHILD.js.map → consensus-vote-RNQQUBXO.js.map} +0 -0
  122. /package/dist/{context-retriever-GEQIPCVL.js.map → context-retriever-HVPLM6DH.js.map} +0 -0
  123. /package/dist/{doctor-deep-DSOZGB5J.js.map → doctor-deep-UZQTD65D.js.map} +0 -0
  124. /package/dist/{doctor-gateway-report-RJOSOHGA.js.map → doctor-gateway-report-LRBPPJD4.js.map} +0 -0
  125. /package/dist/{doctor-live-7LVRGOS5.js.map → doctor-live-QFLT5SDB.js.map} +0 -0
  126. /package/dist/{expert-bridge-5A4ADUBO.js.map → expert-bridge-O3UZXLGB.js.map} +0 -0
  127. /package/dist/{factory-4RZLP7QV.js.map → factory-KCKQQAEH.js.map} +0 -0
  128. /package/dist/{factory-UBAUIA2M.js.map → factory-LUKUIB6C.js.map} +0 -0
  129. /package/dist/{improvement-review-UIYSAI36.js.map → improvement-review-7IABFAU3.js.map} +0 -0
  130. /package/dist/{issue-triage-VKEHVHO4.js.map → issue-triage-YBVJVAUU.js.map} +0 -0
  131. /package/dist/{model-config-helpers-GYPIIXOP.js.map → model-config-helpers-QNV3KD2W.js.map} +0 -0
  132. /package/dist/{openai-compat-adapter-GSDOJ7TQ.js.map → openai-compat-adapter-GPZDIR3D.js.map} +0 -0
  133. /package/dist/{outcome-store-adapter-PXT7AL6T.js.map → outcome-store-adapter-WRONSTUD.js.map} +0 -0
  134. /package/dist/{pr-reviewer-helpers-GUUT7G43.js.map → pr-reviewer-helpers-7SMNNDFJ.js.map} +0 -0
  135. /package/dist/{registry-command-4G3WGKS5.js.map → registry-command-U7K6HIBK.js.map} +0 -0
  136. /package/dist/{repo-security-plan-QO6ACTPZ.js.map → repo-security-plan-LFWLVEL2.js.map} +0 -0
  137. /package/dist/{research-helpers-synthesize-MPEJHPYR.js.map → research-helpers-synthesize-X6MVAAKT.js.map} +0 -0
  138. /package/dist/{routing-memory-VHQDJ6XZ.js.map → routing-memory-OCIVXKOU.js.map} +0 -0
  139. /package/dist/{session-memory-M3CQFFL6.js.map → session-memory-4LMEWO3X.js.map} +0 -0
  140. /package/dist/{setup-config-VGEBCEKX.js.map → setup-config-YNNNR4AE.js.map} +0 -0
  141. /package/dist/{setup-custom-api-DX7CNYKT.js.map → setup-custom-api-QEPUCHOX.js.map} +0 -0
  142. /package/dist/{setup-data-dir-75Z5PSSB.js.map → setup-data-dir-XM5DWH7R.js.map} +0 -0
  143. /package/dist/{tool-memory-7QUPGCWV.js.map → tool-memory-EFGFXF65.js.map} +0 -0
  144. /package/dist/{unified-registry-CLZK3H64.js.map → unified-registry-KLNC24HA.js.map} +0 -0
  145. /package/dist/{weather-report-OKMXF73V.js.map → weather-report-PMYQ3WBW.js.map} +0 -0
@@ -0,0 +1,2412 @@
1
+ import {
2
+ assertCustomApiHostResolvesPublic,
3
+ hostnameOf,
4
+ redactGatewaySecrets,
5
+ validateCustomApiBaseUrl
6
+ } from "./chunk-KASBBKAQ.js";
7
+ import {
8
+ REDACTED_KEY_PLACEHOLDER,
9
+ sanitizeErrorDetails
10
+ } from "./chunk-QM5W3T3O.js";
11
+ import {
12
+ CUSTOM_API_ALLOW_PRIVATE_ENV,
13
+ ConfigError,
14
+ DEFAULT_OPENAI_COMPAT_ENDPOINT,
15
+ ErrorCode,
16
+ ModelCapability,
17
+ ModelError,
18
+ NexusError,
19
+ OPENAI_COMPAT_ENDPOINT_ENV,
20
+ OPENAI_COMPAT_KEY_ENV,
21
+ OPENAI_COMPAT_MODELS_ENV,
22
+ OPENAI_COMPAT_URL_ENV,
23
+ RETRY_AFTER_CONTEXT_KEY,
24
+ TimeoutError,
25
+ createLogger,
26
+ err,
27
+ gatewayCostDetail,
28
+ gatewayEndpointRejection,
29
+ getCliModelName,
30
+ getErrorMessage,
31
+ getTimeProvider,
32
+ getTokenEstimator,
33
+ isEndpointArmId,
34
+ isRateLimitLikeError,
35
+ lookupInTreeCapability,
36
+ ok,
37
+ readModelAllowlist,
38
+ recordUsageEvent,
39
+ refineGatewayCatalog,
40
+ resolveRetryAfterMs
41
+ } from "./chunk-WKFPKPEM.js";
42
+
43
+ // src/adapters/openai-compat-adapter.ts
44
+ import OpenAI2 from "openai";
45
+
46
+ // src/adapters/gateway-host-status.ts
47
+ var GATEWAY_HOST_LOOKUP_TIMED_OUT = "gateway host check timed out; the gateway is not wired this attempt, will retry";
48
+ var REMEDY = `Set ${CUSTOM_API_ALLOW_PRIVATE_ENV}=1 if the gateway runs on a trusted internal host, then restart the server.`;
49
+ async function checkGatewayHost(baseUrl, options = {}) {
50
+ const host = hostnameOf(baseUrl);
51
+ const guard = await withLookupTimeout(
52
+ assertCustomApiHostResolvesPublic(host),
53
+ options.lookupTimeoutMs ?? GATEWAY_HOST_LOOKUP_TIMEOUT_MS
54
+ );
55
+ if (guard === "timed_out") return { state: "lookup_timed_out", host };
56
+ if (guard.ok) return { state: "allowed", host };
57
+ return { state: "refused_private_host", host, reason: guard.error.message, remedy: REMEDY };
58
+ }
59
+ var GATEWAY_HOST_LOOKUP_TIMEOUT_MS = 5e3;
60
+ async function withLookupTimeout(work, ms) {
61
+ let timer;
62
+ const timeout = new Promise((resolve) => {
63
+ timer = setTimeout(() => {
64
+ resolve("timed_out");
65
+ }, ms);
66
+ timer.unref();
67
+ });
68
+ try {
69
+ return await Promise.race([work, timeout]);
70
+ } finally {
71
+ clearTimeout(timer);
72
+ }
73
+ }
74
+ var GatewayHostRefusedError = class extends ConfigError {
75
+ status;
76
+ constructor(status) {
77
+ super(`Gateway URL rejected: ${status.reason}`);
78
+ this.name = "GatewayHostRefusedError";
79
+ this.status = status;
80
+ }
81
+ };
82
+
83
+ // src/adapters/openai-adapter.ts
84
+ import OpenAI, { APIError } from "openai";
85
+
86
+ // src/adapters/base-adapter.ts
87
+ var AdapterModelError = class extends ModelError {
88
+ constructor(message, options) {
89
+ super(message, options);
90
+ this.name = "ModelError";
91
+ }
92
+ };
93
+ function isApiKeyMissing(apiKey) {
94
+ return apiKey === void 0 || apiKey === "" || apiKey.trim() === "";
95
+ }
96
+ function requireApiKey(apiKey, providerName, modelId) {
97
+ if (isApiKeyMissing(apiKey)) {
98
+ throw new ConfigError(`${providerName} API key is required`, {
99
+ context: { providerId: providerName.toLowerCase(), modelId }
100
+ });
101
+ }
102
+ }
103
+ function validateApiKeyPresence(apiKey, providerId, modelId) {
104
+ if (isApiKeyMissing(apiKey)) {
105
+ return err(
106
+ new ConfigError(`${providerId} API key is required`, {
107
+ context: { providerId, modelId }
108
+ })
109
+ );
110
+ }
111
+ return ok(void 0);
112
+ }
113
+ var BaseAdapter = class {
114
+ providerId;
115
+ modelId;
116
+ capabilities;
117
+ /** Logger for request/response logging */
118
+ logger;
119
+ /** Configuration for the adapter */
120
+ config;
121
+ /**
122
+ * Creates a new BaseAdapter instance.
123
+ *
124
+ * @param config - Adapter configuration
125
+ */
126
+ constructor(config) {
127
+ this.providerId = config.providerId;
128
+ this.modelId = config.modelId;
129
+ this.capabilities = config.capabilities;
130
+ this.config = config;
131
+ this.logger = config.logger ?? createLogger({
132
+ adapter: config.providerId,
133
+ model: config.modelId
134
+ });
135
+ }
136
+ /**
137
+ * Count tokens in text using the unified TokenEstimator.
138
+ *
139
+ * This provides a reasonable estimate for most use cases.
140
+ * Concrete adapters may override this with provider-specific tokenizers.
141
+ *
142
+ * @param text - Text to count tokens for
143
+ * @returns Approximate token count
144
+ */
145
+ countTokens(text) {
146
+ return Promise.resolve(getTokenEstimator().estimateText(text));
147
+ }
148
+ /**
149
+ * Validate adapter configuration.
150
+ *
151
+ * Checks that required configuration fields are present and valid.
152
+ * Concrete adapters may override to add provider-specific validation.
153
+ *
154
+ * @returns Ok if valid, ConfigError if invalid
155
+ */
156
+ validateConfig() {
157
+ const errors = [];
158
+ if (!this.providerId || this.providerId.trim() === "") {
159
+ errors.push("Provider ID is required");
160
+ }
161
+ if (!this.modelId || this.modelId.trim() === "") {
162
+ errors.push("Model ID is required");
163
+ }
164
+ if (this.config.timeout !== void 0 && this.config.timeout <= 0) {
165
+ errors.push("Timeout must be positive");
166
+ }
167
+ if (this.config.maxRetries !== void 0 && this.config.maxRetries < 0) {
168
+ errors.push("Max retries cannot be negative");
169
+ }
170
+ if (errors.length > 0) {
171
+ return err(
172
+ new ConfigError(`Invalid adapter configuration: ${errors.join("; ")}`, {
173
+ context: {
174
+ providerId: this.providerId,
175
+ modelId: this.modelId,
176
+ errors
177
+ }
178
+ })
179
+ );
180
+ }
181
+ return ok(void 0);
182
+ }
183
+ /**
184
+ * Check if this adapter supports a specific capability.
185
+ *
186
+ * @param capability - The capability to check for
187
+ * @returns True if the capability is supported
188
+ */
189
+ hasCapability(capability) {
190
+ return this.capabilities.includes(capability);
191
+ }
192
+ /**
193
+ * Log details about an outgoing request.
194
+ * Sanitizes sensitive information before logging.
195
+ *
196
+ * @param request - The completion request to log
197
+ */
198
+ logRequest(request) {
199
+ const messageCount = request.messages.length;
200
+ const hasTools = request.tools !== void 0 && request.tools.length > 0;
201
+ const toolCount = request.tools?.length ?? 0;
202
+ this.logger.debug("Sending completion request", {
203
+ messageCount,
204
+ hasSystemPrompt: request.systemPrompt !== void 0,
205
+ temperature: request.temperature,
206
+ maxTokens: request.maxTokens,
207
+ hasTools,
208
+ toolCount,
209
+ responseFormat: request.responseFormat?.type,
210
+ stopSequences: request.stop?.length ?? 0
211
+ });
212
+ }
213
+ /**
214
+ * Log details about a received response.
215
+ *
216
+ * @param response - The completion response to log
217
+ */
218
+ logResponse(response) {
219
+ this.logger.debug("Received completion response", {
220
+ contentBlocks: response.content.length,
221
+ stopReason: response.stopReason,
222
+ inputTokens: response.usage?.inputTokens ?? 0,
223
+ outputTokens: response.usage?.outputTokens ?? 0,
224
+ totalTokens: response.usage?.totalTokens ?? 0,
225
+ model: response.model
226
+ });
227
+ }
228
+ /**
229
+ * Transform a provider-specific error into a standardized ModelError.
230
+ *
231
+ * Maps common error patterns to appropriate error codes:
232
+ * - Rate limiting (429, quota exceeded)
233
+ * - Timeouts (ETIMEDOUT, ESOCKETTIMEDOUT)
234
+ * - Authentication (401, 403)
235
+ * - Model unavailable (503, 502)
236
+ *
237
+ * @param error - The original error from the provider
238
+ * @returns A standardized ModelError
239
+ */
240
+ transformError(error) {
241
+ if (error instanceof ModelError) {
242
+ return error;
243
+ }
244
+ const errorMessage = getErrorMessage(error);
245
+ const errorCode = this.determineErrorCode(error);
246
+ const param = errorCode === ErrorCode.MODEL_PARAMETER_UNSUPPORTED ? this.extractErrorParam(error) : void 0;
247
+ const retryAfterMs = errorCode === ErrorCode.MODEL_RATE_LIMITED ? resolveRetryAfterMs(error, errorMessage) : void 0;
248
+ const modelError = this.createModelError(errorMessage, errorCode, error, param, retryAfterMs);
249
+ this.logger.error("Model adapter error", modelError, {
250
+ errorCode,
251
+ providerId: this.providerId,
252
+ modelId: this.modelId
253
+ });
254
+ return modelError;
255
+ }
256
+ /**
257
+ * Create a ModelError with appropriate error code.
258
+ */
259
+ createModelError(message, errorCode, originalError, param, retryAfterMs) {
260
+ const fullMessage = `${this.providerId}/${this.modelId}: ${message}`;
261
+ const context = {
262
+ providerId: this.providerId,
263
+ modelId: this.modelId
264
+ };
265
+ if (param !== void 0) {
266
+ context.param = param;
267
+ }
268
+ if (retryAfterMs !== void 0) {
269
+ context[RETRY_AFTER_CONTEXT_KEY] = retryAfterMs;
270
+ }
271
+ const options = {
272
+ code: errorCode,
273
+ context
274
+ };
275
+ if (originalError instanceof Error) {
276
+ options.cause = originalError;
277
+ }
278
+ return new AdapterModelError(fullMessage, options);
279
+ }
280
+ /**
281
+ * Determine the appropriate error code based on error characteristics.
282
+ */
283
+ determineErrorCode(error) {
284
+ if (!(error instanceof Error)) {
285
+ return ErrorCode.MODEL_ERROR;
286
+ }
287
+ const message = error.message.toLowerCase();
288
+ const errorObj = error;
289
+ if (isRateLimitLikeError(error)) {
290
+ return ErrorCode.MODEL_RATE_LIMITED;
291
+ }
292
+ if (this.isTimeoutError(message, errorObj)) {
293
+ return ErrorCode.MODEL_TIMEOUT;
294
+ }
295
+ if (this.isModelNotFoundError(message, errorObj)) {
296
+ return ErrorCode.MODEL_NOT_FOUND;
297
+ }
298
+ if (this.isUnavailableError(message, errorObj)) {
299
+ return ErrorCode.MODEL_UNAVAILABLE;
300
+ }
301
+ if (this.extractErrorParam(error) !== void 0) {
302
+ return ErrorCode.MODEL_PARAMETER_UNSUPPORTED;
303
+ }
304
+ return ErrorCode.MODEL_ERROR;
305
+ }
306
+ /**
307
+ * Extract the offending parameter name from a param-naming 400 (#4069).
308
+ *
309
+ * Returns the param only when the error is a 400 AND carries a non-empty `param`
310
+ * field (the OpenAI SDK / OpenAI-compatible gateways set this on a rejected
311
+ * parameter; the OpenAI adapter threads it onto the classification probe).
312
+ * Returns undefined otherwise, so non-400s and param-less 400s are untouched.
313
+ */
314
+ extractErrorParam(error) {
315
+ if (!(error instanceof Error)) {
316
+ return void 0;
317
+ }
318
+ const errorObj = error;
319
+ if (errorObj.status !== 400) {
320
+ return void 0;
321
+ }
322
+ return typeof errorObj.param === "string" && errorObj.param !== "" ? errorObj.param : void 0;
323
+ }
324
+ /**
325
+ * (#2540 PR 8) Detect model-retirement errors. Distinct from transient
326
+ * 502/503: 404 + vendor messages indicating the model id is gone.
327
+ */
328
+ isModelNotFoundError(message, errorObj) {
329
+ if (errorObj.status === 404) return true;
330
+ const patterns = [
331
+ "model not found",
332
+ "model_not_found",
333
+ "no such model",
334
+ "model is deprecated",
335
+ "model has been deprecated",
336
+ "model is no longer available"
337
+ ];
338
+ return patterns.some((p) => message.includes(p));
339
+ }
340
+ /**
341
+ * Check if error indicates a timeout.
342
+ */
343
+ isTimeoutError(message, errorObj) {
344
+ const timeoutPatterns = ["timeout", "etimedout", "esockettimedout"];
345
+ return errorObj.code === "ETIMEDOUT" || errorObj.code === "ESOCKETTIMEDOUT" || timeoutPatterns.some((pattern) => message.includes(pattern));
346
+ }
347
+ /**
348
+ * Check if error indicates model unavailability.
349
+ */
350
+ isUnavailableError(message, errorObj) {
351
+ const unavailablePatterns = ["unavailable", "service unavailable", "overloaded"];
352
+ return errorObj.status === 502 || errorObj.status === 503 || unavailablePatterns.some((pattern) => message.includes(pattern));
353
+ }
354
+ };
355
+
356
+ // src/adapters/streaming-types.ts
357
+ var StreamError = class extends NexusError {
358
+ constructor(message, options) {
359
+ super(message, { code: ErrorCode.INTERNAL_ERROR, ...options });
360
+ this.name = "StreamError";
361
+ }
362
+ };
363
+ var StreamCancelledError = class extends NexusError {
364
+ constructor(reason) {
365
+ super(reason ?? "Stream was cancelled", { code: ErrorCode.INTERNAL_ERROR });
366
+ this.name = "StreamCancelledError";
367
+ }
368
+ };
369
+ var StreamController = class {
370
+ chunks = [];
371
+ waiters = [];
372
+ _state = "idle";
373
+ _error;
374
+ maxBufferSize;
375
+ abortHandler;
376
+ abortSignal;
377
+ /**
378
+ * Creates a new StreamController.
379
+ * @param options - Stream creation options
380
+ */
381
+ constructor(options = {}) {
382
+ this.maxBufferSize = options.maxBufferSize ?? 100;
383
+ if (options.signal) {
384
+ this.abortSignal = options.signal;
385
+ this.abortHandler = () => {
386
+ this.cancel("AbortSignal triggered");
387
+ };
388
+ options.signal.addEventListener("abort", this.abortHandler);
389
+ }
390
+ }
391
+ /**
392
+ * Current state of the stream.
393
+ */
394
+ get state() {
395
+ return this._state;
396
+ }
397
+ /**
398
+ * Whether the stream is still active (can receive chunks).
399
+ */
400
+ get isActive() {
401
+ return this._state === "idle" || this._state === "streaming" || this._state === "paused";
402
+ }
403
+ /**
404
+ * Current buffer size.
405
+ */
406
+ get bufferSize() {
407
+ return this.chunks.length;
408
+ }
409
+ /**
410
+ * Push a chunk to the stream.
411
+ * @param chunk - The chunk to push
412
+ * @returns Result indicating success or backpressure
413
+ */
414
+ push(chunk) {
415
+ if (!this.isActive) {
416
+ return err(new StreamError(`Cannot push to stream in state: ${this._state}`));
417
+ }
418
+ if (this._state === "idle") {
419
+ this._state = "streaming";
420
+ }
421
+ const waiter = this.waiters.shift();
422
+ if (waiter) {
423
+ waiter.resolve({ done: false, value: chunk });
424
+ return ok(void 0);
425
+ }
426
+ if (this.chunks.length >= this.maxBufferSize) {
427
+ this._state = "paused";
428
+ return err(
429
+ new StreamError("Buffer full - backpressure applied", {
430
+ context: { bufferSize: this.chunks.length, maxBufferSize: this.maxBufferSize }
431
+ })
432
+ );
433
+ }
434
+ this.chunks.push(chunk);
435
+ return ok(void 0);
436
+ }
437
+ /**
438
+ * Complete the stream successfully.
439
+ */
440
+ complete() {
441
+ if (!this.isActive) {
442
+ return;
443
+ }
444
+ this._state = "completed";
445
+ this.removeAbortListener();
446
+ this.resolveAllWaiters();
447
+ }
448
+ /**
449
+ * Complete the stream with an error.
450
+ * @param error - The error that occurred
451
+ */
452
+ error(error) {
453
+ if (!this.isActive) {
454
+ return;
455
+ }
456
+ this._state = "error";
457
+ this._error = error;
458
+ this.removeAbortListener();
459
+ this.rejectAllWaiters(error);
460
+ }
461
+ /**
462
+ * Cancel the stream.
463
+ * @param reason - Optional reason for cancellation
464
+ */
465
+ cancel(reason) {
466
+ if (!this.isActive) {
467
+ return;
468
+ }
469
+ this._state = "cancelled";
470
+ this._error = new StreamCancelledError(reason);
471
+ this.removeAbortListener();
472
+ this.rejectAllWaiters(this._error);
473
+ }
474
+ /**
475
+ * Get the AsyncIterable for consuming the stream.
476
+ */
477
+ getIterable() {
478
+ const nextChunk = () => this.nextChunk();
479
+ const cancel = (reason) => {
480
+ this.cancel(reason);
481
+ };
482
+ return {
483
+ [Symbol.asyncIterator]() {
484
+ return {
485
+ async next() {
486
+ return nextChunk();
487
+ },
488
+ return() {
489
+ cancel("Iterator returned");
490
+ return Promise.resolve({ done: true, value: void 0 });
491
+ }
492
+ };
493
+ }
494
+ };
495
+ }
496
+ async nextChunk() {
497
+ const chunk = this.chunks.shift();
498
+ if (chunk !== void 0) {
499
+ if (this._state === "paused" && this.chunks.length < this.maxBufferSize / 2) {
500
+ this._state = "streaming";
501
+ }
502
+ return { done: false, value: chunk };
503
+ }
504
+ if (this._state === "completed") {
505
+ return { done: true, value: void 0 };
506
+ }
507
+ if (this._state === "cancelled" || this._state === "error") {
508
+ throw this._error ?? new StreamCancelledError();
509
+ }
510
+ return new Promise((resolve, reject) => {
511
+ this.waiters.push({ resolve, reject });
512
+ });
513
+ }
514
+ removeAbortListener() {
515
+ if (this.abortSignal && this.abortHandler) {
516
+ this.abortSignal.removeEventListener("abort", this.abortHandler);
517
+ }
518
+ }
519
+ resolveAllWaiters() {
520
+ for (const waiter of this.waiters) {
521
+ waiter.resolve({ done: true, value: void 0 });
522
+ }
523
+ this.waiters.length = 0;
524
+ }
525
+ rejectAllWaiters(error) {
526
+ for (const waiter of this.waiters) {
527
+ waiter.reject(error);
528
+ }
529
+ this.waiters.length = 0;
530
+ }
531
+ };
532
+ function createStream(options = {}) {
533
+ const controller = new StreamController(options);
534
+ return [controller, controller.getIterable()];
535
+ }
536
+
537
+ // src/adapters/stream-operators-helpers.ts
538
+ async function* take(stream, count, options = {}) {
539
+ if (count <= 0) {
540
+ return;
541
+ }
542
+ let taken = 0;
543
+ for await (const chunk of stream) {
544
+ if (options.signal?.aborted === true) {
545
+ throw new StreamCancelledError("take aborted");
546
+ }
547
+ yield chunk;
548
+ taken++;
549
+ if (taken >= count) {
550
+ return;
551
+ }
552
+ }
553
+ }
554
+ async function* skip(stream, count, options = {}) {
555
+ let skipped = 0;
556
+ for await (const chunk of stream) {
557
+ if (options.signal?.aborted === true) {
558
+ throw new StreamCancelledError("skip aborted");
559
+ }
560
+ if (skipped < count) {
561
+ skipped++;
562
+ continue;
563
+ }
564
+ yield chunk;
565
+ }
566
+ }
567
+ async function* concatStreams(streams, options = {}) {
568
+ for (const stream of streams) {
569
+ if (options.signal?.aborted === true) {
570
+ throw new StreamCancelledError("concat aborted");
571
+ }
572
+ yield* stream;
573
+ }
574
+ }
575
+ async function* fromArray(values, options = {}) {
576
+ for (const value of values) {
577
+ if (options.signal?.aborted === true) {
578
+ throw new StreamCancelledError("fromArray aborted");
579
+ }
580
+ if (options.delayMs !== void 0 && options.delayMs > 0) {
581
+ await new Promise((resolve) => setTimeout(resolve, options.delayMs));
582
+ }
583
+ yield value;
584
+ }
585
+ }
586
+ async function* tapStream(stream, fn, options = {}) {
587
+ let index = 0;
588
+ for await (const chunk of stream) {
589
+ if (options.signal?.aborted === true) {
590
+ throw new StreamCancelledError("tap aborted");
591
+ }
592
+ await fn(chunk, index);
593
+ yield chunk;
594
+ index++;
595
+ }
596
+ }
597
+ async function reduceStream(stream, reducer, initialValue, options = {}) {
598
+ let accumulator = initialValue;
599
+ let index = 0;
600
+ try {
601
+ for await (const chunk of stream) {
602
+ if (options.signal?.aborted === true) {
603
+ return err(new StreamError("Reduce aborted"));
604
+ }
605
+ accumulator = await reducer(accumulator, chunk, index);
606
+ index++;
607
+ }
608
+ return ok(accumulator);
609
+ } catch (error) {
610
+ return err(
611
+ new StreamError("Failed to reduce stream", {
612
+ cause: error instanceof Error ? error : new Error(String(error))
613
+ })
614
+ );
615
+ }
616
+ }
617
+
618
+ // src/adapters/stream-operators.ts
619
+ async function* transformStream(stream, fn, options = {}) {
620
+ let index = 0;
621
+ for await (const chunk of stream) {
622
+ if (options.signal?.aborted === true) {
623
+ throw new StreamCancelledError("Transform aborted");
624
+ }
625
+ yield await fn(chunk, index);
626
+ index++;
627
+ }
628
+ }
629
+ async function* mergeStreams(streams, options = {}) {
630
+ if (streams.length === 0) {
631
+ return;
632
+ }
633
+ const streamOptions = {};
634
+ if (options.signal) {
635
+ streamOptions.signal = options.signal;
636
+ }
637
+ const [controller, merged] = createStream(streamOptions);
638
+ let activeCount = streams.length;
639
+ const consumers = streams.map(async (stream, streamIndex) => {
640
+ try {
641
+ for await (const chunk of stream) {
642
+ if (options.signal?.aborted === true) {
643
+ break;
644
+ }
645
+ controller.push(chunk);
646
+ }
647
+ } catch (error) {
648
+ if (error instanceof StreamCancelledError) {
649
+ controller.cancel(`Stream ${String(streamIndex)} was cancelled`);
650
+ return;
651
+ }
652
+ controller.error(error instanceof Error ? error : new Error(String(error)));
653
+ return;
654
+ } finally {
655
+ activeCount--;
656
+ if (activeCount === 0) {
657
+ controller.complete();
658
+ }
659
+ }
660
+ });
661
+ Promise.all(consumers).catch((error) => {
662
+ controller.error(error instanceof Error ? error : new StreamError(String(error)));
663
+ });
664
+ yield* merged;
665
+ }
666
+ async function* takeUntil(stream, predicate, options = {}) {
667
+ let index = 0;
668
+ for await (const chunk of stream) {
669
+ if (options.signal?.aborted === true) {
670
+ throw new StreamCancelledError("takeUntil aborted");
671
+ }
672
+ const shouldStop = await predicate(chunk, index);
673
+ if (shouldStop) {
674
+ if (options.inclusive === true) {
675
+ yield chunk;
676
+ }
677
+ return;
678
+ }
679
+ yield chunk;
680
+ index++;
681
+ }
682
+ }
683
+ async function* filterStream(stream, predicate, options = {}) {
684
+ let index = 0;
685
+ for await (const chunk of stream) {
686
+ if (options.signal?.aborted === true) {
687
+ throw new StreamCancelledError("filter aborted");
688
+ }
689
+ if (await predicate(chunk, index)) {
690
+ yield chunk;
691
+ }
692
+ index++;
693
+ }
694
+ }
695
+ async function* withTimeout(stream, timeoutMs, options = {}) {
696
+ const iterator = stream[Symbol.asyncIterator]();
697
+ try {
698
+ let running = true;
699
+ while (running) {
700
+ if (options.signal?.aborted === true) {
701
+ throw new StreamCancelledError("withTimeout aborted");
702
+ }
703
+ let timeoutId;
704
+ try {
705
+ const result = await Promise.race([
706
+ iterator.next().then((res) => {
707
+ if (timeoutId !== void 0) {
708
+ clearTimeout(timeoutId);
709
+ }
710
+ return res;
711
+ }),
712
+ new Promise((_, reject) => {
713
+ timeoutId = setTimeout(() => {
714
+ reject(
715
+ new TimeoutError(`Stream timed out after ${String(timeoutMs)}ms`, {
716
+ context: { timeoutMs }
717
+ })
718
+ );
719
+ }, timeoutMs);
720
+ })
721
+ ]);
722
+ if (result.done === true) {
723
+ running = false;
724
+ } else {
725
+ yield result.value;
726
+ }
727
+ } finally {
728
+ if (timeoutId !== void 0) {
729
+ clearTimeout(timeoutId);
730
+ }
731
+ }
732
+ }
733
+ } finally {
734
+ if (iterator.return !== void 0) {
735
+ await iterator.return();
736
+ }
737
+ }
738
+ }
739
+ async function* bufferStream(stream, size, options = {}) {
740
+ if (size <= 0) {
741
+ throw new StreamError("Buffer size must be positive");
742
+ }
743
+ let buffer = [];
744
+ for await (const chunk of stream) {
745
+ if (options.signal?.aborted === true) {
746
+ throw new StreamCancelledError("buffer aborted");
747
+ }
748
+ buffer.push(chunk);
749
+ if (buffer.length >= size) {
750
+ yield buffer;
751
+ buffer = [];
752
+ }
753
+ }
754
+ if (buffer.length > 0) {
755
+ yield buffer;
756
+ }
757
+ }
758
+
759
+ // src/adapters/streaming.ts
760
+ var DEFAULT_COLLECT_STREAM_MAX_CHUNKS = 1e5;
761
+ async function collectStream(stream, options = {}) {
762
+ const chunks = [];
763
+ const maxChunks = options.maxChunks ?? DEFAULT_COLLECT_STREAM_MAX_CHUNKS;
764
+ try {
765
+ for await (const chunk of stream) {
766
+ if (options.signal?.aborted === true) {
767
+ return err(new StreamError("Collection aborted"));
768
+ }
769
+ chunks.push(chunk);
770
+ if (chunks.length >= maxChunks) {
771
+ break;
772
+ }
773
+ }
774
+ return ok(chunks);
775
+ } catch (error) {
776
+ if (error instanceof StreamCancelledError) {
777
+ return err(new StreamError("Stream was cancelled during collection", { cause: error }));
778
+ }
779
+ return err(
780
+ new StreamError("Failed to collect stream", {
781
+ cause: error instanceof Error ? error : new Error(String(error))
782
+ })
783
+ );
784
+ }
785
+ }
786
+
787
+ // src/adapters/openai-types.ts
788
+ var OPENAI_MODELS = {
789
+ GPT_5_2: "gpt-5.2",
790
+ GPT_5_2_INSTANT: "gpt-5.2-chat-latest",
791
+ GPT_5_2_PRO: "gpt-5.2-pro",
792
+ /**
793
+ * Registry-derived: resolves to `codex-5.2`'s `cliModelName`, which since
794
+ * 2026-09-23 is `gpt-5.6-luna`, not a "5.2" model. The key name lags its
795
+ * value; renaming it is a public-API change tracked in #5489.
796
+ */
797
+ GPT_5_2_CODEX: getCliModelName("codex-5.2"),
798
+ GPT_4O: "gpt-4o-2024-11-20",
799
+ GPT_4O_MINI: "gpt-4o-mini-2024-07-18",
800
+ GPT_4_TURBO: "gpt-4-turbo-2024-04-09",
801
+ GPT_35_TURBO: "gpt-3.5-turbo-0125"
802
+ };
803
+ var OPENAI_MODEL_ALIASES = {
804
+ "gpt-5.2-instant": OPENAI_MODELS.GPT_5_2_INSTANT,
805
+ "gpt-4o": OPENAI_MODELS.GPT_4O,
806
+ "gpt-4o-mini": OPENAI_MODELS.GPT_4O_MINI,
807
+ "gpt-4-turbo": OPENAI_MODELS.GPT_4_TURBO,
808
+ "gpt-3.5-turbo": OPENAI_MODELS.GPT_35_TURBO
809
+ };
810
+ var DEFAULT_MAX_TOKENS = 4096;
811
+ function isFunctionToolCall(toolCall) {
812
+ if (typeof toolCall !== "object" || toolCall === null) {
813
+ return false;
814
+ }
815
+ const tc = toolCall;
816
+ return tc["type"] === "function" && typeof tc["function"] === "object" && tc["function"] !== null;
817
+ }
818
+ function resolveModelId(modelId) {
819
+ return OPENAI_MODEL_ALIASES[modelId] ?? modelId;
820
+ }
821
+ function getModelCapabilities(modelId) {
822
+ const capabilities = [ModelCapability.COMPLETION, ModelCapability.STREAMING, ModelCapability.TOOL_USE];
823
+ const resolvedId = resolveModelId(modelId);
824
+ if (resolvedId.includes("gpt-4o") || resolvedId.includes("gpt-4-turbo") || resolvedId.includes("gpt-5.2")) {
825
+ capabilities.push(ModelCapability.VISION);
826
+ }
827
+ if (resolvedId.includes("gpt-5.2")) {
828
+ capabilities.push(ModelCapability.EXTENDED_THINKING);
829
+ }
830
+ return capabilities;
831
+ }
832
+
833
+ // src/config/model-parameter-support.ts
834
+ function parseClaudeMajorMinor(bareId) {
835
+ const undated = bareId.replace(/[-_]\d{6,}$/, "");
836
+ const match = /(\d+)(?:[-_.](\d+))?/.exec(undated);
837
+ if (match === null) return null;
838
+ const major = Number.parseInt(match[1], 10);
839
+ const minor = match[2] !== void 0 ? Number.parseInt(match[2], 10) : 0;
840
+ if (Number.isNaN(major) || Number.isNaN(minor)) return null;
841
+ return { major, minor };
842
+ }
843
+ var LEGACY_TEMPERATURE_SUPPORTING = /claude[-_](?:instant|1|2|3)(?:[-_.]|$)/;
844
+ function claudeTemperatureUnsupported(bare) {
845
+ const version = parseClaudeMajorMinor(bare);
846
+ if (version !== null) {
847
+ return version.major > 4 || version.major === 4 && version.minor >= 7;
848
+ }
849
+ if (LEGACY_TEMPERATURE_SUPPORTING.test(bare)) return false;
850
+ return true;
851
+ }
852
+ function openAiReasoning(id) {
853
+ const segment = id.includes("/") ? id.slice(id.lastIndexOf("/") + 1) : id;
854
+ if (/^o[0-9]/.test(segment)) return true;
855
+ if (segment.includes("codex")) return true;
856
+ if (/gpt-?5(?![0-9])/.test(segment) && !segment.includes("gpt-5-chat")) return true;
857
+ return false;
858
+ }
859
+ function regexRejectsTemperature(modelId) {
860
+ const id = modelId.toLowerCase();
861
+ const claudeAt = id.indexOf("claude");
862
+ if (claudeAt !== -1) {
863
+ return claudeTemperatureUnsupported(id.slice(claudeAt));
864
+ }
865
+ return openAiReasoning(id);
866
+ }
867
+ function regexIsOpenAiReasoning(modelId) {
868
+ const id = modelId.toLowerCase();
869
+ if (id.includes("claude")) return false;
870
+ return openAiReasoning(id);
871
+ }
872
+ function unsupportedParametersForModel(modelId) {
873
+ const cap = lookupInTreeCapability(modelId);
874
+ if (cap?.unsupportedParameters !== void 0) return cap.unsupportedParameters;
875
+ return regexRejectsTemperature(modelId) ? ["temperature"] : [];
876
+ }
877
+ function modelSupportsParameter(modelId, param) {
878
+ return !unsupportedParametersForModel(modelId).includes(param);
879
+ }
880
+ function getMaxTokensParamForModel(modelId) {
881
+ const cap = lookupInTreeCapability(modelId);
882
+ if (cap?.maxTokensParam !== void 0) return cap.maxTokensParam;
883
+ return regexIsOpenAiReasoning(modelId) ? "max_completion_tokens" : "max_tokens";
884
+ }
885
+
886
+ // src/config/temperature-support.ts
887
+ var logger = createLogger({ component: "temperature-support" });
888
+ var warnedDroppedModels = /* @__PURE__ */ new Set();
889
+ function warnTemperatureDropped(modelId) {
890
+ if (warnedDroppedModels.has(modelId)) {
891
+ logger.debug("Omitted unsupported temperature (already warned for this model)", {
892
+ modelId,
893
+ parameter: "temperature"
894
+ });
895
+ return;
896
+ }
897
+ warnedDroppedModels.add(modelId);
898
+ logger.warn(
899
+ `Model "${modelId}" does not support a custom \`temperature\` (the provider rejects non-default values with a 400). nexus-agents omitted it; the request runs at the provider default. Any determinism/consistency you expected from temperature has NO effect on this model.`,
900
+ { modelId, parameter: "temperature", severity: "behavioral" }
901
+ );
902
+ }
903
+
904
+ // src/adapters/optional-params.ts
905
+ var BEHAVIORAL_PARAMS = /* @__PURE__ */ new Set(["temperature", "seed", "top_p"]);
906
+ function parameterSeverity(param) {
907
+ return BEHAVIORAL_PARAMS.has(param) ? "behavioral" : "cosmetic";
908
+ }
909
+ function planOptionalParams(request, modelId) {
910
+ const dropped = [];
911
+ let temperature;
912
+ if (request.temperature !== void 0) {
913
+ if (modelSupportsParameter(modelId, "temperature")) {
914
+ temperature = request.temperature;
915
+ } else {
916
+ warnTemperatureDropped(modelId);
917
+ dropped.push({
918
+ param: "temperature",
919
+ reason: `model_rejects:${modelId}`,
920
+ severity: parameterSeverity("temperature")
921
+ });
922
+ }
923
+ }
924
+ return { ...temperature !== void 0 ? { temperature } : {}, dropped };
925
+ }
926
+
927
+ // src/adapters/openai-mappers.ts
928
+ function mapStopReason(openaiReason) {
929
+ switch (openaiReason) {
930
+ case "stop":
931
+ return "end_turn";
932
+ case "length":
933
+ return "max_tokens";
934
+ case "tool_calls":
935
+ case "function_call":
936
+ return "tool_use";
937
+ // `content_filter` is never mapped as a finish: the adapter turns it into an
938
+ // error before a response is built (#6607, see detectNonAnswer), because
939
+ // every consumer reads a mapped response as an answer.
940
+ default:
941
+ return "end_turn";
942
+ }
943
+ }
944
+ var CONTENT_FILTERED = {
945
+ reason: "content_filter",
946
+ detail: "the reply was blocked by a content filter"
947
+ };
948
+ function detectNonAnswer(choice, usage, reasoningFamily) {
949
+ if (choice.finish_reason === "content_filter") return CONTENT_FILTERED;
950
+ if (choice.finish_reason !== "length" || hasVisibleOutput(choice)) return void 0;
951
+ const reasoningTokens = usage?.completion_tokens_details?.reasoning_tokens;
952
+ if (!reasoningFamily && !(reasoningTokens !== void 0 && reasoningTokens > 0)) {
953
+ return void 0;
954
+ }
955
+ return {
956
+ reason: "reasoning_truncated",
957
+ detail: "the completion budget was spent on reasoning before any output (empty reply, finish length)",
958
+ ...reasoningTokens !== void 0 ? { reasoningTokens } : {}
959
+ };
960
+ }
961
+ function hasVisibleOutput(choice) {
962
+ const text = choice.message.content;
963
+ if (text !== null && text !== "") return true;
964
+ return (choice.message.tool_calls?.length ?? 0) > 0;
965
+ }
966
+ function parseToolArguments(args) {
967
+ try {
968
+ return JSON.parse(args);
969
+ } catch {
970
+ return { _raw: args };
971
+ }
972
+ }
973
+ function mapChoiceToContentBlocks(choice) {
974
+ const blocks = [];
975
+ const message = choice.message;
976
+ if (message.content !== null && message.content !== "") {
977
+ blocks.push({ type: "text", text: message.content });
978
+ }
979
+ if (message.tool_calls !== void 0 && message.tool_calls.length > 0) {
980
+ for (const toolCall of message.tool_calls) {
981
+ if (isFunctionToolCall(toolCall)) {
982
+ blocks.push({
983
+ type: "tool_use",
984
+ id: toolCall.id,
985
+ name: toolCall.function.name,
986
+ input: parseToolArguments(toolCall.function.arguments)
987
+ });
988
+ }
989
+ }
990
+ }
991
+ if (blocks.length === 0) {
992
+ blocks.push({ type: "text", text: "" });
993
+ }
994
+ return blocks;
995
+ }
996
+ function mapMessage(message) {
997
+ if (message.role === "system") {
998
+ return [mapSystemMessage(message)];
999
+ }
1000
+ if (message.role === "user") {
1001
+ return mapUserMessage(message);
1002
+ }
1003
+ return [mapAssistantMessage(message)];
1004
+ }
1005
+ function mapSystemMessage(message) {
1006
+ const content = typeof message.content === "string" ? message.content : message.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
1007
+ return { role: "system", content };
1008
+ }
1009
+ function mapUserMessage(message) {
1010
+ if (typeof message.content === "string") {
1011
+ return [{ role: "user", content: message.content }];
1012
+ }
1013
+ const toolResults = message.content.filter(
1014
+ (b) => b.type === "tool_result"
1015
+ );
1016
+ if (toolResults.length > 0) {
1017
+ return toolResults.map((result) => ({
1018
+ role: "tool",
1019
+ tool_call_id: result.tool_use_id,
1020
+ content: result.content
1021
+ }));
1022
+ }
1023
+ const content = message.content.map((block) => {
1024
+ if (block.type === "text") {
1025
+ return { type: "text", text: block.text };
1026
+ }
1027
+ if (block.type === "image") {
1028
+ return {
1029
+ type: "image_url",
1030
+ image_url: {
1031
+ url: `data:${block.source.media_type};base64,${block.source.data}`
1032
+ }
1033
+ };
1034
+ }
1035
+ return { type: "text", text: "" };
1036
+ });
1037
+ return [{ role: "user", content }];
1038
+ }
1039
+ function mapAssistantMessage(message) {
1040
+ if (typeof message.content === "string") {
1041
+ return { role: "assistant", content: message.content };
1042
+ }
1043
+ const toolUses = message.content.filter(
1044
+ (b) => b.type === "tool_use"
1045
+ );
1046
+ const textContent = message.content.filter((b) => b.type === "text").map((b) => b.text).join("");
1047
+ if (toolUses.length > 0) {
1048
+ const assistantMessage = {
1049
+ role: "assistant",
1050
+ content: textContent !== "" ? textContent : null,
1051
+ tool_calls: toolUses.map((tool) => ({
1052
+ id: tool.id,
1053
+ type: "function",
1054
+ function: {
1055
+ name: tool.name,
1056
+ arguments: JSON.stringify(tool.input)
1057
+ }
1058
+ }))
1059
+ };
1060
+ return assistantMessage;
1061
+ }
1062
+ return { role: "assistant", content: textContent };
1063
+ }
1064
+ function mapTool(tool) {
1065
+ return {
1066
+ type: "function",
1067
+ function: {
1068
+ name: tool.name,
1069
+ description: tool.description,
1070
+ parameters: tool.inputSchema
1071
+ }
1072
+ };
1073
+ }
1074
+ function mapResponseUsage(response) {
1075
+ const u = response.usage;
1076
+ if (u === void 0) return void 0;
1077
+ const cached = u.prompt_tokens_details?.cached_tokens;
1078
+ return {
1079
+ inputTokens: u.prompt_tokens,
1080
+ outputTokens: u.completion_tokens,
1081
+ totalTokens: u.total_tokens,
1082
+ ...cached !== void 0 ? { cachedInputTokens: cached } : {}
1083
+ };
1084
+ }
1085
+ function mapContentDelta(delta, currentIndex, hasStarted) {
1086
+ const chunks = [];
1087
+ if (delta.content !== void 0 && delta.content !== null && delta.content !== "") {
1088
+ if (currentIndex === 0 && !hasStarted) {
1089
+ chunks.push({
1090
+ type: "content_block_start",
1091
+ index: 0,
1092
+ contentBlock: { type: "text", text: "" }
1093
+ });
1094
+ }
1095
+ chunks.push({
1096
+ type: "content_block_delta",
1097
+ index: currentIndex,
1098
+ delta: { type: "text_delta", text: delta.content }
1099
+ });
1100
+ }
1101
+ return chunks;
1102
+ }
1103
+ function createStreamToolCallState() {
1104
+ return { calls: /* @__PURE__ */ new Map() };
1105
+ }
1106
+ function accumulateToolCallsDelta(delta, state) {
1107
+ for (const toolCall of delta.tool_calls ?? []) {
1108
+ const entry = state.calls.get(toolCall.index) ?? { id: "", name: "", args: "" };
1109
+ if (toolCall.id !== void 0) entry.id = toolCall.id;
1110
+ if (toolCall.function?.name !== void 0) entry.name = toolCall.function.name;
1111
+ if (toolCall.function?.arguments !== void 0) entry.args += toolCall.function.arguments;
1112
+ state.calls.set(toolCall.index, entry);
1113
+ }
1114
+ }
1115
+ function flushToolCalls(state) {
1116
+ const chunks = [...state.calls.entries()].sort(([a], [b]) => a - b).map(([index, call]) => ({
1117
+ type: "content_block_start",
1118
+ index,
1119
+ contentBlock: {
1120
+ type: "tool_use",
1121
+ id: call.id,
1122
+ name: call.name,
1123
+ input: call.args === "" ? {} : parseToolArguments(call.args)
1124
+ }
1125
+ }));
1126
+ state.calls.clear();
1127
+ return chunks;
1128
+ }
1129
+ function mapFinishChunks(choice, chunk, currentIndex, toolCalls) {
1130
+ const chunks = [];
1131
+ if (choice.finish_reason !== null) {
1132
+ chunks.push(...flushToolCalls(toolCalls));
1133
+ chunks.push({
1134
+ type: "content_block_stop",
1135
+ index: currentIndex
1136
+ });
1137
+ chunks.push({
1138
+ type: "message_delta",
1139
+ delta: { stop_reason: mapStopReason(choice.finish_reason) },
1140
+ // A stream whose usage IS reported keeps it — when `include_usage` is
1141
+ // wired, this is the branch that carries a real measurement.
1142
+ ...chunk.usage !== void 0 && chunk.usage !== null ? {
1143
+ usage: {
1144
+ // OpenAI still omits prompt_tokens on the final chunk (#4835).
1145
+ inputTokens: 0,
1146
+ inputTokensMeasured: false,
1147
+ outputTokens: chunk.usage.completion_tokens,
1148
+ totalTokens: chunk.usage.total_tokens
1149
+ }
1150
+ } : {}
1151
+ });
1152
+ chunks.push({ type: "message_stop" });
1153
+ }
1154
+ return chunks;
1155
+ }
1156
+ function mapStreamChunk(chunk, currentIndex, hasStarted, toolCalls) {
1157
+ const chunks = [];
1158
+ const choice = chunk.choices[0];
1159
+ if (!hasStarted) {
1160
+ chunks.push({
1161
+ type: "message_start",
1162
+ message: { model: chunk.model }
1163
+ });
1164
+ }
1165
+ if (choice === void 0) {
1166
+ return chunks;
1167
+ }
1168
+ const delta = choice.delta;
1169
+ chunks.push(...mapContentDelta(delta, currentIndex, hasStarted));
1170
+ accumulateToolCallsDelta(delta, toolCalls);
1171
+ chunks.push(...mapFinishChunks(choice, chunk, currentIndex, toolCalls));
1172
+ return chunks;
1173
+ }
1174
+
1175
+ // src/adapters/openai-responses.ts
1176
+ async function completeResponses(client, plan, signal) {
1177
+ const response = await client.responses.create(buildResponsesParams(plan.params), {
1178
+ signal
1179
+ });
1180
+ const dropped = plan.params.stop === void 0 ? plan.dropped : [
1181
+ ...plan.dropped,
1182
+ {
1183
+ param: "stop",
1184
+ reason: "The Responses API does not support stop sequences; omitted.",
1185
+ severity: "behavioral"
1186
+ }
1187
+ ];
1188
+ return { response: responsesToChatCompletion(response), dropped };
1189
+ }
1190
+ function buildResponsesParams(chat) {
1191
+ const params = {
1192
+ model: chat.model,
1193
+ input: chat.messages.flatMap(mapInputMessage),
1194
+ ...chat.max_completion_tokens !== void 0 && {
1195
+ max_output_tokens: chat.max_completion_tokens
1196
+ },
1197
+ ...chat.temperature !== void 0 && { temperature: chat.temperature }
1198
+ };
1199
+ if (chat.tools !== void 0) {
1200
+ params.tools = chat.tools.flatMap(
1201
+ (tool) => tool.type === "function" ? [
1202
+ {
1203
+ type: "function",
1204
+ name: tool.function.name,
1205
+ parameters: tool.function.parameters ?? {},
1206
+ strict: tool.function.strict ?? false,
1207
+ ...tool.function.description !== void 0 && {
1208
+ description: tool.function.description
1209
+ }
1210
+ }
1211
+ ] : []
1212
+ );
1213
+ }
1214
+ if (chat.response_format?.type === "json_object") {
1215
+ params.text = { format: { type: "json_object" } };
1216
+ } else if (chat.response_format?.type === "json_schema") {
1217
+ params.text = {
1218
+ format: {
1219
+ type: "json_schema",
1220
+ name: chat.response_format.json_schema.name,
1221
+ schema: chat.response_format.json_schema.schema ?? {},
1222
+ ...chat.response_format.json_schema.strict !== void 0 && {
1223
+ strict: chat.response_format.json_schema.strict
1224
+ }
1225
+ }
1226
+ };
1227
+ }
1228
+ return params;
1229
+ }
1230
+ function mapInputMessage(message) {
1231
+ if (message.role === "tool")
1232
+ return [
1233
+ {
1234
+ type: "function_call_output",
1235
+ call_id: message.tool_call_id,
1236
+ output: typeof message.content === "string" ? message.content : JSON.stringify(message.content)
1237
+ }
1238
+ ];
1239
+ if (message.role === "function") return [];
1240
+ const items = [];
1241
+ if (message.content !== null && message.content !== void 0) {
1242
+ items.push({
1243
+ role: message.role,
1244
+ content: typeof message.content === "string" ? [{ type: "input_text", text: message.content }] : message.content.flatMap(mapInputContent)
1245
+ });
1246
+ }
1247
+ items.push(...mapInputToolCalls(message));
1248
+ return items;
1249
+ }
1250
+ function mapInputToolCalls(message) {
1251
+ if (message.role !== "assistant") return [];
1252
+ return (message.tool_calls ?? []).flatMap(
1253
+ (call) => call.type === "function" ? [
1254
+ {
1255
+ type: "function_call",
1256
+ call_id: call.id,
1257
+ name: call.function.name,
1258
+ arguments: call.function.arguments
1259
+ }
1260
+ ] : []
1261
+ );
1262
+ }
1263
+ function mapInputContent(part) {
1264
+ if (part.type === "text") return [{ type: "input_text", text: part.text }];
1265
+ if (part.type === "image_url")
1266
+ return [
1267
+ {
1268
+ type: "input_image",
1269
+ image_url: part.image_url.url,
1270
+ detail: part.image_url.detail ?? "auto"
1271
+ }
1272
+ ];
1273
+ return [];
1274
+ }
1275
+ function responsesToChatCompletion(response) {
1276
+ if (response.status === "failed" || response.status === "cancelled") {
1277
+ throw new ModelError(response.error?.message ?? `Responses request ${response.status}`, {
1278
+ code: ErrorCode.MODEL_ERROR,
1279
+ context: { reason: response.status }
1280
+ });
1281
+ }
1282
+ const usage = responseUsage(response.usage);
1283
+ return {
1284
+ id: response.id,
1285
+ model: response.model,
1286
+ object: "chat.completion",
1287
+ created: response.created_at,
1288
+ choices: responseChoices(response),
1289
+ ...usage !== void 0 && { usage }
1290
+ };
1291
+ }
1292
+ function responseText(response) {
1293
+ return response.output.flatMap(
1294
+ (item) => item.type === "message" ? item.content.filter((part) => part.type === "output_text").map((part) => part.text) : []
1295
+ ).join("");
1296
+ }
1297
+ function responseToolCalls(response) {
1298
+ return response.output.flatMap(
1299
+ (item) => item.type === "function_call" ? [
1300
+ {
1301
+ id: item.call_id,
1302
+ type: "function",
1303
+ function: { name: item.name, arguments: item.arguments }
1304
+ }
1305
+ ] : []
1306
+ );
1307
+ }
1308
+ function responseChoices(response) {
1309
+ const text = responseText(response);
1310
+ const calls = responseToolCalls(response);
1311
+ const refused = responseRefused(response);
1312
+ const finishReason = refused ? "content_filter" : response.status === "incomplete" ? "length" : calls.length > 0 ? "tool_calls" : "stop";
1313
+ if (text === "" && calls.length === 0 && !refused && finishReason !== "length") return [];
1314
+ return [
1315
+ {
1316
+ index: 0,
1317
+ finish_reason: finishReason,
1318
+ logprobs: null,
1319
+ message: {
1320
+ role: "assistant",
1321
+ content: text,
1322
+ refusal: null,
1323
+ ...calls.length > 0 && { tool_calls: calls }
1324
+ }
1325
+ }
1326
+ ];
1327
+ }
1328
+ function responseRefused(response) {
1329
+ return response.incomplete_details?.reason === "content_filter" || response.output.some(
1330
+ (item) => item.type === "message" && item.content.some((part) => part.type === "refusal")
1331
+ );
1332
+ }
1333
+ function responseUsage(usage) {
1334
+ if (usage === void 0 || usage === null || typeof usage.input_tokens !== "number" || typeof usage.output_tokens !== "number" || typeof usage.total_tokens !== "number")
1335
+ return void 0;
1336
+ return {
1337
+ prompt_tokens: usage.input_tokens,
1338
+ completion_tokens: usage.output_tokens,
1339
+ total_tokens: usage.total_tokens,
1340
+ ...usage.input_tokens_details !== void 0 && {
1341
+ prompt_tokens_details: { cached_tokens: usage.input_tokens_details.cached_tokens }
1342
+ },
1343
+ ...usage.output_tokens_details !== void 0 && {
1344
+ completion_tokens_details: { reasoning_tokens: usage.output_tokens_details.reasoning_tokens }
1345
+ }
1346
+ };
1347
+ }
1348
+
1349
+ // src/adapters/openai-responses-stream.ts
1350
+ async function* mapResponsesStream(events, mapResponse) {
1351
+ const blocks = /* @__PURE__ */ new Map();
1352
+ let completed = false;
1353
+ for await (const event of events) {
1354
+ assertStreamEvent(event);
1355
+ if (event.type === "response.completed" || event.type === "response.incomplete") {
1356
+ const response = mapResponse(event.response);
1357
+ yield {
1358
+ type: "message_delta",
1359
+ delta: { stop_reason: response.stopReason },
1360
+ ...response.usage !== void 0 && { usage: response.usage }
1361
+ };
1362
+ yield { type: "message_stop" };
1363
+ completed = true;
1364
+ } else {
1365
+ yield* mapContentEvent(event, blocks);
1366
+ }
1367
+ }
1368
+ if (!completed)
1369
+ throw new ModelError("Responses stream ended without a terminal response", {
1370
+ code: ErrorCode.MODEL_ERROR
1371
+ });
1372
+ }
1373
+ function assertStreamEvent(event) {
1374
+ if (event.type === "response.failed")
1375
+ throw new ModelError(event.response.error?.message ?? "Responses stream failed", {
1376
+ code: ErrorCode.MODEL_ERROR
1377
+ });
1378
+ if (event.type === "error") throw new ModelError(event.message, { code: ErrorCode.MODEL_ERROR });
1379
+ if (event.type === "response.refusal.delta" || event.type === "response.refusal.done") {
1380
+ throw new ModelError("Responses reply was refused", { code: ErrorCode.MODEL_ERROR });
1381
+ }
1382
+ }
1383
+ function mapContentEvent(event, blocks) {
1384
+ switch (event.type) {
1385
+ case "response.created":
1386
+ return [{ type: "message_start", message: { model: event.response.model } }];
1387
+ case "response.output_text.delta":
1388
+ return textDeltaChunks(event, blocks);
1389
+ case "response.output_text.done": {
1390
+ const index = blocks.get(textBlockKey(event));
1391
+ return index === void 0 ? [] : [{ type: "content_block_stop", index }];
1392
+ }
1393
+ case "response.output_item.done": {
1394
+ if (event.item.type !== "function_call") return [];
1395
+ const index = blocks.size;
1396
+ blocks.set(`tool:${String(event.output_index)}`, index);
1397
+ return [
1398
+ {
1399
+ type: "content_block_start",
1400
+ index,
1401
+ contentBlock: {
1402
+ type: "tool_use",
1403
+ id: event.item.call_id,
1404
+ name: event.item.name,
1405
+ input: parseArguments(event.item.arguments)
1406
+ }
1407
+ },
1408
+ { type: "content_block_stop", index }
1409
+ ];
1410
+ }
1411
+ default:
1412
+ return [];
1413
+ }
1414
+ }
1415
+ function textBlockKey(event) {
1416
+ return `${String(event.output_index)}:${String(event.content_index)}`;
1417
+ }
1418
+ function textDeltaChunks(event, blocks) {
1419
+ const key = textBlockKey(event);
1420
+ const chunks = [];
1421
+ let index = blocks.get(key);
1422
+ if (index === void 0) {
1423
+ index = blocks.size;
1424
+ blocks.set(key, index);
1425
+ chunks.push({ type: "content_block_start", index, contentBlock: { type: "text", text: "" } });
1426
+ }
1427
+ chunks.push({
1428
+ type: "content_block_delta",
1429
+ index,
1430
+ delta: { type: "text_delta", text: event.delta }
1431
+ });
1432
+ return chunks;
1433
+ }
1434
+ function parseArguments(argumentsText) {
1435
+ try {
1436
+ return JSON.parse(argumentsText);
1437
+ } catch {
1438
+ return { _raw: argumentsText };
1439
+ }
1440
+ }
1441
+
1442
+ // src/adapters/openai-adapter.ts
1443
+ function kvTag(key, val) {
1444
+ return val === null || val === void 0 || val === "" ? "" : `${key}=${String(val)}`;
1445
+ }
1446
+ function redactErrorText(text, secrets) {
1447
+ return sanitizeErrorDetails(
1448
+ redactGatewaySecrets(text, secrets, REDACTED_KEY_PLACEHOLDER),
1449
+ secrets.apiKey
1450
+ );
1451
+ }
1452
+ function describeOpenAIApiError(v, secrets) {
1453
+ const tags = [
1454
+ `HTTP ${String(v.status ?? "unknown")}`,
1455
+ kvTag("type", v.type),
1456
+ kvTag("code", v.code),
1457
+ kvTag("param", v.param),
1458
+ kvTag("request_id", v.requestID)
1459
+ ].filter((t) => t !== "");
1460
+ let body = "";
1461
+ if (v.error !== void 0 && v.error !== null) {
1462
+ try {
1463
+ const raw = JSON.stringify(v.error);
1464
+ body = ` body=${raw.length > MAX_ERROR_BODY_CHARS ? `${raw.slice(0, MAX_ERROR_BODY_CHARS)}\u2026` : raw}`;
1465
+ } catch {
1466
+ body = " body=<unserializable>";
1467
+ }
1468
+ }
1469
+ return redactErrorText(`${tags.join(" ")}: ${v.message ?? ""}${body}`, secrets);
1470
+ }
1471
+ var MAX_ERROR_BODY_CHARS = 600;
1472
+ var OpenAIAdapter = class extends BaseAdapter {
1473
+ client;
1474
+ resolvedModelId;
1475
+ apiSurface;
1476
+ omitDefaultTokenCap;
1477
+ apiKey;
1478
+ /** Headers sent on every request; each value is redacted from error text. */
1479
+ headers;
1480
+ /**
1481
+ * Creates a new OpenAIAdapter instance.
1482
+ *
1483
+ * @param config - OpenAI adapter configuration
1484
+ * @throws {ConfigError} If API key is missing
1485
+ */
1486
+ constructor(config) {
1487
+ const resolvedModelId = config.verbatimModelId === true ? config.modelId : resolveModelId(config.modelId);
1488
+ const baseConfig = {
1489
+ providerId: "openai",
1490
+ modelId: resolvedModelId,
1491
+ capabilities: getModelCapabilities(config.modelId),
1492
+ apiKey: config.apiKey,
1493
+ ...config.logger !== void 0 && { logger: config.logger }
1494
+ };
1495
+ if (config.baseUrl !== void 0) {
1496
+ baseConfig.baseUrl = config.baseUrl;
1497
+ }
1498
+ if (config.timeout !== void 0) {
1499
+ baseConfig.timeout = config.timeout;
1500
+ }
1501
+ if (config.maxRetries !== void 0) {
1502
+ baseConfig.maxRetries = config.maxRetries;
1503
+ }
1504
+ super(baseConfig);
1505
+ this.apiKey = config.apiKey;
1506
+ this.headers = config.defaultHeaders;
1507
+ this.resolvedModelId = resolvedModelId;
1508
+ this.apiSurface = config.apiSurface ?? "chat";
1509
+ this.omitDefaultTokenCap = config.omitDefaultTokenCap ?? false;
1510
+ requireApiKey(config.apiKey, "OpenAI", config.modelId);
1511
+ this.client = this.createClient(config);
1512
+ }
1513
+ /**
1514
+ * Creates the OpenAI client with configuration.
1515
+ */
1516
+ createClient(config) {
1517
+ const clientOptions = {
1518
+ apiKey: config.apiKey,
1519
+ maxRetries: config.maxRetries ?? 2
1520
+ };
1521
+ if (config.baseUrl !== void 0) {
1522
+ clientOptions.baseURL = config.baseUrl;
1523
+ }
1524
+ if (config.timeout !== void 0) {
1525
+ clientOptions.timeout = config.timeout;
1526
+ }
1527
+ if (config.organization !== void 0) {
1528
+ clientOptions.organization = config.organization;
1529
+ }
1530
+ if (config.defaultHeaders !== void 0) {
1531
+ clientOptions.defaultHeaders = config.defaultHeaders;
1532
+ }
1533
+ if (config.fetchOptions !== void 0) {
1534
+ clientOptions.fetchOptions = config.fetchOptions;
1535
+ }
1536
+ clientOptions.fetch = config.fetch;
1537
+ return new OpenAI(clientOptions);
1538
+ }
1539
+ /**
1540
+ * Validates adapter configuration.
1541
+ * Extends base validation with OpenAI-specific checks.
1542
+ */
1543
+ validateConfig() {
1544
+ const baseResult = super.validateConfig();
1545
+ if (!baseResult.ok) {
1546
+ return baseResult;
1547
+ }
1548
+ const keyResult = validateApiKeyPresence(this.config.apiKey, this.providerId, this.modelId);
1549
+ if (!keyResult.ok) return keyResult;
1550
+ return ok(void 0);
1551
+ }
1552
+ /**
1553
+ * Send a completion request to OpenAI.
1554
+ *
1555
+ * @param request - The completion request
1556
+ * @returns Result with response or ModelError
1557
+ */
1558
+ async complete(request) {
1559
+ this.logRequest(request);
1560
+ try {
1561
+ const response = await this.executeCompletion(request);
1562
+ this.logResponse(response);
1563
+ return ok(response);
1564
+ } catch (error) {
1565
+ return err(this.transformError(error));
1566
+ }
1567
+ }
1568
+ /**
1569
+ * Surface an OpenAI / OpenAI-compatible gateway's REAL HTTP status + response
1570
+ * body in the error (#4047). The OpenAI SDK's default message collapses a
1571
+ * gateway rejection to e.g. `"400 status code (no body)"`, which hides WHY a
1572
+ * litellm-style gateway rejected a request — exactly the wall hit when
1573
+ * diagnosing degraded voter panels on a custom gateway. We re-message with the
1574
+ * status/type/code/param/request-id/body. Error-code routing is unchanged: we
1575
+ * classify on the ORIGINAL signal (`status`/`code`/`message`) via a clean probe
1576
+ * error, so the diagnostic `request_id`/`body` can't pollute the message-
1577
+ * substring classifier, then re-message the result with the full detail.
1578
+ * Applies to direct OpenAI and the compat gateway alike.
1579
+ */
1580
+ transformError(error) {
1581
+ const secrets = { apiKey: this.apiKey, headers: this.headers };
1582
+ if (error instanceof APIError) {
1583
+ if (error.cause instanceof ConfigError) {
1584
+ const message = redactErrorText(error.cause.message, secrets);
1585
+ return new ModelError(message, { code: error.cause.code, cause: error.cause });
1586
+ }
1587
+ const v = error;
1588
+ const probe = new Error(typeof v.message === "string" ? v.message : "");
1589
+ probe.status = typeof v.status === "number" ? v.status : void 0;
1590
+ probe.code = v.code ?? void 0;
1591
+ probe.param = typeof v.param === "string" && v.param !== "" ? v.param : void 0;
1592
+ probe.cause = error;
1593
+ const classified = super.transformError(probe);
1594
+ classified.message = `${this.providerId}/${this.modelId}: ${describeOpenAIApiError(v, secrets)}`;
1595
+ return classified;
1596
+ }
1597
+ const err2 = super.transformError(error);
1598
+ err2.message = redactErrorText(err2.message, secrets);
1599
+ return err2;
1600
+ }
1601
+ /**
1602
+ * Stream a completion request from OpenAI.
1603
+ *
1604
+ * @param request - The completion request
1605
+ * @yields StreamChunk objects as they arrive
1606
+ */
1607
+ async *stream(request) {
1608
+ this.logRequest(request);
1609
+ const [controller, iterable] = createStream();
1610
+ this.executeStream(request, controller).catch((error) => {
1611
+ const modelError = this.transformError(error);
1612
+ controller.error(modelError);
1613
+ });
1614
+ yield* iterable;
1615
+ }
1616
+ /**
1617
+ * Count tokens in text using OpenAI-specific estimation.
1618
+ *
1619
+ * @param text - Text to count tokens for
1620
+ * @returns Approximate token count
1621
+ */
1622
+ countTokens(text) {
1623
+ return Promise.resolve(getTokenEstimator().estimateText(text, "openai"));
1624
+ }
1625
+ /**
1626
+ * Executes the completion request against the OpenAI API.
1627
+ */
1628
+ async executeCompletion(request) {
1629
+ const { params, dropped } = this.buildRequestParams(request);
1630
+ if (this.apiSurface === "responses") {
1631
+ const result = await completeResponses(this.client, { params, dropped }, request.signal);
1632
+ return this.mapResponse(result.response, result.dropped);
1633
+ }
1634
+ const response = request.signal !== void 0 ? await this.client.chat.completions.create(params, { signal: request.signal }) : await this.client.chat.completions.create(params);
1635
+ return this.mapResponse(response, dropped);
1636
+ }
1637
+ /**
1638
+ * Executes streaming completion and pushes chunks to the controller.
1639
+ */
1640
+ async executeStream(request, controller) {
1641
+ try {
1642
+ const { params } = this.buildRequestParams(request);
1643
+ if (this.apiSurface === "responses") {
1644
+ const stream2 = await this.client.responses.create(
1645
+ { ...buildResponsesParams(params), stream: true },
1646
+ { signal: request.signal }
1647
+ );
1648
+ for await (const chunk of mapResponsesStream(
1649
+ stream2,
1650
+ (response) => this.mapResponse(responsesToChatCompletion(response))
1651
+ ))
1652
+ controller.push(chunk);
1653
+ controller.complete();
1654
+ return;
1655
+ }
1656
+ const stream = await this.client.chat.completions.create({ ...params, stream: true });
1657
+ let contentIndex = 0;
1658
+ let hasStarted = false;
1659
+ const toolCalls = createStreamToolCallState();
1660
+ for await (const chunk of stream) {
1661
+ if (chunk.choices[0]?.finish_reason === "content_filter") {
1662
+ throw nonAnswerError(`${this.providerId}/${this.modelId}`, CONTENT_FILTERED, chunk.model);
1663
+ }
1664
+ const mappedChunks = mapStreamChunk(chunk, contentIndex, hasStarted, toolCalls);
1665
+ for (const mappedChunk of mappedChunks) {
1666
+ if (mappedChunk.type === "message_start") {
1667
+ hasStarted = true;
1668
+ }
1669
+ if (mappedChunk.type === "content_block_start") {
1670
+ contentIndex++;
1671
+ }
1672
+ controller.push(mappedChunk);
1673
+ }
1674
+ }
1675
+ controller.complete();
1676
+ } catch (error) {
1677
+ const modelError = this.transformError(error);
1678
+ controller.error(modelError);
1679
+ }
1680
+ }
1681
+ /**
1682
+ * Builds OpenAI API request parameters from our CompletionRequest.
1683
+ */
1684
+ buildRequestParams(request) {
1685
+ const messages = this.buildMessages(request);
1686
+ const params = {
1687
+ model: this.resolvedModelId,
1688
+ messages,
1689
+ ...request.maxTokens !== void 0 || !this.omitDefaultTokenCap ? { max_completion_tokens: request.maxTokens ?? this.defaultMaxCompletionTokens() } : {}
1690
+ };
1691
+ const dropped = this.addOptionalParams(params, request);
1692
+ return { params, dropped };
1693
+ }
1694
+ /**
1695
+ * Builds the messages array for the request.
1696
+ */
1697
+ buildMessages(request) {
1698
+ const messages = [];
1699
+ if (request.systemPrompt !== void 0 && request.systemPrompt !== "") {
1700
+ messages.push({ role: "system", content: request.systemPrompt });
1701
+ }
1702
+ for (const msg of request.messages) {
1703
+ if (msg.role === "system" && request.systemPrompt !== void 0) {
1704
+ continue;
1705
+ }
1706
+ messages.push(...mapMessage(msg));
1707
+ }
1708
+ return messages;
1709
+ }
1710
+ /**
1711
+ * Adds optional parameters to the request.
1712
+ */
1713
+ addOptionalParams(params, request) {
1714
+ const plan = planOptionalParams(request, this.resolvedModelId);
1715
+ if (plan.temperature !== void 0) {
1716
+ params.temperature = plan.temperature;
1717
+ }
1718
+ if (request.stop !== void 0 && request.stop.length > 0) {
1719
+ params.stop = request.stop;
1720
+ }
1721
+ if (request.tools !== void 0 && request.tools.length > 0) {
1722
+ params.tools = request.tools.map(mapTool);
1723
+ }
1724
+ this.addResponseFormat(params, request);
1725
+ return plan.dropped;
1726
+ }
1727
+ /**
1728
+ * Adds response format to the request if specified.
1729
+ */
1730
+ addResponseFormat(params, request) {
1731
+ if (request.responseFormat === void 0) {
1732
+ return;
1733
+ }
1734
+ if (request.responseFormat.type === "json_object") {
1735
+ params.response_format = { type: "json_object" };
1736
+ } else if (request.responseFormat.type === "json_schema") {
1737
+ params.response_format = {
1738
+ type: "json_schema",
1739
+ json_schema: {
1740
+ name: "response",
1741
+ schema: request.responseFormat.schema
1742
+ }
1743
+ };
1744
+ }
1745
+ }
1746
+ /**
1747
+ * Maps OpenAI API response to our CompletionResponse format.
1748
+ */
1749
+ mapResponse(response, dropped = []) {
1750
+ const firstChoice = response.choices[0];
1751
+ if (firstChoice === void 0) {
1752
+ throw nonAnswerError(
1753
+ `${this.providerId}/${this.modelId}`,
1754
+ { reason: "no_choices", detail: "the response contained no choices" },
1755
+ response.model
1756
+ );
1757
+ }
1758
+ const nonAnswer = detectNonAnswer(firstChoice, response.usage, this.isReasoningFamily());
1759
+ if (nonAnswer !== void 0)
1760
+ throw nonAnswerError(`${this.providerId}/${this.modelId}`, nonAnswer, response.model);
1761
+ const content = mapChoiceToContentBlocks(firstChoice);
1762
+ const usage = mapResponseUsage(response);
1763
+ return {
1764
+ content,
1765
+ ...usage !== void 0 ? { usage } : {},
1766
+ stopReason: mapStopReason(firstChoice.finish_reason),
1767
+ model: response.model,
1768
+ // #4069: surface dropped params (e.g. an omitted temperature). Omitted
1769
+ // entirely when nothing was dropped (exactOptionalPropertyTypes).
1770
+ ...dropped.length > 0 ? { warnings: dropped } : {}
1771
+ };
1772
+ }
1773
+ /**
1774
+ * Reasoning families are the models that take `max_completion_tokens`
1775
+ * (o-series, gpt-5, codex); the registry answers first, a regex second.
1776
+ */
1777
+ isReasoningFamily() {
1778
+ return getMaxTokensParamForModel(this.resolvedModelId) === "max_completion_tokens";
1779
+ }
1780
+ /**
1781
+ * Default completion cap when the request sets none (#6607). On a reasoning
1782
+ * model the cap covers hidden reasoning tokens AND the visible reply, and
1783
+ * 4,096 can be spent entirely on reasoning, leaving an empty reply. OpenAI's
1784
+ * reasoning guide recommends reserving at least 25,000 tokens for reasoning
1785
+ * and output when starting out, so that is the reasoning default. It is a
1786
+ * ceiling, not a spend: a reply that finishes early is billed for what it
1787
+ * used. Other models keep `DEFAULT_MAX_TOKENS`.
1788
+ */
1789
+ defaultMaxCompletionTokens() {
1790
+ return this.isReasoningFamily() ? REASONING_DEFAULT_MAX_COMPLETION_TOKENS : DEFAULT_MAX_TOKENS;
1791
+ }
1792
+ /**
1793
+ * (#2529) List models served by this OpenAI-compatible endpoint.
1794
+ *
1795
+ * Wraps `GET /v1/models`. Result is cached for `LIST_MODELS_TTL_MS`
1796
+ * so identity resolution doesn't round-trip on every adapter.
1797
+ * Concurrent callers share the in-flight promise.
1798
+ *
1799
+ * Throws on non-2xx so the harness-side identity resolver knows to
1800
+ * fall back to modelId parsing — silent empty-list returns would be
1801
+ * indistinguishable from "this gateway has no models", which a
1802
+ * misconfigured endpoint shouldn't be allowed to claim.
1803
+ */
1804
+ async listModels() {
1805
+ const now = Date.now();
1806
+ if (this.modelsCache !== null && now - this.modelsCache.fetchedAt < LIST_MODELS_TTL_MS) {
1807
+ return this.modelsCache.value;
1808
+ }
1809
+ if (this.modelsInFlight !== null) {
1810
+ return this.modelsInFlight;
1811
+ }
1812
+ const inFlight = this.fetchModels();
1813
+ this.modelsInFlight = inFlight;
1814
+ try {
1815
+ const value = await inFlight;
1816
+ this.modelsCache = { value, fetchedAt: Date.now() };
1817
+ return value;
1818
+ } finally {
1819
+ this.modelsInFlight = null;
1820
+ }
1821
+ }
1822
+ modelsCache = null;
1823
+ modelsInFlight = null;
1824
+ async fetchModels() {
1825
+ const list = await this.client.models.list();
1826
+ return list.data.map((m) => {
1827
+ const meta = { id: m.id };
1828
+ if (typeof m.owned_by === "string") {
1829
+ return { ...meta, ownedBy: m.owned_by, createdAt: m.created };
1830
+ }
1831
+ if (typeof m.created === "number") {
1832
+ return { ...meta, createdAt: m.created };
1833
+ }
1834
+ return meta;
1835
+ });
1836
+ }
1837
+ };
1838
+ var LIST_MODELS_TTL_MS = 5 * 60 * 1e3;
1839
+ var REASONING_DEFAULT_MAX_COMPLETION_TOKENS = 25e3;
1840
+ function nonAnswerError(source, nonAnswer, servedModel) {
1841
+ return new ModelError(`${source}: ${nonAnswer.detail}`, {
1842
+ code: ErrorCode.MODEL_ERROR,
1843
+ context: {
1844
+ reason: nonAnswer.reason,
1845
+ ...servedModel !== void 0 ? { servedModel } : {},
1846
+ ...nonAnswer.reasoningTokens !== void 0 ? { reasoningTokens: nonAnswer.reasoningTokens } : {}
1847
+ }
1848
+ });
1849
+ }
1850
+ function createOpenAIAdapter(config) {
1851
+ return new OpenAIAdapter(config);
1852
+ }
1853
+
1854
+ // src/adapters/gateway-usage-recording.ts
1855
+ var recordingSources = /* @__PURE__ */ new WeakMap();
1856
+ function withGatewayUsageRecording(inner, gatewayArm, modelVerified, providerId) {
1857
+ inner = recordingSources.get(inner) ?? inner;
1858
+ const recordedProviderId = providerId ?? inner.providerId;
1859
+ const wrapped = {
1860
+ gatewayArm,
1861
+ providerId: recordedProviderId,
1862
+ modelId: inner.modelId,
1863
+ capabilities: inner.capabilities,
1864
+ countTokens: (text) => inner.countTokens(text),
1865
+ validateConfig: () => inner.validateConfig(),
1866
+ stream: (request) => inner.stream(request),
1867
+ async complete(request) {
1868
+ const start = getTimeProvider().now();
1869
+ const result = await inner.complete(request);
1870
+ const latencyMs = getTimeProvider().now() - start;
1871
+ try {
1872
+ if (result.ok) {
1873
+ const u = result.value.usage;
1874
+ if (u === void 0) return result;
1875
+ const cost = gatewayCostDetail(gatewayArm, inner.modelId, u.inputTokens, u.outputTokens);
1876
+ recordUsageEvent({
1877
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1878
+ modelId: inner.modelId,
1879
+ providerId: recordedProviderId,
1880
+ inputTokens: u.inputTokens,
1881
+ outputTokens: u.outputTokens,
1882
+ usdCost: cost.costUsd,
1883
+ latencyMs,
1884
+ success: true,
1885
+ priced: cost.priced,
1886
+ ...modelVerified !== void 0 && { modelVerified },
1887
+ ...cost.priced ? { priceSource: cost.resolvedId } : {}
1888
+ });
1889
+ } else {
1890
+ recordFailedCall(inner, latencyMs, result.error.code, modelVerified, recordedProviderId);
1891
+ }
1892
+ } catch {
1893
+ }
1894
+ return result;
1895
+ }
1896
+ };
1897
+ recordingSources.set(wrapped, inner);
1898
+ attachListModels(wrapped, inner);
1899
+ return wrapped;
1900
+ }
1901
+ function recordFailedCall(inner, latencyMs, errorCode, modelVerified, providerId) {
1902
+ recordUsageEvent({
1903
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1904
+ modelId: inner.modelId,
1905
+ providerId,
1906
+ inputTokens: 0,
1907
+ outputTokens: 0,
1908
+ usdCost: 0,
1909
+ latencyMs,
1910
+ success: false,
1911
+ errorCode,
1912
+ ...modelVerified !== void 0 && { modelVerified }
1913
+ });
1914
+ }
1915
+ function attachListModels(wrapped, inner) {
1916
+ const list = inner.listModels?.bind(inner);
1917
+ if (list === void 0) return;
1918
+ wrapped.listModels = () => list();
1919
+ }
1920
+
1921
+ // src/config/opencode-bridge.ts
1922
+ import { readFileSync } from "node:fs";
1923
+ var defaultLogger = createLogger({ component: "opencode-bridge" });
1924
+ function readOpencodeGateway(path, logger2 = defaultLogger) {
1925
+ const raw = readFileOrNull(path, logger2);
1926
+ if (raw === null) return null;
1927
+ const parsed = parseJsonOrNull(raw, path, logger2);
1928
+ if (parsed === null) return null;
1929
+ const options = extractOpenAICompatOptions(parsed);
1930
+ if (options === null) {
1931
+ logger2.debug("opencode.json has no providers.openai-compat.options block", { path });
1932
+ return null;
1933
+ }
1934
+ const baseURL = typeof options.baseURL === "string" ? options.baseURL.trim() : "";
1935
+ const apiKeyRaw = typeof options.apiKey === "string" ? options.apiKey.trim() : "";
1936
+ if (baseURL === "" || apiKeyRaw === "") {
1937
+ logger2.warn("opencode.json providers.openai-compat missing baseURL or apiKey", { path });
1938
+ return null;
1939
+ }
1940
+ const apiKey = resolveEnvInterpolation(apiKeyRaw);
1941
+ if (apiKey === null) {
1942
+ logger2.warn(
1943
+ "opencode.json apiKey references an env var that is not set; gateway not configured",
1944
+ { path }
1945
+ );
1946
+ return null;
1947
+ }
1948
+ logger2.info("Gateway config sourced from opencode.json", { host: hostnameOf(baseURL), path });
1949
+ return { baseURL, apiKey };
1950
+ }
1951
+ function readFileOrNull(path, logger2) {
1952
+ try {
1953
+ return readFileSync(path, "utf8");
1954
+ } catch (err2) {
1955
+ const msg = err2 instanceof Error ? err2.message : String(err2);
1956
+ logger2.debug("Could not read opencode.json", { path, error: msg });
1957
+ return null;
1958
+ }
1959
+ }
1960
+ function parseJsonOrNull(raw, path, logger2) {
1961
+ try {
1962
+ return JSON.parse(raw);
1963
+ } catch (err2) {
1964
+ const name = err2 instanceof Error ? err2.name : "unknown error";
1965
+ logger2.warn("opencode.json is not valid JSON; ignoring", { path, error: name });
1966
+ return null;
1967
+ }
1968
+ }
1969
+ function extractOpenAICompatOptions(parsed) {
1970
+ if (typeof parsed !== "object" || parsed === null) return null;
1971
+ const root = parsed;
1972
+ const providers = root.providers;
1973
+ if (typeof providers !== "object" || providers === null) return null;
1974
+ const provider = providers["openai-compat"];
1975
+ if (typeof provider !== "object" || provider === null) return null;
1976
+ const options = provider.options;
1977
+ if (typeof options !== "object" || options === null) return null;
1978
+ return options;
1979
+ }
1980
+ function resolveEnvInterpolation(value) {
1981
+ const match = /^\{env:([A-Z0-9_]+)\}$/.exec(value);
1982
+ if (match === null) return value;
1983
+ const envName = match[1];
1984
+ if (envName === void 0) return null;
1985
+ const resolved = process.env[envName];
1986
+ if (resolved === void 0 || resolved === "") return null;
1987
+ return resolved;
1988
+ }
1989
+
1990
+ // src/adapters/gateway-client-guard.ts
1991
+ function validateOpenAICompatBaseUrl(baseUrl) {
1992
+ const validated = validateCustomApiBaseUrl(baseUrl);
1993
+ if (!validated.ok) throw validated.error;
1994
+ return validated.value.toString();
1995
+ }
1996
+ function guardedGatewayFetch(baseUrl) {
1997
+ let allowed;
1998
+ const check = async () => {
1999
+ const status = await checkGatewayHost(baseUrl);
2000
+ if (status.state === "refused_private_host") throw new GatewayHostRefusedError(status);
2001
+ if (status.state === "lookup_timed_out") {
2002
+ throw new ConfigError(`${GATEWAY_HOST_LOOKUP_TIMED_OUT} (host ${status.host})`);
2003
+ }
2004
+ };
2005
+ return async (input, init) => {
2006
+ allowed ??= check();
2007
+ try {
2008
+ await allowed;
2009
+ } catch (error) {
2010
+ allowed = void 0;
2011
+ throw error;
2012
+ }
2013
+ return fetch(input, init);
2014
+ };
2015
+ }
2016
+
2017
+ // src/adapters/gateway-http.ts
2018
+ import { BlockList, isIP } from "node:net";
2019
+ import { ProxyAgent } from "undici";
2020
+ var OPENAI_COMPAT_AUTH_HEADER_ENV = "NEXUS_OPENAI_COMPAT_AUTH_HEADER";
2021
+ var OPENAI_COMPAT_EXTRA_HEADERS_ENV = "NEXUS_OPENAI_COMPAT_EXTRA_HEADERS";
2022
+ var HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
2023
+ var CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
2024
+ function parseGatewayExtraHeaders(raw) {
2025
+ const headers = {};
2026
+ if (raw === void 0 || raw.trim() === "") return { ok: true, headers };
2027
+ if (/[\r\n]/.test(raw)) return { ok: false, reason: "contains a newline" };
2028
+ const seen = /* @__PURE__ */ new Set();
2029
+ for (const [index, entry] of raw.split(",").entries()) {
2030
+ if (entry.trim() === "") continue;
2031
+ const at = `entry ${String(index + 1)}`;
2032
+ const parsed = parseHeaderEntry(entry, at);
2033
+ if ("reason" in parsed) return { ok: false, reason: parsed.reason };
2034
+ const key = parsed.name.toLowerCase();
2035
+ if (seen.has(key)) return { ok: false, reason: `${at} is a duplicate header name` };
2036
+ seen.add(key);
2037
+ headers[parsed.name] = parsed.value;
2038
+ }
2039
+ return { ok: true, headers };
2040
+ }
2041
+ function parseHeaderEntry(entry, at) {
2042
+ const eq = entry.indexOf("=");
2043
+ if (eq < 0) return { reason: `${at} has no '='` };
2044
+ const name = entry.slice(0, eq).trim();
2045
+ const value = entry.slice(eq + 1).trim();
2046
+ if (!HEADER_NAME.test(name)) return { reason: `${at} has an illegal header name` };
2047
+ if (CONTROL_CHARS.test(value)) return { reason: `${at} value contains a control character` };
2048
+ if (name.toLowerCase() === "authorization") {
2049
+ return { reason: `${at} sets Authorization; use ${OPENAI_COMPAT_AUTH_HEADER_ENV}` };
2050
+ }
2051
+ return { name, value };
2052
+ }
2053
+ function authHeaderRejection(raw) {
2054
+ return HEADER_NAME.test(raw) ? void 0 : "not a legal header name";
2055
+ }
2056
+ function firstSet(env, names) {
2057
+ for (const name of names) {
2058
+ const value = env[name]?.trim();
2059
+ if (value !== void 0 && value !== "") return value;
2060
+ }
2061
+ return void 0;
2062
+ }
2063
+ function splitNoProxyEntry(entry) {
2064
+ const bracketed = /^\[([^\]]+)\](?::(\d+))?$/.exec(entry);
2065
+ if (bracketed !== null) return { host: bracketed[1] ?? "", port: bracketed[2] };
2066
+ const parts = entry.split(":");
2067
+ if (parts.length === 2 && /^\d+$/.test(parts[1] ?? "")) {
2068
+ return { host: parts[0] ?? "", port: parts[1] };
2069
+ }
2070
+ return { host: entry, port: void 0 };
2071
+ }
2072
+ function matchesNoProxy(url, noProxy) {
2073
+ const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
2074
+ const port = url.port !== "" ? url.port : url.protocol === "https:" ? "443" : "80";
2075
+ return noProxy.split(/[\s,]+/).some((raw) => noProxyEntryMatches(raw.trim().toLowerCase(), host, port));
2076
+ }
2077
+ function noProxyEntryMatches(entry, host, port) {
2078
+ if (entry === "") return false;
2079
+ if (entry === "*") return true;
2080
+ const parsed = splitNoProxyEntry(entry);
2081
+ if (parsed.port !== void 0 && parsed.port !== port) return false;
2082
+ const hostFamily = isIP(host);
2083
+ if (hostFamily === 4 || hostFamily === 6) {
2084
+ return ipEntryMatches(parsed.host, host, hostFamily);
2085
+ }
2086
+ const suffix = parsed.host.replace(/^\*?\./, "");
2087
+ return suffix !== "" && (host === suffix || host.endsWith(`.${suffix}`));
2088
+ }
2089
+ function ipEntryMatches(entry, host, family) {
2090
+ const type = family === 4 ? "ipv4" : "ipv6";
2091
+ const slash = entry.indexOf("/");
2092
+ const address = slash === -1 ? entry : entry.slice(0, slash);
2093
+ if (isIP(address) !== family) return false;
2094
+ const range = new BlockList();
2095
+ if (slash === -1) {
2096
+ range.addAddress(address, type);
2097
+ return range.check(host, type);
2098
+ }
2099
+ const prefixText = entry.slice(slash + 1);
2100
+ const prefix = Number(prefixText);
2101
+ if (!/^\d{1,3}$/.test(prefixText) || prefix > (family === 4 ? 32 : 128)) return false;
2102
+ range.addSubnet(address, prefix, type);
2103
+ return range.check(host, type);
2104
+ }
2105
+ function gatewayProxyUrl(baseUrl, env) {
2106
+ let url;
2107
+ try {
2108
+ url = new URL(baseUrl);
2109
+ } catch {
2110
+ return void 0;
2111
+ }
2112
+ const names = url.protocol === "https:" ? ["https_proxy", "HTTPS_PROXY"] : url.protocol === "http:" ? ["http_proxy", "HTTP_PROXY"] : [];
2113
+ const proxy = firstSet(env, names);
2114
+ if (proxy === void 0) return void 0;
2115
+ const noProxy = firstSet(env, ["no_proxy", "NO_PROXY"]);
2116
+ return noProxy !== void 0 && matchesNoProxy(url, noProxy) ? void 0 : proxy;
2117
+ }
2118
+ function proxyRejection(proxy) {
2119
+ try {
2120
+ const { protocol } = new URL(proxy);
2121
+ return protocol === "http:" || protocol === "https:" ? void 0 : "only http:// and https:// proxies are supported";
2122
+ } catch {
2123
+ return "not a valid URL";
2124
+ }
2125
+ }
2126
+ function readAuthHeader(env, logger2) {
2127
+ const raw = env[OPENAI_COMPAT_AUTH_HEADER_ENV]?.trim();
2128
+ if (raw === void 0 || raw === "") return void 0;
2129
+ const reason = authHeaderRejection(raw);
2130
+ if (reason !== void 0) {
2131
+ logger2.warn(
2132
+ `${OPENAI_COMPAT_AUTH_HEADER_ENV} ignored (${reason}); the key is sent as a bearer token`,
2133
+ {
2134
+ env: OPENAI_COMPAT_AUTH_HEADER_ENV,
2135
+ reason
2136
+ }
2137
+ );
2138
+ return void 0;
2139
+ }
2140
+ return raw.toLowerCase() === "authorization" ? void 0 : raw;
2141
+ }
2142
+ function readExtraHeaders(env, authHeader, logger2) {
2143
+ const parsed = parseGatewayExtraHeaders(env[OPENAI_COMPAT_EXTRA_HEADERS_ENV]);
2144
+ let reason = parsed.ok ? void 0 : parsed.reason;
2145
+ if (parsed.ok && authHeader !== void 0) {
2146
+ const clash = Object.keys(parsed.headers).some(
2147
+ (name) => name.toLowerCase() === authHeader.toLowerCase()
2148
+ );
2149
+ if (clash) reason = `sets ${OPENAI_COMPAT_AUTH_HEADER_ENV}'s header, which carries the key`;
2150
+ }
2151
+ if (reason !== void 0) {
2152
+ logger2.warn(
2153
+ `${OPENAI_COMPAT_EXTRA_HEADERS_ENV} ignored (${reason}); no extra headers are sent`,
2154
+ {
2155
+ env: OPENAI_COMPAT_EXTRA_HEADERS_ENV,
2156
+ reason
2157
+ }
2158
+ );
2159
+ return void 0;
2160
+ }
2161
+ return parsed.ok && Object.keys(parsed.headers).length > 0 ? parsed.headers : void 0;
2162
+ }
2163
+ function readProxy(baseUrl, env, logger2) {
2164
+ const proxy = gatewayProxyUrl(baseUrl, env);
2165
+ if (proxy === void 0) return void 0;
2166
+ const reason = proxyRejection(proxy);
2167
+ if (reason === void 0) return proxy;
2168
+ const variable = baseUrl.startsWith("https:") ? "HTTPS_PROXY" : "HTTP_PROXY";
2169
+ logger2.warn(`${variable} ignored for the gateway (${reason}); gateway calls go direct`, {
2170
+ env: variable,
2171
+ reason
2172
+ });
2173
+ return void 0;
2174
+ }
2175
+ function readGatewayTransport(baseUrl, env, logger2) {
2176
+ const authHeader = readAuthHeader(env, logger2);
2177
+ const extraHeaders = readExtraHeaders(env, authHeader, logger2);
2178
+ const proxyUrl = readProxy(baseUrl, env, logger2);
2179
+ return {
2180
+ ...authHeader !== void 0 && { authHeader },
2181
+ ...extraHeaders !== void 0 && { extraHeaders },
2182
+ ...proxyUrl !== void 0 && { proxyUrl }
2183
+ };
2184
+ }
2185
+ var proxyAgents = /* @__PURE__ */ new Map();
2186
+ function proxyAgentFor(proxyUrl) {
2187
+ const existing = proxyAgents.get(proxyUrl);
2188
+ const agent = existing ?? new ProxyAgent(proxyUrl);
2189
+ if (existing === void 0) proxyAgents.set(proxyUrl, agent);
2190
+ return agent;
2191
+ }
2192
+ function gatewayClientOptions(transport) {
2193
+ const headers = gatewayHeaders(transport);
2194
+ return {
2195
+ ...transport.apiSurface !== void 0 && { apiSurface: transport.apiSurface },
2196
+ ...Object.keys(headers).length > 0 && { defaultHeaders: headers },
2197
+ ...transport.proxyUrl !== void 0 && {
2198
+ fetchOptions: { dispatcher: proxyAgentFor(transport.proxyUrl) }
2199
+ }
2200
+ };
2201
+ }
2202
+ function gatewayHeaders(transport) {
2203
+ const headers = { ...transport.extraHeaders };
2204
+ if (transport.authHeader !== void 0) {
2205
+ headers["Authorization"] = null;
2206
+ headers[transport.authHeader] = transport.apiKey;
2207
+ }
2208
+ return headers;
2209
+ }
2210
+
2211
+ // src/adapters/openai-compat-adapter.ts
2212
+ function isGatewayModelAdapter(adapter) {
2213
+ const arm = adapter.gatewayArm;
2214
+ return typeof arm === "string" && isEndpointArmId(arm);
2215
+ }
2216
+ function readOpenAICompatEnv() {
2217
+ const fromEnv = readGatewayFromEnv();
2218
+ if (fromEnv !== null) return fromEnv;
2219
+ return readGatewayFromOpencode();
2220
+ }
2221
+ function readGatewayFromEnv() {
2222
+ const envUrl = process.env[OPENAI_COMPAT_URL_ENV]?.trim();
2223
+ const envKey = process.env[OPENAI_COMPAT_KEY_ENV]?.trim();
2224
+ if (envUrl === void 0 || envUrl === "") return null;
2225
+ if (envKey === void 0 || envKey === "") return null;
2226
+ return {
2227
+ baseUrl: envUrl,
2228
+ apiKey: envKey,
2229
+ endpoint: readOpenAICompatEndpoint(),
2230
+ modelAllowlist: readModelAllowlist(),
2231
+ ...readGatewayTransport(envUrl, process.env, endpointLogger)
2232
+ };
2233
+ }
2234
+ function readGatewayFromOpencode() {
2235
+ const opencodePath = process.env["NEXUS_OPENCODE_CONFIG"]?.trim();
2236
+ if (opencodePath === void 0 || opencodePath === "") return null;
2237
+ const fromFile = readOpencodeGateway(opencodePath);
2238
+ if (fromFile === null) return null;
2239
+ return {
2240
+ baseUrl: fromFile.baseURL,
2241
+ apiKey: fromFile.apiKey,
2242
+ endpoint: readOpenAICompatEndpoint(),
2243
+ modelAllowlist: readModelAllowlist(),
2244
+ ...readGatewayTransport(fromFile.baseURL, process.env, endpointLogger)
2245
+ };
2246
+ }
2247
+ var endpointLogger = createLogger({ component: "openai-compat-adapter" });
2248
+ function readOpenAICompatEndpoint(env = process.env, logger2 = endpointLogger) {
2249
+ const raw = env[OPENAI_COMPAT_ENDPOINT_ENV]?.trim();
2250
+ if (raw === void 0 || raw === "") return DEFAULT_OPENAI_COMPAT_ENDPOINT;
2251
+ const reason = gatewayEndpointRejection(raw);
2252
+ if (reason === void 0) return raw;
2253
+ logger2.warn(
2254
+ `${OPENAI_COMPAT_ENDPOINT_ENV} ignored (${reason}); the gateway registers as api:${DEFAULT_OPENAI_COMPAT_ENDPOINT}`,
2255
+ { env: OPENAI_COMPAT_ENDPOINT_ENV, reason, value: "<redacted>" }
2256
+ );
2257
+ return DEFAULT_OPENAI_COMPAT_ENDPOINT;
2258
+ }
2259
+ var MAX_DISCOVERED_MODELS = 256;
2260
+ var MODEL_DISCOVERY_TIMEOUT_MS = 1e4;
2261
+ var MODEL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
2262
+ function keepValidModelIds(models, logger2) {
2263
+ const kept = models.filter((m) => MODEL_ID_PATTERN.test(m.id));
2264
+ const dropped = models.length - kept.length;
2265
+ if (dropped > 0) {
2266
+ logger2?.warn(
2267
+ `Dropped ${String(dropped)} gateway model id(s) that are not a valid model id shape (whitespace, control characters, or over 128 chars)`,
2268
+ { dropped, kept: kept.length }
2269
+ );
2270
+ }
2271
+ return kept;
2272
+ }
2273
+ async function discoverModels(config, logger2) {
2274
+ const catalog = await discoverGatewayCatalog(config, logger2);
2275
+ return catalog.ok ? ok(catalog.value.models) : catalog;
2276
+ }
2277
+ async function discoverGatewayCatalog(config, logger2) {
2278
+ const hostCheck = await checkGatewayHost(config.baseUrl);
2279
+ if (hostCheck.state === "refused_private_host") {
2280
+ return err(new GatewayHostRefusedError(hostCheck));
2281
+ }
2282
+ if (hostCheck.state === "lookup_timed_out") {
2283
+ return err(new ConfigError(`${GATEWAY_HOST_LOOKUP_TIMED_OUT} (host ${hostCheck.host})`));
2284
+ }
2285
+ try {
2286
+ const client = new OpenAI2({
2287
+ baseURL: config.baseUrl,
2288
+ apiKey: config.apiKey,
2289
+ // Discovery runs during server bootstrap, so an unresponsive gateway must
2290
+ // not stall startup indefinitely. The SDK's own default is far longer.
2291
+ timeout: MODEL_DISCOVERY_TIMEOUT_MS,
2292
+ maxRetries: 1,
2293
+ // Auth header, extra headers and proxy (#6608) — the same options every
2294
+ // per-model adapter gets, so discovery cannot pass where completions fail.
2295
+ ...gatewayClientOptions(config)
2296
+ });
2297
+ const list = await client.models.list();
2298
+ const listed = list.data.map((m) => ({ id: m.id, listing: m, model: m }));
2299
+ const allowlist = config.modelAllowlist ?? [];
2300
+ const refined = refineGatewayCatalog(keepValidModelIds(listed, logger2), allowlist, logger2);
2301
+ if (refined.length > MAX_DISCOVERED_MODELS) {
2302
+ return err(overCapError(config, list.data.length, refined.length, allowlist.length > 0));
2303
+ }
2304
+ return ok({
2305
+ listedCount: list.data.length,
2306
+ models: refined.map(({ model: m }) => ({
2307
+ id: m.id,
2308
+ created: m.created,
2309
+ ownedBy: m.owned_by
2310
+ }))
2311
+ });
2312
+ } catch (e) {
2313
+ return err(
2314
+ new ConfigError(
2315
+ `Failed to discover models from ${hostnameOf(config.baseUrl)}: ${redactGatewaySecrets(getErrorMessage(e), { apiKey: config.apiKey, headers: config.extraHeaders })}. Verify ${OPENAI_COMPAT_URL_ENV} and ${OPENAI_COMPAT_KEY_ENV}, then retry.`
2316
+ )
2317
+ );
2318
+ }
2319
+ }
2320
+ function overCapError(config, listed, refined, allowlisted) {
2321
+ const cap = String(MAX_DISCOVERED_MODELS);
2322
+ const host = hostnameOf(config.baseUrl);
2323
+ const detail = allowlisted ? `${OPENAI_COMPAT_MODELS_ENV} matched ${String(refined)} of its ${String(listed)} listed models` : `it listed ${String(listed)} models (${String(refined)} chat models)`;
2324
+ return new ConfigError(
2325
+ `Gateway ${host}: ${detail}, above the ${cap} cap. Refusing to build an adapter per model. Set ${OPENAI_COMPAT_MODELS_ENV} to a comma-separated allowlist of model ids (\`*\` is a wildcard) naming at most ${cap} models.`
2326
+ );
2327
+ }
2328
+ function createOpenAICompatAdapter(modelId, config, created) {
2329
+ const inner = createOpenAICompatClient(modelId, config);
2330
+ const wrapped = withGatewayUsageRecording(
2331
+ inner,
2332
+ `api:${config.endpoint ?? DEFAULT_OPENAI_COMPAT_ENDPOINT}`,
2333
+ config.modelVerified
2334
+ );
2335
+ return created === void 0 ? wrapped : Object.assign(wrapped, { created });
2336
+ }
2337
+ function createOpenAICompatClient(modelId, config) {
2338
+ const baseUrl = validateOpenAICompatBaseUrl(config.baseUrl);
2339
+ return new OpenAIAdapter({
2340
+ modelId,
2341
+ apiKey: config.apiKey,
2342
+ baseUrl,
2343
+ verbatimModelId: true,
2344
+ ...config.omitDefaultTokenCap !== void 0 && {
2345
+ omitDefaultTokenCap: config.omitDefaultTokenCap
2346
+ },
2347
+ ...gatewayClientOptions(config),
2348
+ fetch: guardedGatewayFetch(baseUrl),
2349
+ ...config.logger !== void 0 && { logger: config.logger },
2350
+ ...config.timeout !== void 0 && { timeout: config.timeout },
2351
+ ...config.maxRetries !== void 0 && { maxRetries: config.maxRetries }
2352
+ });
2353
+ }
2354
+ async function buildOpenAICompatAdapters(logger2) {
2355
+ const config = readOpenAICompatEnv();
2356
+ if (config === null) return null;
2357
+ const discovered = await discoverModels(config, logger2);
2358
+ if (!discovered.ok) return discovered;
2359
+ return ok(
2360
+ discovered.value.map(
2361
+ (m) => createOpenAICompatAdapter(m.id, { ...config, modelVerified: true }, m.created)
2362
+ )
2363
+ );
2364
+ }
2365
+
2366
+ export {
2367
+ AdapterModelError,
2368
+ requireApiKey,
2369
+ validateApiKeyPresence,
2370
+ BaseAdapter,
2371
+ StreamError,
2372
+ StreamCancelledError,
2373
+ StreamController,
2374
+ createStream,
2375
+ take,
2376
+ skip,
2377
+ concatStreams,
2378
+ fromArray,
2379
+ tapStream,
2380
+ reduceStream,
2381
+ transformStream,
2382
+ mergeStreams,
2383
+ takeUntil,
2384
+ filterStream,
2385
+ withTimeout,
2386
+ bufferStream,
2387
+ DEFAULT_COLLECT_STREAM_MAX_CHUNKS,
2388
+ collectStream,
2389
+ planOptionalParams,
2390
+ parseGatewayExtraHeaders,
2391
+ gatewayProxyUrl,
2392
+ readGatewayTransport,
2393
+ GATEWAY_HOST_LOOKUP_TIMED_OUT,
2394
+ checkGatewayHost,
2395
+ GatewayHostRefusedError,
2396
+ OPENAI_MODELS,
2397
+ OPENAI_MODEL_ALIASES,
2398
+ OpenAIAdapter,
2399
+ createOpenAIAdapter,
2400
+ withGatewayUsageRecording,
2401
+ readOpencodeGateway,
2402
+ validateOpenAICompatBaseUrl,
2403
+ isGatewayModelAdapter,
2404
+ readOpenAICompatEnv,
2405
+ readOpenAICompatEndpoint,
2406
+ discoverModels,
2407
+ discoverGatewayCatalog,
2408
+ createOpenAICompatAdapter,
2409
+ createOpenAICompatClient,
2410
+ buildOpenAICompatAdapters
2411
+ };
2412
+ //# sourceMappingURL=chunk-KLSNDPU6.js.map