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.
- package/dist/{ast-rule-runner-AAOBENF2.js → ast-rule-runner-RI7ZJ4ZI.js} +3 -3
- package/dist/{child-mcp-config-FRWGQIHP.js → child-mcp-config-73BX4OTR.js} +2 -2
- package/dist/{chunk-NUXSGWDS.js → chunk-23QA6GQD.js} +5 -5
- package/dist/{chunk-EGUISJBB.js → chunk-2DYCHHWW.js} +4 -4
- package/dist/{chunk-TLITBKMZ.js → chunk-2TA6KTRF.js} +15 -15
- package/dist/{chunk-YHY7GMAL.js → chunk-4JJI34RV.js} +2 -2
- package/dist/{chunk-KPGWNBDA.js → chunk-7EV3QAKO.js} +2 -2
- package/dist/{chunk-SCQ5YAYO.js → chunk-D6E3LJGP.js} +4 -4
- package/dist/{chunk-XYYXVJSO.js → chunk-EJ5EBTAV.js} +2 -2
- package/dist/{chunk-BJLP4E6Q.js → chunk-FNHVTDHZ.js} +3 -3
- package/dist/{chunk-VGCRYAEH.js → chunk-FWQSOPBA.js} +4 -4
- package/dist/{chunk-RBFHY2LF.js → chunk-GAX75XCV.js} +2 -2
- package/dist/{chunk-O46IOND3.js → chunk-GX5ZHZR2.js} +7 -7
- package/dist/{chunk-33C22IY4.js → chunk-J44RKRGQ.js} +3 -3
- package/dist/{chunk-L4P4A7Z2.js → chunk-JGY5HMWI.js} +2 -2
- package/dist/{chunk-WCHN7B2Q.js → chunk-JUUGLXSN.js} +8 -10
- package/dist/{chunk-WCHN7B2Q.js.map → chunk-JUUGLXSN.js.map} +1 -1
- package/dist/{chunk-BK6Q3VQK.js → chunk-KASBBKAQ.js} +2 -2
- package/dist/chunk-KASBBKAQ.js.map +1 -0
- package/dist/chunk-KLSNDPU6.js +2412 -0
- package/dist/chunk-KLSNDPU6.js.map +1 -0
- package/dist/{chunk-HQWQ7MR5.js → chunk-LP5ISXKM.js} +4 -4
- package/dist/{chunk-QPU7QKPT.js → chunk-LXYT2P2Y.js} +3 -3
- package/dist/{chunk-C6MM6Z65.js → chunk-OMSKBBL7.js} +45 -27
- package/dist/chunk-OMSKBBL7.js.map +1 -0
- package/dist/{chunk-M4IJ3H6M.js → chunk-PXYSAG5B.js} +2 -2
- package/dist/{chunk-52Q72CPP.js → chunk-QG3VDSWD.js} +3 -3
- package/dist/{chunk-IRFKJ2KR.js → chunk-QM5W3T3O.js} +2 -2
- package/dist/{chunk-O3VPGING.js → chunk-QRC3QLUQ.js} +92 -95
- package/dist/chunk-QRC3QLUQ.js.map +1 -0
- package/dist/{chunk-MCWIMVG2.js → chunk-QS5HGPCN.js} +2 -2
- package/dist/{chunk-O5OUJ3PX.js → chunk-SS53PZO6.js} +5 -5
- package/dist/{chunk-VHOOJ4FG.js → chunk-SX2XWCGA.js} +2 -2
- package/dist/{chunk-2VD5OQQR.js → chunk-TDUXUVUU.js} +2 -2
- package/dist/{chunk-XFTTKOHK.js → chunk-U37AITDA.js} +2 -2
- package/dist/{chunk-V5FGXVSX.js → chunk-UZJ6IPJR.js} +5 -5
- package/dist/{chunk-MYM2TPNL.js → chunk-V26YQQK4.js} +2 -2
- package/dist/{chunk-HVZVVDFN.js → chunk-VLOPXV47.js} +5 -5
- package/dist/{chunk-AMK4BTAV.js → chunk-WD4DENPC.js} +3 -3
- package/dist/{chunk-WI73OZAU.js → chunk-WKFPKPEM.js} +13 -13
- package/dist/chunk-WKFPKPEM.js.map +1 -0
- package/dist/{chunk-IYEZVAJT.js → chunk-XDUTAUK3.js} +40 -42
- package/dist/{chunk-IYEZVAJT.js.map → chunk-XDUTAUK3.js.map} +1 -1
- package/dist/{cli-circuit-breaker-AM425BRL.js → cli-circuit-breaker-62YOQQT3.js} +3 -3
- package/dist/{cli-readiness-LWOMHZ34.js → cli-readiness-O6EOQV4L.js} +7 -7
- package/dist/cli.js +61 -56
- package/dist/cli.js.map +1 -1
- package/dist/{composite-router-VXQFXNET.js → composite-router-YPZ3D5Y6.js} +2 -2
- package/dist/{consensus-vote-V2JQHILD.js → consensus-vote-RNQQUBXO.js} +16 -17
- package/dist/{context-retriever-GEQIPCVL.js → context-retriever-HVPLM6DH.js} +8 -8
- package/dist/{doctor-deep-DSOZGB5J.js → doctor-deep-UZQTD65D.js} +3 -3
- package/dist/{doctor-gateway-report-RJOSOHGA.js → doctor-gateway-report-LRBPPJD4.js} +6 -7
- package/dist/{doctor-live-7LVRGOS5.js → doctor-live-QFLT5SDB.js} +11 -11
- package/dist/{expert-bridge-5A4ADUBO.js → expert-bridge-O3UZXLGB.js} +4 -4
- package/dist/{factory-UBAUIA2M.js → factory-KCKQQAEH.js} +10 -10
- package/dist/{factory-4RZLP7QV.js → factory-LUKUIB6C.js} +5 -5
- package/dist/{improvement-review-UIYSAI36.js → improvement-review-7IABFAU3.js} +6 -6
- package/dist/index.d.ts +20 -22
- package/dist/index.js +35 -37
- package/dist/index.js.map +1 -1
- package/dist/{init-opencode-WUN3CTBM.js → init-opencode-RYPBCM4P.js} +5 -6
- package/dist/{init-opencode-WUN3CTBM.js.map → init-opencode-RYPBCM4P.js.map} +1 -1
- package/dist/{issue-triage-VKEHVHO4.js → issue-triage-YBVJVAUU.js} +6 -6
- package/dist/{model-config-helpers-GYPIIXOP.js → model-config-helpers-QNV3KD2W.js} +2 -2
- package/dist/{openai-compat-adapter-GSDOJ7TQ.js → openai-compat-adapter-GPZDIR3D.js} +11 -8
- package/dist/{outcome-store-adapter-PXT7AL6T.js → outcome-store-adapter-WRONSTUD.js} +2 -2
- package/dist/{pr-reviewer-helpers-GUUT7G43.js → pr-reviewer-helpers-7SMNNDFJ.js} +4 -4
- package/dist/{registry-command-4G3WGKS5.js → registry-command-U7K6HIBK.js} +2 -2
- package/dist/{repo-security-plan-QO6ACTPZ.js → repo-security-plan-LFWLVEL2.js} +3 -3
- package/dist/{research-helpers-synthesize-MPEJHPYR.js → research-helpers-synthesize-X6MVAAKT.js} +4 -4
- package/dist/{routing-memory-VHQDJ6XZ.js → routing-memory-OCIVXKOU.js} +2 -2
- package/dist/{session-memory-M3CQFFL6.js → session-memory-4LMEWO3X.js} +3 -3
- package/dist/{setup-config-VGEBCEKX.js → setup-config-YNNNR4AE.js} +3 -3
- package/dist/{setup-custom-api-DX7CNYKT.js → setup-custom-api-QEPUCHOX.js} +3 -3
- package/dist/setup-data-dir-XM5DWH7R.js +35 -0
- package/dist/{tool-memory-7QUPGCWV.js → tool-memory-EFGFXF65.js} +5 -5
- package/dist/{unified-registry-CLZK3H64.js → unified-registry-KLNC24HA.js} +11 -11
- package/dist/{weather-report-OKMXF73V.js → weather-report-PMYQ3WBW.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-BK6Q3VQK.js.map +0 -1
- package/dist/chunk-C6MM6Z65.js.map +0 -1
- package/dist/chunk-K6M2ZWX5.js +0 -1116
- package/dist/chunk-K6M2ZWX5.js.map +0 -1
- package/dist/chunk-O3VPGING.js.map +0 -1
- package/dist/chunk-WI73OZAU.js.map +0 -1
- package/dist/chunk-XQMQGUJT.js +0 -995
- package/dist/chunk-XQMQGUJT.js.map +0 -1
- package/dist/setup-data-dir-75Z5PSSB.js +0 -36
- /package/dist/{ast-rule-runner-AAOBENF2.js.map → ast-rule-runner-RI7ZJ4ZI.js.map} +0 -0
- /package/dist/{child-mcp-config-FRWGQIHP.js.map → child-mcp-config-73BX4OTR.js.map} +0 -0
- /package/dist/{chunk-NUXSGWDS.js.map → chunk-23QA6GQD.js.map} +0 -0
- /package/dist/{chunk-EGUISJBB.js.map → chunk-2DYCHHWW.js.map} +0 -0
- /package/dist/{chunk-TLITBKMZ.js.map → chunk-2TA6KTRF.js.map} +0 -0
- /package/dist/{chunk-YHY7GMAL.js.map → chunk-4JJI34RV.js.map} +0 -0
- /package/dist/{chunk-KPGWNBDA.js.map → chunk-7EV3QAKO.js.map} +0 -0
- /package/dist/{chunk-SCQ5YAYO.js.map → chunk-D6E3LJGP.js.map} +0 -0
- /package/dist/{chunk-XYYXVJSO.js.map → chunk-EJ5EBTAV.js.map} +0 -0
- /package/dist/{chunk-BJLP4E6Q.js.map → chunk-FNHVTDHZ.js.map} +0 -0
- /package/dist/{chunk-VGCRYAEH.js.map → chunk-FWQSOPBA.js.map} +0 -0
- /package/dist/{chunk-RBFHY2LF.js.map → chunk-GAX75XCV.js.map} +0 -0
- /package/dist/{chunk-O46IOND3.js.map → chunk-GX5ZHZR2.js.map} +0 -0
- /package/dist/{chunk-33C22IY4.js.map → chunk-J44RKRGQ.js.map} +0 -0
- /package/dist/{chunk-L4P4A7Z2.js.map → chunk-JGY5HMWI.js.map} +0 -0
- /package/dist/{chunk-HQWQ7MR5.js.map → chunk-LP5ISXKM.js.map} +0 -0
- /package/dist/{chunk-QPU7QKPT.js.map → chunk-LXYT2P2Y.js.map} +0 -0
- /package/dist/{chunk-M4IJ3H6M.js.map → chunk-PXYSAG5B.js.map} +0 -0
- /package/dist/{chunk-52Q72CPP.js.map → chunk-QG3VDSWD.js.map} +0 -0
- /package/dist/{chunk-IRFKJ2KR.js.map → chunk-QM5W3T3O.js.map} +0 -0
- /package/dist/{chunk-MCWIMVG2.js.map → chunk-QS5HGPCN.js.map} +0 -0
- /package/dist/{chunk-O5OUJ3PX.js.map → chunk-SS53PZO6.js.map} +0 -0
- /package/dist/{chunk-VHOOJ4FG.js.map → chunk-SX2XWCGA.js.map} +0 -0
- /package/dist/{chunk-2VD5OQQR.js.map → chunk-TDUXUVUU.js.map} +0 -0
- /package/dist/{chunk-XFTTKOHK.js.map → chunk-U37AITDA.js.map} +0 -0
- /package/dist/{chunk-V5FGXVSX.js.map → chunk-UZJ6IPJR.js.map} +0 -0
- /package/dist/{chunk-MYM2TPNL.js.map → chunk-V26YQQK4.js.map} +0 -0
- /package/dist/{chunk-HVZVVDFN.js.map → chunk-VLOPXV47.js.map} +0 -0
- /package/dist/{chunk-AMK4BTAV.js.map → chunk-WD4DENPC.js.map} +0 -0
- /package/dist/{cli-circuit-breaker-AM425BRL.js.map → cli-circuit-breaker-62YOQQT3.js.map} +0 -0
- /package/dist/{cli-readiness-LWOMHZ34.js.map → cli-readiness-O6EOQV4L.js.map} +0 -0
- /package/dist/{composite-router-VXQFXNET.js.map → composite-router-YPZ3D5Y6.js.map} +0 -0
- /package/dist/{consensus-vote-V2JQHILD.js.map → consensus-vote-RNQQUBXO.js.map} +0 -0
- /package/dist/{context-retriever-GEQIPCVL.js.map → context-retriever-HVPLM6DH.js.map} +0 -0
- /package/dist/{doctor-deep-DSOZGB5J.js.map → doctor-deep-UZQTD65D.js.map} +0 -0
- /package/dist/{doctor-gateway-report-RJOSOHGA.js.map → doctor-gateway-report-LRBPPJD4.js.map} +0 -0
- /package/dist/{doctor-live-7LVRGOS5.js.map → doctor-live-QFLT5SDB.js.map} +0 -0
- /package/dist/{expert-bridge-5A4ADUBO.js.map → expert-bridge-O3UZXLGB.js.map} +0 -0
- /package/dist/{factory-4RZLP7QV.js.map → factory-KCKQQAEH.js.map} +0 -0
- /package/dist/{factory-UBAUIA2M.js.map → factory-LUKUIB6C.js.map} +0 -0
- /package/dist/{improvement-review-UIYSAI36.js.map → improvement-review-7IABFAU3.js.map} +0 -0
- /package/dist/{issue-triage-VKEHVHO4.js.map → issue-triage-YBVJVAUU.js.map} +0 -0
- /package/dist/{model-config-helpers-GYPIIXOP.js.map → model-config-helpers-QNV3KD2W.js.map} +0 -0
- /package/dist/{openai-compat-adapter-GSDOJ7TQ.js.map → openai-compat-adapter-GPZDIR3D.js.map} +0 -0
- /package/dist/{outcome-store-adapter-PXT7AL6T.js.map → outcome-store-adapter-WRONSTUD.js.map} +0 -0
- /package/dist/{pr-reviewer-helpers-GUUT7G43.js.map → pr-reviewer-helpers-7SMNNDFJ.js.map} +0 -0
- /package/dist/{registry-command-4G3WGKS5.js.map → registry-command-U7K6HIBK.js.map} +0 -0
- /package/dist/{repo-security-plan-QO6ACTPZ.js.map → repo-security-plan-LFWLVEL2.js.map} +0 -0
- /package/dist/{research-helpers-synthesize-MPEJHPYR.js.map → research-helpers-synthesize-X6MVAAKT.js.map} +0 -0
- /package/dist/{routing-memory-VHQDJ6XZ.js.map → routing-memory-OCIVXKOU.js.map} +0 -0
- /package/dist/{session-memory-M3CQFFL6.js.map → session-memory-4LMEWO3X.js.map} +0 -0
- /package/dist/{setup-config-VGEBCEKX.js.map → setup-config-YNNNR4AE.js.map} +0 -0
- /package/dist/{setup-custom-api-DX7CNYKT.js.map → setup-custom-api-QEPUCHOX.js.map} +0 -0
- /package/dist/{setup-data-dir-75Z5PSSB.js.map → setup-data-dir-XM5DWH7R.js.map} +0 -0
- /package/dist/{tool-memory-7QUPGCWV.js.map → tool-memory-EFGFXF65.js.map} +0 -0
- /package/dist/{unified-registry-CLZK3H64.js.map → unified-registry-KLNC24HA.js.map} +0 -0
- /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
|