@remnic/plugin-openclaw 1.0.53 → 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.
Files changed (84) hide show
  1. package/dist/index.js +510 -2071
  2. package/openclaw.plugin.json +3 -3
  3. package/package.json +4 -3
  4. package/scripts/faiss_index.py +141 -29
  5. package/dist/calibration-RKL2LRW4.js +0 -250
  6. package/dist/capsule-cli-EHZPMXBC.js +0 -33
  7. package/dist/capsule-crypto-JS67OSWM.js +0 -17
  8. package/dist/capsule-export-DX53CPIT.js +0 -17
  9. package/dist/capsule-import-4OXCPHOT.js +0 -16
  10. package/dist/capsule-merge-25AUN33Q.js +0 -195
  11. package/dist/causal-chain-BVTOWZKC.js +0 -23
  12. package/dist/causal-consolidation-V533DIGW.js +0 -233
  13. package/dist/causal-retrieval-XAP6QKHZ.js +0 -182
  14. package/dist/causal-trajectory-graph-ZWQWZ7N5.js +0 -59
  15. package/dist/chunk-25J4PXDH.js +0 -30
  16. package/dist/chunk-2RQU6GVD.js +0 -453
  17. package/dist/chunk-3A5ELHTT.js +0 -61
  18. package/dist/chunk-3G7FAF6S.js +0 -60
  19. package/dist/chunk-3IKMUNW5.js +0 -177
  20. package/dist/chunk-4XDQ3KEC.js +0 -4048
  21. package/dist/chunk-5ZW5XJQ6.js +0 -128
  22. package/dist/chunk-6IWEAUN6.js +0 -148
  23. package/dist/chunk-7JOLBJJ5.js +0 -874
  24. package/dist/chunk-B52XADV3.js +0 -244
  25. package/dist/chunk-BQZ7JACP.js +0 -389
  26. package/dist/chunk-BU5KJVWF.js +0 -78
  27. package/dist/chunk-BZ4EYURA.js +0 -181
  28. package/dist/chunk-CEL5ZLKP.js +0 -233
  29. package/dist/chunk-EH4AXGRO.js +0 -48
  30. package/dist/chunk-I2KLQ2HA.js +0 -22
  31. package/dist/chunk-IO5WWY6A.js +0 -156
  32. package/dist/chunk-JC3FCKYL.js +0 -516
  33. package/dist/chunk-JZBOXOUC.js +0 -259
  34. package/dist/chunk-KC7KSQR4.js +0 -302
  35. package/dist/chunk-LZCGPRHS.js +0 -228
  36. package/dist/chunk-MBIFE6SA.js +0 -250
  37. package/dist/chunk-MWLY7VQX.js +0 -896
  38. package/dist/chunk-MXFJXUHC.js +0 -292
  39. package/dist/chunk-NUWDSTP7.js +0 -233
  40. package/dist/chunk-PSAPZGDX.js +0 -725
  41. package/dist/chunk-QCCP4RU5.js +0 -190
  42. package/dist/chunk-QMUQV5NP.js +0 -146
  43. package/dist/chunk-QQXJODFL.js +0 -328
  44. package/dist/chunk-QVTGUY3H.js +0 -1295
  45. package/dist/chunk-SWOYEQN2.js +0 -1005
  46. package/dist/chunk-TH5FF5SC.js +0 -16
  47. package/dist/chunk-TXOEHSVP.js +0 -275
  48. package/dist/chunk-UFU5GGGA.js +0 -47
  49. package/dist/chunk-UZJ7EERS.js +0 -272
  50. package/dist/chunk-W6EEFUCJ.js +0 -349
  51. package/dist/chunk-YGGGUTG3.js +0 -125
  52. package/dist/chunk-YJYZMLD5.js +0 -360
  53. package/dist/chunk-YKV4EFUI.js +0 -199
  54. package/dist/chunk-YXDXRE6N.js +0 -6500
  55. package/dist/chunk-ZS6VABML.js +0 -185
  56. package/dist/cipher-E23BHBSO.js +0 -27
  57. package/dist/consolidation-undo-FKJZCJHS.js +0 -426
  58. package/dist/contradiction-review-WJRWNQ5N.js +0 -29
  59. package/dist/contradiction-scan-5X423QGT.js +0 -12
  60. package/dist/dreams-ledger-KDX44I7R.js +0 -290
  61. package/dist/engine-7VG3IXKN.js +0 -16
  62. package/dist/extraction-judge-telemetry-O4ZVGLTU.js +0 -14
  63. package/dist/fallback-llm-43UMEXNJ.js +0 -10
  64. package/dist/first-start-migration-H2SAXAGR.js +0 -263
  65. package/dist/forget-ZECIDNL5.js +0 -68
  66. package/dist/fs-utils-OYXSZSVV.js +0 -39
  67. package/dist/graph-edge-decay-24ZKD5QL.js +0 -202
  68. package/dist/kdf-RXKIWHRU.js +0 -25
  69. package/dist/logger-XG7JKLPS.js +0 -9
  70. package/dist/memory-governance-TK4B4ZZ2.js +0 -25
  71. package/dist/metadata-WK2TRPYZ.js +0 -20
  72. package/dist/migrate-from-identity-anchor-SNDNKHZD.js +0 -7
  73. package/dist/path-ZKO74XXC.js +0 -7
  74. package/dist/peers-W53WSDXG.js +0 -43
  75. package/dist/purge-IKJISXEQ.js +0 -204
  76. package/dist/resolution-BN35OXDS.js +0 -11
  77. package/dist/secure-store-6RWYM6GK.js +0 -155
  78. package/dist/state-4ITLYMAU.js +0 -7
  79. package/dist/state-store-ET3ADVY5.js +0 -14
  80. package/dist/storage-HUNVVHZ7.js +0 -27
  81. package/dist/tier-stats-ZRQBV6G2.js +0 -147
  82. package/dist/trace-IL2Y34EH.js +0 -289
  83. package/dist/tui-7KRDCMYK.js +0 -12
  84. package/dist/types-MBUINTB2.js +0 -30
@@ -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
- };