@remnic/plugin-openclaw 1.0.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/calibration-KQXCC77L.js +235 -0
- package/dist/causal-chain-LA3IQNL6.js +22 -0
- package/dist/causal-consolidation-WINYJQJ4.js +205 -0
- package/dist/causal-retrieval-ITNQBUQM.js +182 -0
- package/dist/causal-trajectory-graph-7Z5DD66L.js +59 -0
- package/dist/chunk-5LE4HTVL.js +46 -0
- package/dist/chunk-BZ27H3BL.js +158 -0
- package/dist/chunk-DMGIUDBO.js +41 -0
- package/dist/chunk-GUSMRW4H.js +12 -0
- package/dist/chunk-H3SKMYPU.js +340 -0
- package/dist/chunk-HSBPDYF4.js +278 -0
- package/dist/chunk-JGEKL3WH.js +434 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-TJZ7KBCC.js +577 -0
- package/dist/chunk-WLR4WL6B.js +5893 -0
- package/dist/chunk-Y7JG2Q3V.js +4242 -0
- package/dist/engine-QHRKR53Q.js +11 -0
- package/dist/fallback-llm-2VMRPBHR.js +8 -0
- package/dist/index.js +61705 -0
- package/dist/legacy-hook-compat-XQ7FP6FV.js +35 -0
- package/dist/logger-NZE2OBVA.js +9 -0
- package/dist/storage-AGYBIME4.js +16 -0
- package/openclaw.plugin.json +3779 -0
- package/package.json +54 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import {
|
|
2
|
+
log
|
|
3
|
+
} from "./chunk-DMGIUDBO.js";
|
|
4
|
+
|
|
5
|
+
// ../remnic-core/src/json-extract.ts
|
|
6
|
+
function stripCodeFences(text) {
|
|
7
|
+
return text.replace(/```(?:json)?\s*([\s\S]*?)```/gi, (_m, inner) => String(inner).trim());
|
|
8
|
+
}
|
|
9
|
+
function extractJsonCandidates(text) {
|
|
10
|
+
const trimmed = text.trim();
|
|
11
|
+
const cleaned = stripCodeFences(trimmed);
|
|
12
|
+
const candidates = [];
|
|
13
|
+
if (cleaned.length > 0) candidates.push(cleaned);
|
|
14
|
+
candidates.push(...scanBalancedJsonBlocks(cleaned));
|
|
15
|
+
const objMatch = cleaned.match(/\{[\s\S]*\}/);
|
|
16
|
+
if (objMatch) candidates.push(objMatch[0]);
|
|
17
|
+
const seen = /* @__PURE__ */ new Set();
|
|
18
|
+
return candidates.map((c) => c.trim()).filter((c) => c.length > 0).filter((c) => {
|
|
19
|
+
if (seen.has(c)) return false;
|
|
20
|
+
seen.add(c);
|
|
21
|
+
return true;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function scanBalancedJsonBlocks(text) {
|
|
25
|
+
const out = [];
|
|
26
|
+
const opens = /* @__PURE__ */ new Set(["{", "["]);
|
|
27
|
+
const closes = { "{": "}", "[": "]" };
|
|
28
|
+
for (let i = 0; i < text.length; i++) {
|
|
29
|
+
const start = text[i];
|
|
30
|
+
if (!opens.has(start)) continue;
|
|
31
|
+
const expectedClose = closes[start];
|
|
32
|
+
let depth = 0;
|
|
33
|
+
let inString = false;
|
|
34
|
+
let escape = false;
|
|
35
|
+
for (let j = i; j < text.length; j++) {
|
|
36
|
+
const ch = text[j];
|
|
37
|
+
if (inString) {
|
|
38
|
+
if (escape) {
|
|
39
|
+
escape = false;
|
|
40
|
+
} else if (ch === "\\") {
|
|
41
|
+
escape = true;
|
|
42
|
+
} else if (ch === '"') {
|
|
43
|
+
inString = false;
|
|
44
|
+
}
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (ch === '"') {
|
|
48
|
+
inString = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (ch === start) depth++;
|
|
52
|
+
if (ch === expectedClose) depth--;
|
|
53
|
+
if (depth === 0) {
|
|
54
|
+
out.push(text.slice(i, j + 1).trim());
|
|
55
|
+
i = j;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ../remnic-core/src/openai-chat-compat.ts
|
|
64
|
+
function normalizedModel(model) {
|
|
65
|
+
return model.trim().toLowerCase();
|
|
66
|
+
}
|
|
67
|
+
function matchesModelFamily(normalized, familyPattern) {
|
|
68
|
+
return familyPattern.test(normalized);
|
|
69
|
+
}
|
|
70
|
+
function shouldAssumeOpenAiChatCompletions(baseUrl) {
|
|
71
|
+
if (!baseUrl) return true;
|
|
72
|
+
try {
|
|
73
|
+
return new URL(baseUrl).hostname.toLowerCase() === "api.openai.com";
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function usesMaxCompletionTokens(model, options) {
|
|
79
|
+
const normalized = normalizedModel(model);
|
|
80
|
+
if (options?.assumeOpenAI !== true) return false;
|
|
81
|
+
if (matchesModelFamily(normalized, /^gpt-5(?:$|[-.])/)) return true;
|
|
82
|
+
if (matchesModelFamily(normalized, /^gpt-4o(?:$|[-.])/)) return true;
|
|
83
|
+
if (matchesModelFamily(normalized, /^gpt-4\.1(?:$|[-.])/)) return true;
|
|
84
|
+
if (matchesModelFamily(normalized, /^o1(?:$|[-.])/)) return true;
|
|
85
|
+
if (matchesModelFamily(normalized, /^o3(?:$|[-.])/)) return true;
|
|
86
|
+
return matchesModelFamily(normalized, /^o4-mini(?:$|[-.])/);
|
|
87
|
+
}
|
|
88
|
+
function buildChatCompletionTokenLimit(model, maxTokens, options) {
|
|
89
|
+
const safeMaxTokens = Math.max(0, Math.floor(maxTokens));
|
|
90
|
+
if (usesMaxCompletionTokens(model, options)) {
|
|
91
|
+
return { max_completion_tokens: safeMaxTokens };
|
|
92
|
+
}
|
|
93
|
+
return { max_tokens: safeMaxTokens };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ../remnic-core/src/runtime/env.ts
|
|
97
|
+
import os from "os";
|
|
98
|
+
var REMNIC_ENGRAM_PREFIX_PAIRS = [
|
|
99
|
+
["REMNIC_", "ENGRAM_"],
|
|
100
|
+
["ENGRAM_", "REMNIC_"]
|
|
101
|
+
];
|
|
102
|
+
function getEnvMap() {
|
|
103
|
+
const runtimeProcess = globalThis.process;
|
|
104
|
+
return runtimeProcess?.["env"];
|
|
105
|
+
}
|
|
106
|
+
function legacyEnvCandidates(name) {
|
|
107
|
+
const candidates = [name];
|
|
108
|
+
for (const [primary, legacy] of REMNIC_ENGRAM_PREFIX_PAIRS) {
|
|
109
|
+
if (name.startsWith(primary)) {
|
|
110
|
+
candidates.push(`${legacy}${name.slice(primary.length)}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return candidates;
|
|
114
|
+
}
|
|
115
|
+
function readEnvVar(name) {
|
|
116
|
+
const env = getEnvMap();
|
|
117
|
+
for (const candidate of legacyEnvCandidates(name)) {
|
|
118
|
+
const value = env?.[candidate];
|
|
119
|
+
if (typeof value === "string") return value;
|
|
120
|
+
}
|
|
121
|
+
return void 0;
|
|
122
|
+
}
|
|
123
|
+
function resolveHomeDir() {
|
|
124
|
+
return readEnvVar("HOME") || os.homedir();
|
|
125
|
+
}
|
|
126
|
+
function cloneEnv() {
|
|
127
|
+
return { ...getEnvMap() ?? {} };
|
|
128
|
+
}
|
|
129
|
+
function mergeEnv(overrides) {
|
|
130
|
+
const merged = cloneEnv();
|
|
131
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
132
|
+
if (typeof value === "string") merged[key] = value;
|
|
133
|
+
else delete merged[key];
|
|
134
|
+
}
|
|
135
|
+
return merged;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ../remnic-core/src/resolve-provider-secret.ts
|
|
139
|
+
import path from "path";
|
|
140
|
+
import os2 from "os";
|
|
141
|
+
var _resolveApiKeyForProvider = null;
|
|
142
|
+
var _resolverLoaded = false;
|
|
143
|
+
var _resolverNextRetryAt = 0;
|
|
144
|
+
var RESOLVER_RETRY_BACKOFF_MS = 6e4;
|
|
145
|
+
var resolvedCache = /* @__PURE__ */ new Map();
|
|
146
|
+
async function getGatewayResolver() {
|
|
147
|
+
if (_resolverLoaded) {
|
|
148
|
+
return _resolveApiKeyForProvider;
|
|
149
|
+
}
|
|
150
|
+
if (_resolverNextRetryAt > 0 && Date.now() < _resolverNextRetryAt) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const candidates = [
|
|
155
|
+
// Try glob-matching the runtime module name (hash varies per build)
|
|
156
|
+
...await findRuntimeModules()
|
|
157
|
+
];
|
|
158
|
+
const { pathToFileURL } = await import("url");
|
|
159
|
+
for (const candidate of candidates) {
|
|
160
|
+
try {
|
|
161
|
+
const importUrl = pathToFileURL(candidate).href;
|
|
162
|
+
const mod = await import(importUrl);
|
|
163
|
+
if (typeof mod.resolveApiKeyForProvider === "function") {
|
|
164
|
+
_resolveApiKeyForProvider = mod.resolveApiKeyForProvider;
|
|
165
|
+
_resolverLoaded = true;
|
|
166
|
+
log.debug("loaded gateway resolveApiKeyForProvider from runtime module");
|
|
167
|
+
return _resolveApiKeyForProvider;
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
_resolverNextRetryAt = Date.now() + RESOLVER_RETRY_BACKOFF_MS;
|
|
175
|
+
log.debug(`gateway resolveApiKeyForProvider not available \u2014 will retry after ${RESOLVER_RETRY_BACKOFF_MS / 1e3}s`);
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
async function findRuntimeModules() {
|
|
179
|
+
const { readdirSync } = await import("fs");
|
|
180
|
+
const { createRequire } = await import("module");
|
|
181
|
+
const candidates = [];
|
|
182
|
+
const distDirs = [];
|
|
183
|
+
try {
|
|
184
|
+
const req = createRequire(import.meta.url);
|
|
185
|
+
const openclawMain = req.resolve("openclaw");
|
|
186
|
+
const openclawDist = path.join(path.dirname(openclawMain), "..", "dist");
|
|
187
|
+
if (openclawDist) distDirs.push(path.resolve(openclawDist));
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const { realpathSync } = await import("fs");
|
|
192
|
+
const mainScript = process.argv[1];
|
|
193
|
+
if (mainScript) {
|
|
194
|
+
const realScript = realpathSync(mainScript);
|
|
195
|
+
if (realScript.includes("openclaw")) {
|
|
196
|
+
const distDir = path.dirname(realScript);
|
|
197
|
+
if (!distDirs.includes(distDir)) distDirs.push(distDir);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
for (const dir of distDirs) {
|
|
203
|
+
try {
|
|
204
|
+
const files = readdirSync(dir);
|
|
205
|
+
for (const f of files) {
|
|
206
|
+
if (f.startsWith("runtime-model-auth.runtime-") && f.endsWith(".js")) {
|
|
207
|
+
candidates.push(path.join(dir, f));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return candidates;
|
|
214
|
+
}
|
|
215
|
+
async function resolveProviderApiKey(providerId, apiKeyValue, gatewayConfig, agentDir) {
|
|
216
|
+
const cacheKey = `provider:${providerId}`;
|
|
217
|
+
if (resolvedCache.has(cacheKey)) {
|
|
218
|
+
return resolvedCache.get(cacheKey);
|
|
219
|
+
}
|
|
220
|
+
let resolved;
|
|
221
|
+
if (typeof apiKeyValue === "string" && apiKeyValue.trim().length > 0) {
|
|
222
|
+
if (apiKeyValue === "secretref-managed" || apiKeyValue.endsWith("-oauth") || apiKeyValue.endsWith("-local") || apiKeyValue === "lm-studio" || apiKeyValue.startsWith("gcp-")) {
|
|
223
|
+
} else {
|
|
224
|
+
resolved = apiKeyValue;
|
|
225
|
+
resolvedCache.set(cacheKey, resolved);
|
|
226
|
+
return resolved;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const resolver = await getGatewayResolver();
|
|
230
|
+
if (resolver) {
|
|
231
|
+
try {
|
|
232
|
+
const resolvedAgentDir = agentDir ?? path.join(os2.homedir(), ".openclaw", "agents", "main", "agent");
|
|
233
|
+
const auth = await resolver({ provider: providerId, cfg: gatewayConfig, agentDir: resolvedAgentDir });
|
|
234
|
+
if (auth?.apiKey) {
|
|
235
|
+
resolved = auth.apiKey;
|
|
236
|
+
log.debug(`resolved API key for provider "${providerId}" via gateway auth (source: ${auth.source ?? "unknown"}, mode: ${auth.mode ?? "unknown"})`);
|
|
237
|
+
resolvedCache.set(cacheKey, resolved);
|
|
238
|
+
return resolved;
|
|
239
|
+
}
|
|
240
|
+
} catch (err) {
|
|
241
|
+
log.debug(
|
|
242
|
+
`gateway auth resolution failed for provider "${providerId}": ${err instanceof Error ? err.message : String(err)}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
resolved = resolveFromEnv(providerId);
|
|
247
|
+
if (resolved) {
|
|
248
|
+
log.debug(`resolved API key for provider "${providerId}" from environment variable`);
|
|
249
|
+
} else {
|
|
250
|
+
log.debug(`could not resolve API key for provider "${providerId}" \u2014 skipping`);
|
|
251
|
+
}
|
|
252
|
+
if (resolved) {
|
|
253
|
+
resolvedCache.set(cacheKey, resolved);
|
|
254
|
+
}
|
|
255
|
+
return resolved;
|
|
256
|
+
}
|
|
257
|
+
function resolveFromEnv(providerId) {
|
|
258
|
+
const normalized = providerId.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
259
|
+
const candidates = [
|
|
260
|
+
`${normalized}_API_KEY`,
|
|
261
|
+
`${normalized}_TOKEN`
|
|
262
|
+
];
|
|
263
|
+
for (const envVar of candidates) {
|
|
264
|
+
const value = readEnvVar(envVar);
|
|
265
|
+
if (value && value.trim().length > 0) {
|
|
266
|
+
return value.trim();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return void 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ../remnic-core/src/fallback-llm.ts
|
|
273
|
+
var FallbackLlmClient = class {
|
|
274
|
+
gatewayConfig;
|
|
275
|
+
constructor(gatewayConfig) {
|
|
276
|
+
this.gatewayConfig = gatewayConfig;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Check if fallback is available (gateway config has at least one model).
|
|
280
|
+
*/
|
|
281
|
+
isAvailable(agentId) {
|
|
282
|
+
const models = this.getModelChain(agentId);
|
|
283
|
+
return models.length > 0;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Make a chat completion request using the gateway's default AI chain.
|
|
287
|
+
* Tries primary first, then each fallback in order.
|
|
288
|
+
* When agentId is provided, uses that agent persona's model chain instead of defaults.
|
|
289
|
+
*/
|
|
290
|
+
async chatCompletion(messages, options = {}) {
|
|
291
|
+
const models = this.getModelChain(options.agentId);
|
|
292
|
+
if (models.length === 0) {
|
|
293
|
+
log.warn("fallback LLM: no models configured in gateway");
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const runChain = async () => {
|
|
297
|
+
for (let i = 0; i < models.length; i++) {
|
|
298
|
+
const model = models[i];
|
|
299
|
+
const isFallback = i > 0;
|
|
300
|
+
try {
|
|
301
|
+
const result = await this.tryModel(model, messages, options);
|
|
302
|
+
if (result) {
|
|
303
|
+
if (isFallback) {
|
|
304
|
+
log.debug(`fallback LLM: succeeded using ${model.modelString} (fallback ${i})`);
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
content: result.content,
|
|
308
|
+
modelUsed: model.modelString,
|
|
309
|
+
usage: result.usage
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
} catch (err) {
|
|
313
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
314
|
+
log.debug(`fallback LLM: ${model.modelString} failed (${errorMsg}), trying next...`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
log.warn(`fallback LLM: all ${models.length} models in chain failed`);
|
|
318
|
+
return null;
|
|
319
|
+
};
|
|
320
|
+
if (typeof options.timeoutMs === "number") {
|
|
321
|
+
if (options.timeoutMs <= 0) {
|
|
322
|
+
log.warn("fallback LLM: timed out before request started");
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
let timeoutHandle;
|
|
326
|
+
try {
|
|
327
|
+
return await Promise.race([
|
|
328
|
+
runChain(),
|
|
329
|
+
new Promise((resolve) => {
|
|
330
|
+
timeoutHandle = setTimeout(() => {
|
|
331
|
+
log.warn(`fallback LLM: timed out after ${options.timeoutMs}ms`);
|
|
332
|
+
resolve(null);
|
|
333
|
+
}, options.timeoutMs);
|
|
334
|
+
})
|
|
335
|
+
]);
|
|
336
|
+
} finally {
|
|
337
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return await runChain();
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Make a request with structured output (Zod schema).
|
|
344
|
+
* Returns parsed JSON or null on failure.
|
|
345
|
+
*/
|
|
346
|
+
async parseWithSchema(messages, schema, options = {}) {
|
|
347
|
+
const detailed = await this.parseWithSchemaDetailed(messages, schema, options);
|
|
348
|
+
return detailed?.result ?? null;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Like parseWithSchema but also returns the model that was used,
|
|
352
|
+
* so callers can emit accurate trace events.
|
|
353
|
+
*/
|
|
354
|
+
async parseWithSchemaDetailed(messages, schema, options = {}) {
|
|
355
|
+
const response = await this.chatCompletion(messages, options);
|
|
356
|
+
if (!response?.content) return null;
|
|
357
|
+
try {
|
|
358
|
+
const candidates = extractJsonCandidates(response.content);
|
|
359
|
+
for (const c of candidates) {
|
|
360
|
+
try {
|
|
361
|
+
const parsed = JSON.parse(c);
|
|
362
|
+
return { result: schema.parse(parsed), modelUsed: response.modelUsed };
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
} catch (err) {
|
|
368
|
+
log.warn("fallback LLM: failed to parse structured output:", err);
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Get the full model chain from gateway config.
|
|
374
|
+
* Returns array of models in order: [primary, fallback1, fallback2, ...]
|
|
375
|
+
*
|
|
376
|
+
* When agentId is provided, looks up the matching entry in agents.list[]
|
|
377
|
+
* and uses that persona's model chain. Falls back to agents.defaults.model
|
|
378
|
+
* if agentId is not found or not provided.
|
|
379
|
+
*/
|
|
380
|
+
getModelChain(agentId) {
|
|
381
|
+
const chain = [];
|
|
382
|
+
const providers = this.gatewayConfig?.models?.providers;
|
|
383
|
+
if (!providers) return chain;
|
|
384
|
+
let modelConfig;
|
|
385
|
+
if (agentId) {
|
|
386
|
+
const persona = this.gatewayConfig?.agents?.list?.find(
|
|
387
|
+
(a) => a.id === agentId
|
|
388
|
+
);
|
|
389
|
+
if (persona?.model) {
|
|
390
|
+
modelConfig = persona.model;
|
|
391
|
+
log.debug(`fallback LLM: using agent persona "${agentId}" model chain`);
|
|
392
|
+
} else {
|
|
393
|
+
log.warn(
|
|
394
|
+
`fallback LLM: agent persona "${agentId}" not found or has no model config, falling back to defaults`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (!modelConfig) {
|
|
399
|
+
modelConfig = this.gatewayConfig?.agents?.defaults?.model;
|
|
400
|
+
}
|
|
401
|
+
const modelStrings = [];
|
|
402
|
+
if (modelConfig?.primary) {
|
|
403
|
+
modelStrings.push(modelConfig.primary);
|
|
404
|
+
}
|
|
405
|
+
if (Array.isArray(modelConfig?.fallbacks)) {
|
|
406
|
+
for (const fb of modelConfig.fallbacks) {
|
|
407
|
+
if (typeof fb === "string" && !modelStrings.includes(fb)) {
|
|
408
|
+
modelStrings.push(fb);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
for (const modelString of modelStrings) {
|
|
413
|
+
const modelRef = this.parseModelString(modelString, providers);
|
|
414
|
+
if (modelRef) {
|
|
415
|
+
chain.push(modelRef);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return chain;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Parse a "provider/model" string and look up its config.
|
|
422
|
+
*/
|
|
423
|
+
parseModelString(modelString, providers) {
|
|
424
|
+
const parts = modelString.split("/");
|
|
425
|
+
if (parts.length < 2) {
|
|
426
|
+
log.warn(`fallback LLM: invalid model format: ${modelString}`);
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
const providerId = parts[0];
|
|
430
|
+
const modelId = parts.slice(1).join("/");
|
|
431
|
+
const providerConfig = providers[providerId];
|
|
432
|
+
if (!providerConfig) {
|
|
433
|
+
log.warn(`fallback LLM: provider not found: ${providerId}`);
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
return { providerId, modelId, providerConfig, modelString };
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Resolve the API key for a provider, handling OpenClaw secret ref formats.
|
|
440
|
+
* Results are cached per provider so exec calls only happen once.
|
|
441
|
+
*/
|
|
442
|
+
async resolveApiKey(providerId, providerConfig) {
|
|
443
|
+
return resolveProviderApiKey(providerId, providerConfig.apiKey, this.gatewayConfig);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Try to call a single model.
|
|
447
|
+
*/
|
|
448
|
+
async tryModel(model, messages, options) {
|
|
449
|
+
const rawKey = model.providerConfig.apiKey;
|
|
450
|
+
const needsResolution = rawKey === "secretref-managed" || typeof rawKey === "object" && rawKey !== null;
|
|
451
|
+
const resolvedApiKey = await this.resolveApiKey(model.providerId, model.providerConfig);
|
|
452
|
+
if (needsResolution && !resolvedApiKey) {
|
|
453
|
+
throw new Error(`API key for provider "${model.providerId}" could not be resolved from secret ref`);
|
|
454
|
+
}
|
|
455
|
+
const configWithResolvedKey = resolvedApiKey ? { ...model.providerConfig, apiKey: resolvedApiKey } : model.providerConfig;
|
|
456
|
+
switch (model.providerConfig.api) {
|
|
457
|
+
case "anthropic-messages":
|
|
458
|
+
return await this.callAnthropic(configWithResolvedKey, model.modelId, messages, options);
|
|
459
|
+
case "openai-completions":
|
|
460
|
+
default:
|
|
461
|
+
return await this.callOpenAI(
|
|
462
|
+
configWithResolvedKey,
|
|
463
|
+
model.modelId,
|
|
464
|
+
messages,
|
|
465
|
+
options,
|
|
466
|
+
shouldAssumeOpenAiChatCompletions(model.providerConfig.baseUrl)
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Call OpenAI-compatible API.
|
|
472
|
+
*/
|
|
473
|
+
async callOpenAI(config, modelId, messages, options, assumeOpenAI) {
|
|
474
|
+
const base = config.baseUrl.replace(/\/$/, "");
|
|
475
|
+
const url = base.endsWith("/v1") ? `${base}/chat/completions` : `${base}/v1/chat/completions`;
|
|
476
|
+
const headers = {
|
|
477
|
+
"Content-Type": "application/json",
|
|
478
|
+
...config.headers
|
|
479
|
+
};
|
|
480
|
+
if (config.apiKey && typeof config.apiKey === "string") {
|
|
481
|
+
if (config.authHeader !== false) {
|
|
482
|
+
headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const body = {
|
|
486
|
+
model: modelId,
|
|
487
|
+
messages,
|
|
488
|
+
temperature: options.temperature ?? 0.3,
|
|
489
|
+
...buildChatCompletionTokenLimit(modelId, options.maxTokens ?? 4096, {
|
|
490
|
+
assumeOpenAI
|
|
491
|
+
})
|
|
492
|
+
};
|
|
493
|
+
const response = await fetch(url, {
|
|
494
|
+
method: "POST",
|
|
495
|
+
headers,
|
|
496
|
+
body: JSON.stringify(body)
|
|
497
|
+
});
|
|
498
|
+
if (!response.ok) {
|
|
499
|
+
const error = await response.text();
|
|
500
|
+
throw new Error(`OpenAI API error: ${response.status} ${error}`);
|
|
501
|
+
}
|
|
502
|
+
const data = await response.json();
|
|
503
|
+
const content = data.choices?.[0]?.message?.content;
|
|
504
|
+
if (!content) {
|
|
505
|
+
throw new Error("Empty response from OpenAI API");
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
content,
|
|
509
|
+
usage: data.usage ? {
|
|
510
|
+
inputTokens: data.usage.prompt_tokens,
|
|
511
|
+
outputTokens: data.usage.completion_tokens,
|
|
512
|
+
totalTokens: data.usage.total_tokens
|
|
513
|
+
} : void 0
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Call Anthropic Messages API.
|
|
518
|
+
*/
|
|
519
|
+
async callAnthropic(config, modelId, messages, options) {
|
|
520
|
+
const url = `${config.baseUrl.replace(/\/$/, "")}/messages`;
|
|
521
|
+
const headers = {
|
|
522
|
+
"Content-Type": "application/json",
|
|
523
|
+
"anthropic-version": "2023-06-01",
|
|
524
|
+
...config.headers
|
|
525
|
+
};
|
|
526
|
+
if (config.apiKey && typeof config.apiKey === "string") {
|
|
527
|
+
headers["x-api-key"] = config.apiKey;
|
|
528
|
+
}
|
|
529
|
+
const systemMessage = messages.find((m) => m.role === "system")?.content;
|
|
530
|
+
const nonSystemMessages = messages.filter((m) => m.role !== "system");
|
|
531
|
+
const anthropicMessages = nonSystemMessages.map((m) => ({
|
|
532
|
+
role: m.role,
|
|
533
|
+
content: m.content
|
|
534
|
+
}));
|
|
535
|
+
const body = {
|
|
536
|
+
model: modelId,
|
|
537
|
+
messages: anthropicMessages,
|
|
538
|
+
max_tokens: options.maxTokens ?? 4096,
|
|
539
|
+
temperature: options.temperature ?? 0.3
|
|
540
|
+
};
|
|
541
|
+
if (systemMessage) {
|
|
542
|
+
body.system = systemMessage;
|
|
543
|
+
}
|
|
544
|
+
const response = await fetch(url, {
|
|
545
|
+
method: "POST",
|
|
546
|
+
headers,
|
|
547
|
+
body: JSON.stringify(body)
|
|
548
|
+
});
|
|
549
|
+
if (!response.ok) {
|
|
550
|
+
const error = await response.text();
|
|
551
|
+
throw new Error(`Anthropic API error: ${response.status} ${error}`);
|
|
552
|
+
}
|
|
553
|
+
const data = await response.json();
|
|
554
|
+
const content = data.content?.[0]?.text;
|
|
555
|
+
if (!content) {
|
|
556
|
+
throw new Error("Empty response from Anthropic API");
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
content,
|
|
560
|
+
usage: data.usage ? {
|
|
561
|
+
inputTokens: data.usage.input_tokens,
|
|
562
|
+
outputTokens: data.usage.output_tokens,
|
|
563
|
+
totalTokens: (data.usage.input_tokens ?? 0) + (data.usage.output_tokens ?? 0)
|
|
564
|
+
} : void 0
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
export {
|
|
570
|
+
readEnvVar,
|
|
571
|
+
resolveHomeDir,
|
|
572
|
+
mergeEnv,
|
|
573
|
+
extractJsonCandidates,
|
|
574
|
+
shouldAssumeOpenAiChatCompletions,
|
|
575
|
+
buildChatCompletionTokenLimit,
|
|
576
|
+
FallbackLlmClient
|
|
577
|
+
};
|