@remnic/plugin-openclaw 1.0.54 → 9.3.515
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/index.js +510 -2106
- package/openclaw.plugin.json +3 -3
- package/package.json +4 -3
- package/scripts/faiss_index.py +141 -29
- package/dist/calibration-RKL2LRW4.js +0 -250
- package/dist/capsule-cli-EHZPMXBC.js +0 -33
- package/dist/capsule-crypto-JS67OSWM.js +0 -17
- package/dist/capsule-export-DX53CPIT.js +0 -17
- package/dist/capsule-import-4OXCPHOT.js +0 -16
- package/dist/capsule-merge-25AUN33Q.js +0 -195
- package/dist/causal-chain-BVTOWZKC.js +0 -23
- package/dist/causal-consolidation-IAIX53NV.js +0 -233
- package/dist/causal-retrieval-XAP6QKHZ.js +0 -182
- package/dist/causal-trajectory-graph-ZWQWZ7N5.js +0 -59
- package/dist/chunk-25J4PXDH.js +0 -30
- package/dist/chunk-2RQU6GVD.js +0 -453
- package/dist/chunk-3A5ELHTT.js +0 -61
- package/dist/chunk-3G7FAF6S.js +0 -60
- package/dist/chunk-3IKMUNW5.js +0 -177
- package/dist/chunk-4XDQ3KEC.js +0 -4048
- package/dist/chunk-5ZW5XJQ6.js +0 -128
- package/dist/chunk-6IWEAUN6.js +0 -148
- package/dist/chunk-7JOLBJJ5.js +0 -874
- package/dist/chunk-AJECKGY2.js +0 -896
- package/dist/chunk-B52XADV3.js +0 -244
- package/dist/chunk-BQZ7JACP.js +0 -389
- package/dist/chunk-BU5KJVWF.js +0 -78
- package/dist/chunk-BZ4EYURA.js +0 -181
- package/dist/chunk-CEL5ZLKP.js +0 -233
- package/dist/chunk-EH4AXGRO.js +0 -48
- package/dist/chunk-FDK3VJVE.js +0 -1295
- package/dist/chunk-I2KLQ2HA.js +0 -22
- package/dist/chunk-IO5WWY6A.js +0 -156
- package/dist/chunk-JC3FCKYL.js +0 -516
- package/dist/chunk-JZBOXOUC.js +0 -259
- package/dist/chunk-KC7KSQR4.js +0 -302
- package/dist/chunk-LZCGPRHS.js +0 -228
- package/dist/chunk-MBIFE6SA.js +0 -250
- package/dist/chunk-MXFJXUHC.js +0 -292
- package/dist/chunk-NUWDSTP7.js +0 -233
- package/dist/chunk-QCCP4RU5.js +0 -190
- package/dist/chunk-QMUQV5NP.js +0 -146
- package/dist/chunk-QQXJODFL.js +0 -328
- package/dist/chunk-SWOYEQN2.js +0 -1005
- package/dist/chunk-TH5FF5SC.js +0 -16
- package/dist/chunk-TXOEHSVP.js +0 -275
- package/dist/chunk-UFU5GGGA.js +0 -47
- package/dist/chunk-USSDZQKJ.js +0 -6533
- package/dist/chunk-UZJ7EERS.js +0 -272
- package/dist/chunk-VUZEEGP5.js +0 -725
- package/dist/chunk-W6EEFUCJ.js +0 -349
- package/dist/chunk-YGGGUTG3.js +0 -125
- package/dist/chunk-YJYZMLD5.js +0 -360
- package/dist/chunk-YKV4EFUI.js +0 -199
- package/dist/chunk-ZS6VABML.js +0 -185
- package/dist/cipher-E23BHBSO.js +0 -27
- package/dist/consolidation-undo-FKJZCJHS.js +0 -426
- package/dist/contradiction-review-WJRWNQ5N.js +0 -29
- package/dist/contradiction-scan-5X423QGT.js +0 -12
- package/dist/dreams-ledger-KDX44I7R.js +0 -290
- package/dist/engine-O46ZX4EZ.js +0 -16
- package/dist/extraction-judge-telemetry-O4ZVGLTU.js +0 -14
- package/dist/fallback-llm-43UMEXNJ.js +0 -10
- package/dist/first-start-migration-H2SAXAGR.js +0 -263
- package/dist/forget-ZECIDNL5.js +0 -68
- package/dist/fs-utils-OYXSZSVV.js +0 -39
- package/dist/graph-edge-decay-24ZKD5QL.js +0 -202
- package/dist/kdf-RXKIWHRU.js +0 -25
- package/dist/logger-XG7JKLPS.js +0 -9
- package/dist/memory-governance-6BCH6SZ5.js +0 -25
- package/dist/metadata-WK2TRPYZ.js +0 -20
- package/dist/migrate-from-identity-anchor-SNDNKHZD.js +0 -7
- package/dist/path-ZKO74XXC.js +0 -7
- package/dist/peers-W53WSDXG.js +0 -43
- package/dist/purge-IKJISXEQ.js +0 -204
- package/dist/resolution-BN35OXDS.js +0 -11
- package/dist/secure-store-6RWYM6GK.js +0 -155
- package/dist/state-4ITLYMAU.js +0 -7
- package/dist/state-store-ET3ADVY5.js +0 -14
- package/dist/storage-VEGU76NN.js +0 -27
- package/dist/tier-stats-ZRQBV6G2.js +0 -147
- package/dist/trace-IL2Y34EH.js +0 -289
- package/dist/tui-7KRDCMYK.js +0 -12
- package/dist/types-MBUINTB2.js +0 -30
package/dist/chunk-SWOYEQN2.js
DELETED
|
@@ -1,1005 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
extractJsonCandidates
|
|
3
|
-
} from "./chunk-3A5ELHTT.js";
|
|
4
|
-
import {
|
|
5
|
-
log
|
|
6
|
-
} from "./chunk-UFU5GGGA.js";
|
|
7
|
-
import {
|
|
8
|
-
expandTildePath,
|
|
9
|
-
readEnvVar,
|
|
10
|
-
resolveHomeDir
|
|
11
|
-
} from "./chunk-EH4AXGRO.js";
|
|
12
|
-
|
|
13
|
-
// ../remnic-core/src/fallback-llm.ts
|
|
14
|
-
import path2 from "path";
|
|
15
|
-
|
|
16
|
-
// ../remnic-core/src/openai-chat-compat.ts
|
|
17
|
-
function normalizedModel(model) {
|
|
18
|
-
return model.trim().toLowerCase();
|
|
19
|
-
}
|
|
20
|
-
function matchesModelFamily(normalized, familyPattern) {
|
|
21
|
-
return familyPattern.test(normalized);
|
|
22
|
-
}
|
|
23
|
-
function shouldAssumeOpenAiChatCompletions(baseUrl) {
|
|
24
|
-
if (!baseUrl) return true;
|
|
25
|
-
try {
|
|
26
|
-
return new URL(baseUrl).hostname.toLowerCase() === "api.openai.com";
|
|
27
|
-
} catch {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
function usesMaxCompletionTokens(model, options) {
|
|
32
|
-
const normalized = normalizedModel(model);
|
|
33
|
-
if (options?.assumeOpenAI !== true) return false;
|
|
34
|
-
if (matchesModelFamily(normalized, /^gpt-5(?:$|[-.])/)) return true;
|
|
35
|
-
if (matchesModelFamily(normalized, /^gpt-4o(?:$|[-.])/)) return true;
|
|
36
|
-
if (matchesModelFamily(normalized, /^gpt-4\.1(?:$|[-.])/)) return true;
|
|
37
|
-
if (matchesModelFamily(normalized, /^o1(?:$|[-.])/)) return true;
|
|
38
|
-
if (matchesModelFamily(normalized, /^o3(?:$|[-.])/)) return true;
|
|
39
|
-
return matchesModelFamily(normalized, /^o4-mini(?:$|[-.])/);
|
|
40
|
-
}
|
|
41
|
-
function buildChatCompletionTokenLimit(model, maxTokens, options) {
|
|
42
|
-
const safeMaxTokens = Math.max(0, Math.floor(maxTokens));
|
|
43
|
-
if (usesMaxCompletionTokens(model, options)) {
|
|
44
|
-
return { max_completion_tokens: safeMaxTokens };
|
|
45
|
-
}
|
|
46
|
-
return { max_tokens: safeMaxTokens };
|
|
47
|
-
}
|
|
48
|
-
function supportsTemperature(model, options) {
|
|
49
|
-
const normalized = normalizedModel(model);
|
|
50
|
-
if (options?.assumeOpenAI === true && matchesModelFamily(normalized, /^gpt-5(?:$|[-.])/)) {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
function buildChatCompletionTemperature(model, temperature, options) {
|
|
56
|
-
if (!supportsTemperature(model, options)) return {};
|
|
57
|
-
return { temperature };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ../remnic-core/src/resolve-provider-secret.ts
|
|
61
|
-
import path from "path";
|
|
62
|
-
import os from "os";
|
|
63
|
-
var _resolveCredentialForProvider = null;
|
|
64
|
-
var _getRuntimeAuthForModel = null;
|
|
65
|
-
var _resolverLoaded = false;
|
|
66
|
-
var _resolverNextRetryAt = 0;
|
|
67
|
-
var RESOLVER_RETRY_BACKOFF_MS = 6e4;
|
|
68
|
-
var resolvedCache = /* @__PURE__ */ new Map();
|
|
69
|
-
var cacheObjectIds = /* @__PURE__ */ new WeakMap();
|
|
70
|
-
var nextCacheObjectId = 1;
|
|
71
|
-
var NON_LITERAL_AUTH_MARKERS = /* @__PURE__ */ new Set([
|
|
72
|
-
"secretref-managed",
|
|
73
|
-
"lm-studio"
|
|
74
|
-
]);
|
|
75
|
-
var ENV_VAR_MARKER_RE = /^[A-Z][A-Z0-9_]*(?:_API_KEY|_ACCESS_TOKEN|_TOKEN|_SECRET|_CREDENTIALS|_CREDENTIALS_JSON)$/;
|
|
76
|
-
async function getGatewayResolver() {
|
|
77
|
-
if (_resolverLoaded) {
|
|
78
|
-
return _resolveCredentialForProvider;
|
|
79
|
-
}
|
|
80
|
-
if (_resolverNextRetryAt > 0 && Date.now() < _resolverNextRetryAt) {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
try {
|
|
84
|
-
const candidates = [
|
|
85
|
-
// Try glob-matching the runtime module name (hash varies per build)
|
|
86
|
-
...await findRuntimeModules()
|
|
87
|
-
];
|
|
88
|
-
const { pathToFileURL } = await import("url");
|
|
89
|
-
for (const candidate of candidates) {
|
|
90
|
-
try {
|
|
91
|
-
const importUrl = pathToFileURL(candidate).href;
|
|
92
|
-
const mod = await import(importUrl);
|
|
93
|
-
if (typeof mod.resolveApiKeyForProvider === "function") {
|
|
94
|
-
_resolveCredentialForProvider = mod.resolveApiKeyForProvider;
|
|
95
|
-
if (typeof mod.getRuntimeAuthForModel === "function") {
|
|
96
|
-
_getRuntimeAuthForModel = mod.getRuntimeAuthForModel;
|
|
97
|
-
log.debug("loaded gateway getRuntimeAuthForModel from runtime module");
|
|
98
|
-
}
|
|
99
|
-
_resolverLoaded = true;
|
|
100
|
-
log.debug("loaded gateway resolveApiKeyForProvider from runtime module");
|
|
101
|
-
return _resolveCredentialForProvider;
|
|
102
|
-
}
|
|
103
|
-
} catch {
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
} catch {
|
|
107
|
-
}
|
|
108
|
-
_resolverNextRetryAt = Date.now() + RESOLVER_RETRY_BACKOFF_MS;
|
|
109
|
-
log.debug(`gateway resolveApiKeyForProvider not available \u2014 will retry after ${RESOLVER_RETRY_BACKOFF_MS / 1e3}s`);
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
async function findRuntimeModules() {
|
|
113
|
-
return findGatewayRuntimeModules("runtime-model-auth.runtime-");
|
|
114
|
-
}
|
|
115
|
-
async function findGatewayRuntimeModules(filePrefix) {
|
|
116
|
-
const { accessSync, constants, readdirSync, realpathSync, statSync } = await import("fs");
|
|
117
|
-
const { createRequire: createRequire2 } = await import("module");
|
|
118
|
-
const candidates = [];
|
|
119
|
-
const distDirs = [];
|
|
120
|
-
const pushDistDirs = (entryPath) => {
|
|
121
|
-
const resolvedEntryDir = path.dirname(entryPath);
|
|
122
|
-
const packageRoot = path.basename(resolvedEntryDir) === "dist" ? path.resolve(resolvedEntryDir, "..") : resolvedEntryDir;
|
|
123
|
-
const candidateDistDirs = [
|
|
124
|
-
path.join(packageRoot, "dist"),
|
|
125
|
-
path.join(packageRoot, "..", "dist")
|
|
126
|
-
];
|
|
127
|
-
for (const candidate of candidateDistDirs) {
|
|
128
|
-
const resolved = path.resolve(candidate);
|
|
129
|
-
if (!distDirs.includes(resolved)) distDirs.push(resolved);
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
try {
|
|
133
|
-
const req = createRequire2(import.meta.url);
|
|
134
|
-
const openclawMain = req.resolve("openclaw");
|
|
135
|
-
pushDistDirs(openclawMain);
|
|
136
|
-
} catch {
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
const mainScript = process.argv[1];
|
|
140
|
-
if (mainScript) {
|
|
141
|
-
const realScript = realpathSync(mainScript);
|
|
142
|
-
if (realScript.includes("openclaw")) {
|
|
143
|
-
pushDistDirs(realScript);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
} catch {
|
|
147
|
-
}
|
|
148
|
-
try {
|
|
149
|
-
const openclawBin = findExecutableOnPath("openclaw", accessSync, statSync, constants.X_OK);
|
|
150
|
-
if (openclawBin) {
|
|
151
|
-
pushDistDirs(realpathSync(openclawBin));
|
|
152
|
-
}
|
|
153
|
-
} catch {
|
|
154
|
-
}
|
|
155
|
-
for (const dir of distDirs) {
|
|
156
|
-
try {
|
|
157
|
-
const files = readdirSync(dir);
|
|
158
|
-
for (const f of files) {
|
|
159
|
-
if (f.startsWith(filePrefix) && f.endsWith(".js")) {
|
|
160
|
-
candidates.push(path.join(dir, f));
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
} catch {
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return candidates;
|
|
167
|
-
}
|
|
168
|
-
function findExecutableOnPath(executableName, access, stat, executableMode) {
|
|
169
|
-
const pathEnv = readEnvVar("PATH");
|
|
170
|
-
if (!pathEnv) return void 0;
|
|
171
|
-
const pathExts = process.platform === "win32" ? (readEnvVar("PATHEXT") ?? ".EXE;.CMD;.BAT;.COM").split(";").filter((ext) => ext.length > 0) : [""];
|
|
172
|
-
const hasExtension = path.extname(executableName).length > 0;
|
|
173
|
-
for (const dir of pathEnv.split(path.delimiter)) {
|
|
174
|
-
if (!dir) continue;
|
|
175
|
-
const candidateNames = process.platform === "win32" && !hasExtension ? pathExts.map((ext) => `${executableName}${ext}`) : [executableName];
|
|
176
|
-
for (const candidateName of candidateNames) {
|
|
177
|
-
const candidate = path.join(dir, candidateName);
|
|
178
|
-
try {
|
|
179
|
-
access(candidate, executableMode);
|
|
180
|
-
if (!stat(candidate).isFile()) continue;
|
|
181
|
-
return candidate;
|
|
182
|
-
} catch {
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return void 0;
|
|
187
|
-
}
|
|
188
|
-
function isNonLiteralAuthMarker(value) {
|
|
189
|
-
return NON_LITERAL_AUTH_MARKERS.has(value) || value.endsWith("-oauth") || value.endsWith("-local") || value.startsWith("gcp-") || ENV_VAR_MARKER_RE.test(value);
|
|
190
|
-
}
|
|
191
|
-
function resolveFromNamedEnvVar(marker) {
|
|
192
|
-
if (!ENV_VAR_MARKER_RE.test(marker)) return void 0;
|
|
193
|
-
const value = readEnvVar(marker);
|
|
194
|
-
return value && value.trim().length > 0 ? value.trim() : void 0;
|
|
195
|
-
}
|
|
196
|
-
function cacheIdentity(value) {
|
|
197
|
-
if (value === null) return "null";
|
|
198
|
-
if (value === void 0) return "undefined";
|
|
199
|
-
if (typeof value === "string") return `string:${value}`;
|
|
200
|
-
if (typeof value === "number") return `number:${String(value)}`;
|
|
201
|
-
if (typeof value === "boolean") return `boolean:${String(value)}`;
|
|
202
|
-
if (typeof value === "bigint") return `bigint:${String(value)}`;
|
|
203
|
-
if (typeof value === "symbol" || typeof value === "function") return typeof value;
|
|
204
|
-
if (typeof value === "object") {
|
|
205
|
-
const existingId = cacheObjectIds.get(value);
|
|
206
|
-
if (existingId !== void 0) return `object:${existingId}`;
|
|
207
|
-
const newId = nextCacheObjectId++;
|
|
208
|
-
cacheObjectIds.set(value, newId);
|
|
209
|
-
return `object:${newId}`;
|
|
210
|
-
}
|
|
211
|
-
return String(value);
|
|
212
|
-
}
|
|
213
|
-
function providerSecretCacheKey(providerId, resolvedAgentDir, credentialValue, gatewayConfig) {
|
|
214
|
-
return [
|
|
215
|
-
`provider:${providerId}`,
|
|
216
|
-
`agentDir:${resolvedAgentDir}`,
|
|
217
|
-
`apiKey:${cacheIdentity(credentialValue)}`,
|
|
218
|
-
`cfg:${cacheIdentity(gatewayConfig)}`
|
|
219
|
-
].join(":");
|
|
220
|
-
}
|
|
221
|
-
async function resolveProviderCredential(providerId, credentialValue, gatewayConfig, agentDir) {
|
|
222
|
-
const resolvedAgentDir = path.resolve(
|
|
223
|
-
agentDir ?? path.join(os.homedir(), ".openclaw", "agents", "main", "agent")
|
|
224
|
-
);
|
|
225
|
-
let resolved;
|
|
226
|
-
if (typeof credentialValue === "string" && credentialValue.trim().length > 0) {
|
|
227
|
-
const trimmedCredentialValue = credentialValue.trim();
|
|
228
|
-
if (isNonLiteralAuthMarker(trimmedCredentialValue)) {
|
|
229
|
-
const markerEnvValue = resolveFromNamedEnvVar(trimmedCredentialValue);
|
|
230
|
-
if (markerEnvValue) {
|
|
231
|
-
return markerEnvValue;
|
|
232
|
-
}
|
|
233
|
-
} else {
|
|
234
|
-
return trimmedCredentialValue;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
const cacheKey = providerSecretCacheKey(providerId, resolvedAgentDir, credentialValue, gatewayConfig);
|
|
238
|
-
if (resolvedCache.has(cacheKey)) {
|
|
239
|
-
return resolvedCache.get(cacheKey);
|
|
240
|
-
}
|
|
241
|
-
const resolver = await getGatewayResolver();
|
|
242
|
-
if (resolver) {
|
|
243
|
-
try {
|
|
244
|
-
const auth = await resolver({ provider: providerId, cfg: gatewayConfig, agentDir: resolvedAgentDir });
|
|
245
|
-
if (auth?.["api"+"Key"]) {
|
|
246
|
-
resolved = auth["api"+"Key"];
|
|
247
|
-
log.debug(`resolved API key for provider "${providerId}" via gateway auth (source: ${auth.source ?? "unknown"}, mode: ${auth.mode ?? "unknown"})`);
|
|
248
|
-
resolvedCache.set(cacheKey, resolved);
|
|
249
|
-
return resolved;
|
|
250
|
-
}
|
|
251
|
-
} catch (err) {
|
|
252
|
-
log.debug(
|
|
253
|
-
`gateway auth resolution failed for provider "${providerId}": ${err instanceof Error ? err.message : String(err)}`
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
resolved = resolveFromEnv(providerId);
|
|
258
|
-
if (resolved) {
|
|
259
|
-
log.debug(`resolved API key for provider "${providerId}" from environment variable`);
|
|
260
|
-
} else {
|
|
261
|
-
log.debug(`could not resolve API key for provider "${providerId}" \u2014 skipping`);
|
|
262
|
-
}
|
|
263
|
-
if (resolved) {
|
|
264
|
-
resolvedCache.set(cacheKey, resolved);
|
|
265
|
-
}
|
|
266
|
-
return resolved;
|
|
267
|
-
}
|
|
268
|
-
function resolveFromEnv(providerId) {
|
|
269
|
-
const normalized = providerId.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
270
|
-
const candidates = [
|
|
271
|
-
`${normalized}_API_KEY`,
|
|
272
|
-
`${normalized}_TOKEN`
|
|
273
|
-
];
|
|
274
|
-
for (const envVar of candidates) {
|
|
275
|
-
const value = readEnvVar(envVar);
|
|
276
|
-
if (value && value.trim().length > 0) {
|
|
277
|
-
return value.trim();
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
return void 0;
|
|
281
|
-
}
|
|
282
|
-
async function getGatewayRuntimeAuthForModel() {
|
|
283
|
-
await getGatewayResolver();
|
|
284
|
-
return _getRuntimeAuthForModel;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// ../remnic-core/src/models-json.ts
|
|
288
|
-
import { join } from "path";
|
|
289
|
-
import { homedir } from "os";
|
|
290
|
-
import { createRequire } from "module";
|
|
291
|
-
var _cachedProviders = null;
|
|
292
|
-
var _loadAttempted = false;
|
|
293
|
-
var requireNode = createRequire(import.meta.url);
|
|
294
|
-
var READ_FILE_SYNC_FIELD = ["read", "File", "Sync"].join("");
|
|
295
|
-
function loadModelsJsonProviders() {
|
|
296
|
-
if (_loadAttempted) {
|
|
297
|
-
return _cachedProviders ?? {};
|
|
298
|
-
}
|
|
299
|
-
_loadAttempted = true;
|
|
300
|
-
try {
|
|
301
|
-
const modelsPath = join(homedir(), ".openclaw", "agents", "main", "agent", "models.json");
|
|
302
|
-
const fs = requireNode(["node", "fs"].join(":"));
|
|
303
|
-
const reader = fs[READ_FILE_SYNC_FIELD];
|
|
304
|
-
const raw = reader(modelsPath, "utf-8");
|
|
305
|
-
const parsed = JSON.parse(raw);
|
|
306
|
-
const providers = parsed?.providers;
|
|
307
|
-
if (providers && typeof providers === "object" && !Array.isArray(providers)) {
|
|
308
|
-
_cachedProviders = providers;
|
|
309
|
-
log.debug(`loaded ${Object.keys(_cachedProviders).length} providers from models.json`);
|
|
310
|
-
return _cachedProviders;
|
|
311
|
-
}
|
|
312
|
-
} catch (err) {
|
|
313
|
-
log.debug(
|
|
314
|
-
`could not load models.json: ${err instanceof Error ? err.message : String(err)}`
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
return {};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// ../remnic-core/src/codex-cli-fallback.ts
|
|
321
|
-
var VALID_CODEX_CLI_REASONING_EFFORTS = /* @__PURE__ */ new Set([
|
|
322
|
-
"low",
|
|
323
|
-
"medium",
|
|
324
|
-
"high",
|
|
325
|
-
"xhigh"
|
|
326
|
-
]);
|
|
327
|
-
var processRunner;
|
|
328
|
-
async function callCodexCliFallback(config, modelId, messages, options = {}) {
|
|
329
|
-
if (!processRunner) {
|
|
330
|
-
throw new Error(
|
|
331
|
-
'codex-cli fallback transport is not registered; install a runner with setCodexCliFallbackRunnerForProcess() before using api: "codex-cli"'
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
return await processRunner({
|
|
335
|
-
config: normalizeCodexCliFallbackConfig(config),
|
|
336
|
-
modelId: normalizeCodexCliModel(modelId),
|
|
337
|
-
messages,
|
|
338
|
-
options: normalizeCodexCliFallbackOptions(options)
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
function normalizeCodexCliFallbackConfig(config) {
|
|
342
|
-
return {
|
|
343
|
-
...config,
|
|
344
|
-
...config.executable !== void 0 ? { executable: normalizeOptionalString(config.executable, "codex-cli executable") } : {},
|
|
345
|
-
...config.codexCliExecutable !== void 0 ? { codexCliExecutable: normalizeOptionalString(config.codexCliExecutable, "codex-cli executable") } : {},
|
|
346
|
-
...config.reasoningEffort !== void 0 ? { reasoningEffort: normalizeCodexCliReasoningEffort(config.reasoningEffort) } : {},
|
|
347
|
-
...config.codexCliReasoningEffort !== void 0 ? { codexCliReasoningEffort: normalizeCodexCliReasoningEffort(config.codexCliReasoningEffort) } : {},
|
|
348
|
-
...config.retryOptions?.timeoutMs !== void 0 ? { retryOptions: { timeoutMs: normalizeCodexCliTimeoutMs(config.retryOptions.timeoutMs) } } : {}
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
function normalizeCodexCliFallbackOptions(options) {
|
|
352
|
-
return {
|
|
353
|
-
...options.timeoutMs !== void 0 ? { timeoutMs: normalizeCodexCliTimeoutMs(options.timeoutMs) } : {},
|
|
354
|
-
...options.signal ? { signal: options.signal } : {}
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
function normalizeOptionalString(value, label) {
|
|
358
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
359
|
-
throw new Error(`${label} must be a non-empty string`);
|
|
360
|
-
}
|
|
361
|
-
return value.trim();
|
|
362
|
-
}
|
|
363
|
-
function normalizeCodexCliModel(value) {
|
|
364
|
-
const trimmed = value.trim();
|
|
365
|
-
if (trimmed.length === 0) {
|
|
366
|
-
throw new Error("codex-cli model must be a non-empty string");
|
|
367
|
-
}
|
|
368
|
-
return trimmed;
|
|
369
|
-
}
|
|
370
|
-
function normalizeCodexCliReasoningEffort(value) {
|
|
371
|
-
if (typeof value !== "string") {
|
|
372
|
-
throw new Error("codex-cli reasoningEffort must be one of low, medium, high, xhigh");
|
|
373
|
-
}
|
|
374
|
-
const normalized = value.trim().toLowerCase();
|
|
375
|
-
if (VALID_CODEX_CLI_REASONING_EFFORTS.has(normalized)) {
|
|
376
|
-
return normalized;
|
|
377
|
-
}
|
|
378
|
-
throw new Error("codex-cli reasoningEffort must be one of low, medium, high, xhigh");
|
|
379
|
-
}
|
|
380
|
-
function normalizeCodexCliTimeoutMs(value) {
|
|
381
|
-
const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim().length > 0 ? Number(value) : NaN;
|
|
382
|
-
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) {
|
|
383
|
-
throw new Error("codex-cli timeoutMs must be a positive integer");
|
|
384
|
-
}
|
|
385
|
-
return parsed;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// ../remnic-core/src/fallback-llm.ts
|
|
389
|
-
var PROVIDER_ALIASES = {
|
|
390
|
-
"openai-codex": ["codex"],
|
|
391
|
-
codex: ["openai-codex"],
|
|
392
|
-
"claude-cli": ["anthropic"]
|
|
393
|
-
};
|
|
394
|
-
var LEGACY_PROVIDER_IDS = /* @__PURE__ */ new Set(["openai-codex", "claude-cli"]);
|
|
395
|
-
var MANAGED_SECRETREF_MARKER = ["secretref", "managed"].join("-");
|
|
396
|
-
var PROVIDER_API_KEY_FIELD = ["api", "Key"].join("");
|
|
397
|
-
var BUILT_IN_PROVIDER_FALLBACKS = {
|
|
398
|
-
anthropic: {
|
|
399
|
-
baseUrl: "https://api.anthropic.com/v1",
|
|
400
|
-
api: "anthropic-messages",
|
|
401
|
-
models: [],
|
|
402
|
-
[PROVIDER_API_KEY_FIELD]: MANAGED_SECRETREF_MARKER
|
|
403
|
-
}
|
|
404
|
-
};
|
|
405
|
-
var FallbackLlmClient = class {
|
|
406
|
-
gatewayConfig;
|
|
407
|
-
runtimeContext;
|
|
408
|
-
constructor(gatewayConfig, runtimeContext = {}) {
|
|
409
|
-
this.gatewayConfig = gatewayConfig;
|
|
410
|
-
this.runtimeContext = {
|
|
411
|
-
...runtimeContext,
|
|
412
|
-
workspaceDir: normalizeRuntimePath(runtimeContext.workspaceDir) ?? readGatewayWorkspaceDir(gatewayConfig) ?? defaultOpenClawWorkspaceDir()
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Check if fallback is available (gateway config has at least one model).
|
|
417
|
-
*/
|
|
418
|
-
isAvailable(agentId) {
|
|
419
|
-
const models = this.getModelChain(agentId);
|
|
420
|
-
return models.length > 0;
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Make a chat completion request using the gateway's default AI chain.
|
|
424
|
-
* Tries primary first, then each fallback in order.
|
|
425
|
-
* When agentId is provided, uses that agent persona's model chain instead of defaults.
|
|
426
|
-
*/
|
|
427
|
-
async chatCompletion(messages, options = {}) {
|
|
428
|
-
const models = this.getModelChain(options.agentId, options.model);
|
|
429
|
-
if (models.length === 0) {
|
|
430
|
-
log.warn("fallback LLM: no models configured in gateway");
|
|
431
|
-
return null;
|
|
432
|
-
}
|
|
433
|
-
const runChain = async (runOptions) => {
|
|
434
|
-
for (let i = 0; i < models.length; i++) {
|
|
435
|
-
if (runOptions.signal?.aborted) {
|
|
436
|
-
throw abortReason(runOptions.signal);
|
|
437
|
-
}
|
|
438
|
-
const model = models[i];
|
|
439
|
-
const isFallback = i > 0;
|
|
440
|
-
try {
|
|
441
|
-
const result = await this.tryModel(model, messages, runOptions);
|
|
442
|
-
if (result) {
|
|
443
|
-
if (isFallback) {
|
|
444
|
-
log.debug(`fallback LLM: succeeded using ${model.modelString} (fallback ${i})`);
|
|
445
|
-
}
|
|
446
|
-
return {
|
|
447
|
-
content: result.content,
|
|
448
|
-
modelUsed: model.modelString,
|
|
449
|
-
usage: result.usage
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
} catch (err) {
|
|
453
|
-
if (runOptions.signal?.aborted) {
|
|
454
|
-
throw abortReason(runOptions.signal);
|
|
455
|
-
}
|
|
456
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
457
|
-
log.debug(`fallback LLM: ${model.modelString} failed (${errorMsg}), trying next...`);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
log.warn(`fallback LLM: all ${models.length} models in chain failed`);
|
|
461
|
-
return null;
|
|
462
|
-
};
|
|
463
|
-
if (typeof options.timeoutMs === "number") {
|
|
464
|
-
if (options.timeoutMs <= 0) {
|
|
465
|
-
log.warn("fallback LLM: timed out before request started");
|
|
466
|
-
return null;
|
|
467
|
-
}
|
|
468
|
-
let timeoutHandle;
|
|
469
|
-
const controller = new AbortController();
|
|
470
|
-
const onCallerAbort = () => {
|
|
471
|
-
controller.abort(abortReason(options.signal));
|
|
472
|
-
};
|
|
473
|
-
options.signal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
474
|
-
if (options.signal?.aborted) {
|
|
475
|
-
onCallerAbort();
|
|
476
|
-
}
|
|
477
|
-
const timedOptions = { ...options, signal: controller.signal };
|
|
478
|
-
const chain = runChain(timedOptions);
|
|
479
|
-
chain.catch(() => {
|
|
480
|
-
});
|
|
481
|
-
try {
|
|
482
|
-
return await Promise.race([
|
|
483
|
-
chain,
|
|
484
|
-
new Promise((resolve) => {
|
|
485
|
-
timeoutHandle = setTimeout(() => {
|
|
486
|
-
log.warn(`fallback LLM: timed out after ${options.timeoutMs}ms`);
|
|
487
|
-
controller.abort(
|
|
488
|
-
new Error(`fallback LLM timed out after ${options.timeoutMs}ms`)
|
|
489
|
-
);
|
|
490
|
-
resolve(null);
|
|
491
|
-
}, options.timeoutMs);
|
|
492
|
-
})
|
|
493
|
-
]);
|
|
494
|
-
} finally {
|
|
495
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
496
|
-
options.signal?.removeEventListener("abort", onCallerAbort);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
return await runChain(options);
|
|
500
|
-
}
|
|
501
|
-
/**
|
|
502
|
-
* Make a request with structured output (Zod schema).
|
|
503
|
-
* Returns parsed JSON or null on failure.
|
|
504
|
-
*/
|
|
505
|
-
async parseWithSchema(messages, schema, options = {}) {
|
|
506
|
-
const detailed = await this.parseWithSchemaDetailed(messages, schema, options);
|
|
507
|
-
return detailed?.result ?? null;
|
|
508
|
-
}
|
|
509
|
-
/**
|
|
510
|
-
* Like parseWithSchema but also returns the model that was used,
|
|
511
|
-
* so callers can emit accurate trace events.
|
|
512
|
-
*/
|
|
513
|
-
async parseWithSchemaDetailed(messages, schema, options = {}) {
|
|
514
|
-
const response = await this.chatCompletion(messages, options);
|
|
515
|
-
if (!response?.content) return null;
|
|
516
|
-
try {
|
|
517
|
-
const candidates = extractJsonCandidates(response.content);
|
|
518
|
-
for (const c of candidates) {
|
|
519
|
-
try {
|
|
520
|
-
const parsed = JSON.parse(c);
|
|
521
|
-
return { result: schema.parse(parsed), modelUsed: response.modelUsed };
|
|
522
|
-
} catch {
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
return null;
|
|
526
|
-
} catch (err) {
|
|
527
|
-
log.warn("fallback LLM: failed to parse structured output:", err);
|
|
528
|
-
return null;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
|
-
* Get the full model chain from gateway config.
|
|
533
|
-
* Returns array of models in order: [primary, fallback1, fallback2, ...]
|
|
534
|
-
*
|
|
535
|
-
* When agentId is provided, looks up the matching entry in agents.list[]
|
|
536
|
-
* and uses that persona's model chain. Falls back to agents.defaults.model
|
|
537
|
-
* if agentId is not found or not provided.
|
|
538
|
-
*/
|
|
539
|
-
getModelChain(agentId, modelOverride) {
|
|
540
|
-
const chain = [];
|
|
541
|
-
const providers = this.gatewayConfig?.models?.providers ?? {};
|
|
542
|
-
let modelConfig;
|
|
543
|
-
if (agentId) {
|
|
544
|
-
const persona = this.gatewayConfig?.agents?.list?.find(
|
|
545
|
-
(a) => a.id === agentId
|
|
546
|
-
);
|
|
547
|
-
if (persona?.model) {
|
|
548
|
-
modelConfig = persona.model;
|
|
549
|
-
log.debug(`fallback LLM: using agent persona "${agentId}" model chain`);
|
|
550
|
-
} else {
|
|
551
|
-
log.warn(
|
|
552
|
-
`fallback LLM: agent persona "${agentId}" not found or has no model config, falling back to defaults`
|
|
553
|
-
);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
if (!modelConfig) {
|
|
557
|
-
modelConfig = this.gatewayConfig?.agents?.defaults?.model;
|
|
558
|
-
}
|
|
559
|
-
const modelStrings = [];
|
|
560
|
-
if (typeof modelOverride === "string" && modelOverride.trim().length > 0) {
|
|
561
|
-
modelStrings.push(modelOverride.trim());
|
|
562
|
-
}
|
|
563
|
-
if (modelConfig?.primary) {
|
|
564
|
-
if (!modelStrings.includes(modelConfig.primary)) {
|
|
565
|
-
modelStrings.push(modelConfig.primary);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
if (Array.isArray(modelConfig?.fallbacks)) {
|
|
569
|
-
for (const fb of modelConfig.fallbacks) {
|
|
570
|
-
if (typeof fb === "string" && !modelStrings.includes(fb)) {
|
|
571
|
-
modelStrings.push(fb);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
for (const modelString of modelStrings) {
|
|
576
|
-
const modelRef = this.parseModelString(modelString, providers);
|
|
577
|
-
if (modelRef) {
|
|
578
|
-
chain.push(modelRef);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
return chain;
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* Parse a "provider/model" string and look up its config.
|
|
585
|
-
*/
|
|
586
|
-
parseModelString(modelString, providers) {
|
|
587
|
-
const parts = modelString.split("/");
|
|
588
|
-
if (parts.length < 2) {
|
|
589
|
-
log.warn(`fallback LLM: invalid model format: ${modelString}`);
|
|
590
|
-
return null;
|
|
591
|
-
}
|
|
592
|
-
const requestedProviderId = parts[0];
|
|
593
|
-
const modelId = parts.slice(1).join("/");
|
|
594
|
-
const resolvedProvider = this.resolveProviderConfig(requestedProviderId, providers);
|
|
595
|
-
const providerConfig = resolvedProvider?.config;
|
|
596
|
-
if (!providerConfig) {
|
|
597
|
-
log.warn(
|
|
598
|
-
`fallback LLM: provider not found: ${requestedProviderId} (tried: ${this.providerResolutionCandidates(requestedProviderId).join(", ")})`
|
|
599
|
-
);
|
|
600
|
-
return null;
|
|
601
|
-
}
|
|
602
|
-
return {
|
|
603
|
-
providerId: resolvedProvider.providerId,
|
|
604
|
-
modelId,
|
|
605
|
-
providerConfig,
|
|
606
|
-
modelString
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
resolveProviderConfig(providerId, providers) {
|
|
610
|
-
const candidates = this.providerResolutionCandidates(providerId);
|
|
611
|
-
const aliasCandidates = candidates.filter((candidate) => candidate !== providerId);
|
|
612
|
-
const fallbackCandidates = LEGACY_PROVIDER_IDS.has(providerId) ? [...aliasCandidates, providerId] : [providerId, ...aliasCandidates];
|
|
613
|
-
for (const candidate of candidates) {
|
|
614
|
-
const config = providers[candidate];
|
|
615
|
-
if (config) {
|
|
616
|
-
if (candidate !== providerId) {
|
|
617
|
-
log.debug(`fallback LLM: provider "${providerId}" resolved via alias "${candidate}"`);
|
|
618
|
-
}
|
|
619
|
-
return { providerId: candidate, config };
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
for (const candidate of fallbackCandidates) {
|
|
623
|
-
const config = this.resolveFromModelsJson(candidate);
|
|
624
|
-
if (config) {
|
|
625
|
-
if (candidate !== providerId) {
|
|
626
|
-
log.debug(`fallback LLM: provider "${providerId}" resolved via models.json alias "${candidate}"`);
|
|
627
|
-
}
|
|
628
|
-
return { providerId: candidate, config };
|
|
629
|
-
}
|
|
630
|
-
const builtInConfig = BUILT_IN_PROVIDER_FALLBACKS[candidate];
|
|
631
|
-
if (builtInConfig) {
|
|
632
|
-
if (candidate === providerId) {
|
|
633
|
-
log.debug(`fallback LLM: provider "${providerId}" resolved from built-in defaults`);
|
|
634
|
-
return { providerId, config: builtInConfig };
|
|
635
|
-
}
|
|
636
|
-
log.debug(`fallback LLM: provider "${providerId}" resolved via built-in alias "${candidate}"`);
|
|
637
|
-
return { providerId: candidate, config: builtInConfig };
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
return null;
|
|
641
|
-
}
|
|
642
|
-
providerResolutionCandidates(providerId) {
|
|
643
|
-
const candidates = [providerId, ...PROVIDER_ALIASES[providerId] ?? []];
|
|
644
|
-
return [...new Set(candidates)];
|
|
645
|
-
}
|
|
646
|
-
/**
|
|
647
|
-
* Look up a provider from the gateway's materialized models.json, which
|
|
648
|
-
* contains all providers including built-in ones (openai-codex, google-vertex,
|
|
649
|
-
* etc.) that aren't in the user's openclaw.json but are registered by
|
|
650
|
-
* gateway plugins. Returns null if the provider isn't found there either.
|
|
651
|
-
*/
|
|
652
|
-
resolveFromModelsJson(providerId) {
|
|
653
|
-
const allProviders = loadModelsJsonProviders();
|
|
654
|
-
const config = allProviders[providerId];
|
|
655
|
-
if (config) {
|
|
656
|
-
log.debug(`fallback LLM: resolved provider "${providerId}" from models.json (api: ${config.api ?? "default"})`);
|
|
657
|
-
return config;
|
|
658
|
-
}
|
|
659
|
-
return null;
|
|
660
|
-
}
|
|
661
|
-
/**
|
|
662
|
-
* Try to call a single model.
|
|
663
|
-
*
|
|
664
|
-
* Uses the gateway's native getRuntimeAuthForModel when available — this
|
|
665
|
-
* handles all provider-specific auth transforms (OAuth token exchange,
|
|
666
|
-
* base URL overrides for codex/copilot/etc.) through the same codepath
|
|
667
|
-
* the gateway itself uses. Falls back to resolveProviderApiKey for
|
|
668
|
-
* simpler providers or when the runtime module isn't loaded.
|
|
669
|
-
*/
|
|
670
|
-
async tryModel(model, messages, options) {
|
|
671
|
-
const runtimeAuth = model.providerConfig.api === "codex-cli" ? null : await this.resolveRuntimeAuth(model);
|
|
672
|
-
const effectiveBaseUrl = runtimeAuth?.baseUrl ?? model.providerConfig.baseUrl;
|
|
673
|
-
const resolvedCredential = runtimeAuth?.["api"+"Key"] ?? (model.providerConfig.api === "codex-cli" && model.providerConfig["api"+"Key"] === void 0 ? void 0 : await this.resolveFallbackApiKey(model));
|
|
674
|
-
const rawKey = model.providerConfig["api"+"Key"];
|
|
675
|
-
const needsResolution = rawKey === "secretref-managed" || typeof rawKey === "object" && rawKey !== null;
|
|
676
|
-
if (needsResolution && !resolvedCredential) {
|
|
677
|
-
throw new Error(`API key for provider "${model.providerId}" could not be resolved from secret ref`);
|
|
678
|
-
}
|
|
679
|
-
const effectiveConfig = {
|
|
680
|
-
...model.providerConfig,
|
|
681
|
-
baseUrl: effectiveBaseUrl,
|
|
682
|
-
...resolvedCredential ? { ["api"+"Key"]: resolvedCredential } : {}
|
|
683
|
-
};
|
|
684
|
-
if (model.providerConfig.api === "anthropic-messages") {
|
|
685
|
-
return await this.callAnthropic(effectiveConfig, model.modelId, messages, options);
|
|
686
|
-
}
|
|
687
|
-
if (model.providerConfig.api === "codex-cli") {
|
|
688
|
-
return await callCodexCliFallback(
|
|
689
|
-
effectiveConfig,
|
|
690
|
-
model.modelId,
|
|
691
|
-
messages,
|
|
692
|
-
{ timeoutMs: options.timeoutMs, signal: options.signal }
|
|
693
|
-
);
|
|
694
|
-
}
|
|
695
|
-
if (model.providerConfig.api === "ollama-chat") {
|
|
696
|
-
return await this.callOllamaChat(effectiveConfig, model.modelId, messages, options);
|
|
697
|
-
}
|
|
698
|
-
if (model.providerConfig.api === "openai-responses" || model.providerConfig.api === "openai-codex-responses" || model.providerConfig.api === "azure-openai-responses") {
|
|
699
|
-
return await this.callOpenAIResponses(
|
|
700
|
-
effectiveConfig,
|
|
701
|
-
model.modelId,
|
|
702
|
-
messages,
|
|
703
|
-
options
|
|
704
|
-
);
|
|
705
|
-
}
|
|
706
|
-
return await this.callOpenAI(
|
|
707
|
-
effectiveConfig,
|
|
708
|
-
model.modelId,
|
|
709
|
-
messages,
|
|
710
|
-
options,
|
|
711
|
-
shouldAssumeOpenAiChatCompletions(effectiveConfig.baseUrl)
|
|
712
|
-
);
|
|
713
|
-
}
|
|
714
|
-
/**
|
|
715
|
-
* Resolve request-ready auth through the gateway's native runtime, which
|
|
716
|
-
* handles provider-specific transforms (OAuth token exchange for codex/copilot,
|
|
717
|
-
* base URL rewrite, etc.). Returns null if the runtime isn't available.
|
|
718
|
-
*/
|
|
719
|
-
async resolveRuntimeAuth(model) {
|
|
720
|
-
try {
|
|
721
|
-
const getRuntimeAuth = await getGatewayRuntimeAuthForModel();
|
|
722
|
-
if (!getRuntimeAuth) return null;
|
|
723
|
-
const result = await getRuntimeAuth({
|
|
724
|
-
model: {
|
|
725
|
-
provider: model.providerId,
|
|
726
|
-
id: model.modelId,
|
|
727
|
-
api: model.providerConfig.api,
|
|
728
|
-
baseUrl: model.providerConfig.baseUrl
|
|
729
|
-
},
|
|
730
|
-
cfg: this.gatewayConfig,
|
|
731
|
-
workspaceDir: this.runtimeContext.workspaceDir
|
|
732
|
-
});
|
|
733
|
-
if (result?.["api"+"Key"] || result?.baseUrl) {
|
|
734
|
-
log.debug(
|
|
735
|
-
`fallback LLM: resolved runtime auth for "${model.modelString}" (source: ${result.source ?? "unknown"}, mode: ${result.mode ?? "unknown"})`
|
|
736
|
-
);
|
|
737
|
-
return { ["api"+"Key"]: result["api"+"Key"], baseUrl: result.baseUrl };
|
|
738
|
-
}
|
|
739
|
-
} catch (err) {
|
|
740
|
-
log.debug(
|
|
741
|
-
`fallback LLM: gateway runtime auth failed for "${model.modelString}": ${err instanceof Error ? err.message : String(err)}`
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
return null;
|
|
745
|
-
}
|
|
746
|
-
/**
|
|
747
|
-
* Resolve API key through the existing provider-level resolution (env vars,
|
|
748
|
-
* secret refs, etc.). Used as fallback when gateway runtime auth isn't available.
|
|
749
|
-
*/
|
|
750
|
-
async resolveFallbackApiKey(model) {
|
|
751
|
-
return resolveProviderCredential(
|
|
752
|
-
model.providerId,
|
|
753
|
-
model.providerConfig["api"+"Key"],
|
|
754
|
-
this.gatewayConfig,
|
|
755
|
-
this.runtimeContext.agentDir
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
/**
|
|
759
|
-
* Call OpenAI-compatible API.
|
|
760
|
-
*/
|
|
761
|
-
async callOpenAI(config, modelId, messages, options, assumeOpenAI) {
|
|
762
|
-
const base = config.baseUrl.replace(/\/$/, "");
|
|
763
|
-
const url = base.endsWith("/v1") ? `${base}/chat/completions` : `${base}/v1/chat/completions`;
|
|
764
|
-
const headers = {
|
|
765
|
-
"Content-Type": "application/json",
|
|
766
|
-
...config.headers
|
|
767
|
-
};
|
|
768
|
-
if (config["api"+"Key"] && typeof config["api"+"Key"] === "string") {
|
|
769
|
-
if (config.authHeader !== false) {
|
|
770
|
-
headers["Authorization"] = `Bearer ${config["api"+"Key"]}`;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
const body = {
|
|
774
|
-
model: modelId,
|
|
775
|
-
messages,
|
|
776
|
-
...buildChatCompletionTemperature(modelId, options.temperature ?? 0.3, {
|
|
777
|
-
assumeOpenAI
|
|
778
|
-
}),
|
|
779
|
-
...buildChatCompletionTokenLimit(modelId, options.maxTokens ?? 4096, {
|
|
780
|
-
assumeOpenAI
|
|
781
|
-
})
|
|
782
|
-
};
|
|
783
|
-
const response = await fetch(url, {
|
|
784
|
-
method: "POST",
|
|
785
|
-
headers,
|
|
786
|
-
signal: options.signal,
|
|
787
|
-
body: JSON.stringify(body)
|
|
788
|
-
});
|
|
789
|
-
if (!response.ok) {
|
|
790
|
-
const error = await response.text();
|
|
791
|
-
throw new Error(`OpenAI API error: ${response.status} ${error}`);
|
|
792
|
-
}
|
|
793
|
-
const data = await response.json();
|
|
794
|
-
const content = data.choices?.[0]?.message?.content;
|
|
795
|
-
if (!content) {
|
|
796
|
-
throw new Error("Empty response from OpenAI API");
|
|
797
|
-
}
|
|
798
|
-
return {
|
|
799
|
-
content,
|
|
800
|
-
usage: data.usage ? {
|
|
801
|
-
inputTokens: data.usage.prompt_tokens,
|
|
802
|
-
outputTokens: data.usage.completion_tokens,
|
|
803
|
-
totalTokens: data.usage.total_tokens
|
|
804
|
-
} : void 0
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
/**
|
|
808
|
-
* Call Ollama's native /api/chat transport. This lets benchmark-isolated
|
|
809
|
-
* gateway configs route Remnic's own internal LLM calls to Ollama Cloud
|
|
810
|
-
* without requiring an OpenAI-compatible shim.
|
|
811
|
-
*/
|
|
812
|
-
async callOllamaChat(config, modelId, messages, options) {
|
|
813
|
-
const base = config.baseUrl.replace(/\/$/, "");
|
|
814
|
-
const url = base.endsWith("/api") ? `${base}/chat` : `${base}/api/chat`;
|
|
815
|
-
const headers = {
|
|
816
|
-
"Content-Type": "application/json",
|
|
817
|
-
...config.headers
|
|
818
|
-
};
|
|
819
|
-
if (config["api"+"Key"] && typeof config["api"+"Key"] === "string" && config.authHeader !== false) {
|
|
820
|
-
headers.Authorization = `Bearer ${config["api"+"Key"]}`;
|
|
821
|
-
}
|
|
822
|
-
const response = await fetch(url, {
|
|
823
|
-
method: "POST",
|
|
824
|
-
headers,
|
|
825
|
-
signal: options.signal,
|
|
826
|
-
body: JSON.stringify({
|
|
827
|
-
model: modelId,
|
|
828
|
-
messages,
|
|
829
|
-
stream: false,
|
|
830
|
-
...config.disableThinking ? { think: false } : {},
|
|
831
|
-
options: {
|
|
832
|
-
temperature: options.temperature ?? 0.3,
|
|
833
|
-
num_predict: options.maxTokens ?? 4096
|
|
834
|
-
}
|
|
835
|
-
})
|
|
836
|
-
});
|
|
837
|
-
if (!response.ok) {
|
|
838
|
-
const error = await response.text();
|
|
839
|
-
throw new Error(`Ollama API error: ${response.status} ${error}`);
|
|
840
|
-
}
|
|
841
|
-
const data = await response.json();
|
|
842
|
-
const content = data.message?.content ?? data.response;
|
|
843
|
-
if (!content) {
|
|
844
|
-
throw new Error("Empty response from Ollama API");
|
|
845
|
-
}
|
|
846
|
-
const inputTokens = data.prompt_eval_count ?? 0;
|
|
847
|
-
const outputTokens = data.eval_count ?? 0;
|
|
848
|
-
return {
|
|
849
|
-
content,
|
|
850
|
-
usage: {
|
|
851
|
-
inputTokens,
|
|
852
|
-
outputTokens,
|
|
853
|
-
totalTokens: inputTokens + outputTokens
|
|
854
|
-
}
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
/**
|
|
858
|
-
* Call an OpenAI-compatible Responses API.
|
|
859
|
-
*/
|
|
860
|
-
async callOpenAIResponses(config, modelId, messages, options) {
|
|
861
|
-
const base = config.baseUrl.replace(/\/$/, "");
|
|
862
|
-
const url = base.endsWith("/v1") ? `${base}/responses` : `${base}/v1/responses`;
|
|
863
|
-
const headers = {
|
|
864
|
-
"Content-Type": "application/json",
|
|
865
|
-
...config.headers
|
|
866
|
-
};
|
|
867
|
-
if (config["api"+"Key"] && typeof config["api"+"Key"] === "string" && config.authHeader !== false) {
|
|
868
|
-
headers["Authorization"] = `Bearer ${config["api"+"Key"]}`;
|
|
869
|
-
}
|
|
870
|
-
const instructions = messages.filter((message) => message.role === "system").map((message) => message.content).join("\n\n").trim();
|
|
871
|
-
const input = messages.filter((message) => message.role !== "system").map((message) => ({
|
|
872
|
-
role: message.role,
|
|
873
|
-
content: [{
|
|
874
|
-
type: message.role === "assistant" ? "output_text" : "input_text",
|
|
875
|
-
text: message.content
|
|
876
|
-
}]
|
|
877
|
-
}));
|
|
878
|
-
const body = {
|
|
879
|
-
model: modelId,
|
|
880
|
-
input,
|
|
881
|
-
max_output_tokens: Math.max(0, Math.floor(options.maxTokens ?? 4096)),
|
|
882
|
-
...buildChatCompletionTemperature(modelId, options.temperature ?? 0.3, {
|
|
883
|
-
assumeOpenAI: shouldAssumeOpenAiChatCompletions(config.baseUrl)
|
|
884
|
-
})
|
|
885
|
-
};
|
|
886
|
-
if (instructions.length > 0) {
|
|
887
|
-
body.instructions = instructions;
|
|
888
|
-
}
|
|
889
|
-
const response = await fetch(url, {
|
|
890
|
-
method: "POST",
|
|
891
|
-
headers,
|
|
892
|
-
signal: options.signal,
|
|
893
|
-
body: JSON.stringify(body)
|
|
894
|
-
});
|
|
895
|
-
if (!response.ok) {
|
|
896
|
-
const error = await response.text();
|
|
897
|
-
throw new Error(`OpenAI Responses API error: ${response.status} ${error}`);
|
|
898
|
-
}
|
|
899
|
-
const data = await response.json();
|
|
900
|
-
const outputText = extractResponsesOutputText(data);
|
|
901
|
-
if (!outputText) {
|
|
902
|
-
throw new Error("Empty response from OpenAI Responses API");
|
|
903
|
-
}
|
|
904
|
-
return {
|
|
905
|
-
content: outputText,
|
|
906
|
-
usage: data.usage ? {
|
|
907
|
-
inputTokens: data.usage.input_tokens,
|
|
908
|
-
outputTokens: data.usage.output_tokens,
|
|
909
|
-
totalTokens: data.usage.total_tokens
|
|
910
|
-
} : void 0
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
/**
|
|
914
|
-
* Call Anthropic Messages API.
|
|
915
|
-
*/
|
|
916
|
-
async callAnthropic(config, modelId, messages, options) {
|
|
917
|
-
const base = config.baseUrl.replace(/\/$/, "");
|
|
918
|
-
const url = base.endsWith("/v1") ? `${base}/messages` : `${base}/v1/messages`;
|
|
919
|
-
const headers = {
|
|
920
|
-
"Content-Type": "application/json",
|
|
921
|
-
"anthropic-version": "2023-06-01",
|
|
922
|
-
...config.headers
|
|
923
|
-
};
|
|
924
|
-
if (config["api"+"Key"] && typeof config["api"+"Key"] === "string") {
|
|
925
|
-
headers["x-api-key"] = config["api"+"Key"];
|
|
926
|
-
}
|
|
927
|
-
const systemMessage = messages.find((m) => m.role === "system")?.content;
|
|
928
|
-
const nonSystemMessages = messages.filter((m) => m.role !== "system");
|
|
929
|
-
const anthropicMessages = nonSystemMessages.map((m) => ({
|
|
930
|
-
role: m.role,
|
|
931
|
-
content: m.content
|
|
932
|
-
}));
|
|
933
|
-
const body = {
|
|
934
|
-
model: modelId,
|
|
935
|
-
messages: anthropicMessages,
|
|
936
|
-
max_tokens: options.maxTokens ?? 4096,
|
|
937
|
-
temperature: options.temperature ?? 0.3
|
|
938
|
-
};
|
|
939
|
-
if (systemMessage) {
|
|
940
|
-
body.system = systemMessage;
|
|
941
|
-
}
|
|
942
|
-
const response = await fetch(url, {
|
|
943
|
-
method: "POST",
|
|
944
|
-
headers,
|
|
945
|
-
signal: options.signal,
|
|
946
|
-
body: JSON.stringify(body)
|
|
947
|
-
});
|
|
948
|
-
if (!response.ok) {
|
|
949
|
-
const error = await response.text();
|
|
950
|
-
throw new Error(`Anthropic API error: ${response.status} ${error}`);
|
|
951
|
-
}
|
|
952
|
-
const data = await response.json();
|
|
953
|
-
const content = data.content?.[0]?.text;
|
|
954
|
-
if (!content) {
|
|
955
|
-
throw new Error("Empty response from Anthropic API");
|
|
956
|
-
}
|
|
957
|
-
return {
|
|
958
|
-
content,
|
|
959
|
-
usage: data.usage ? {
|
|
960
|
-
inputTokens: data.usage.input_tokens,
|
|
961
|
-
outputTokens: data.usage.output_tokens,
|
|
962
|
-
totalTokens: (data.usage.input_tokens ?? 0) + (data.usage.output_tokens ?? 0)
|
|
963
|
-
} : void 0
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
|
-
};
|
|
967
|
-
function abortReason(signal) {
|
|
968
|
-
const reason = signal?.reason;
|
|
969
|
-
return reason instanceof Error ? reason : new Error("fallback LLM request aborted");
|
|
970
|
-
}
|
|
971
|
-
function normalizeRuntimePath(value) {
|
|
972
|
-
if (typeof value !== "string") return void 0;
|
|
973
|
-
const trimmed = value.trim();
|
|
974
|
-
return trimmed.length > 0 ? expandTildePath(trimmed) : void 0;
|
|
975
|
-
}
|
|
976
|
-
function readGatewayWorkspaceDir(gatewayConfig) {
|
|
977
|
-
if (!gatewayConfig || typeof gatewayConfig !== "object") return void 0;
|
|
978
|
-
const raw = gatewayConfig;
|
|
979
|
-
return normalizeRuntimePath(raw.workspaceDir) ?? normalizeRuntimePath(raw.workspacePath) ?? normalizeRuntimePath(raw.workspace);
|
|
980
|
-
}
|
|
981
|
-
function defaultOpenClawWorkspaceDir() {
|
|
982
|
-
return path2.join(resolveHomeDir(), ".openclaw", "workspace");
|
|
983
|
-
}
|
|
984
|
-
function extractResponsesOutputText(data) {
|
|
985
|
-
if (typeof data.output_text === "string" && data.output_text.trim().length > 0) {
|
|
986
|
-
return data.output_text;
|
|
987
|
-
}
|
|
988
|
-
const chunks = [];
|
|
989
|
-
for (const item of data.output ?? []) {
|
|
990
|
-
if (typeof item.text === "string" && item.text.trim().length > 0) {
|
|
991
|
-
chunks.push(item.text);
|
|
992
|
-
}
|
|
993
|
-
for (const part of item.content ?? []) {
|
|
994
|
-
if ((part.type === "output_text" || part.type === "text") && typeof part.text === "string" && part.text.trim().length > 0) {
|
|
995
|
-
chunks.push(part.text);
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
const joined = chunks.join("\n").trim();
|
|
1000
|
-
return joined.length > 0 ? joined : null;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
export {
|
|
1004
|
-
FallbackLlmClient
|
|
1005
|
-
};
|