@elvatis_com/openclaw-cli-bridge-elvatis 3.5.0 → 3.5.1

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > OpenClaw plugin that bridges locally installed AI CLIs (Codex, Gemini, Claude Code, OpenCode, Pi) as model providers — with slash commands for instant model switching, restore, health testing, and model listing.
4
4
 
5
- **Current version:** `3.5.0`
5
+ **Current version:** `3.5.1`
6
6
 
7
7
  ---
8
8
 
package/SKILL.md CHANGED
@@ -68,4 +68,4 @@ On gateway restart, if any session has expired, a **WhatsApp alert** is sent aut
68
68
 
69
69
  See `README.md` for full configuration reference and architecture diagram.
70
70
 
71
- **Version:** 3.5.0
71
+ **Version:** 3.5.1
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-cli-bridge-elvatis",
3
3
  "slug": "openclaw-cli-bridge-elvatis",
4
4
  "name": "OpenClaw CLI Bridge",
5
- "version": "3.5.0",
5
+ "version": "3.5.1",
6
6
  "license": "MIT",
7
7
  "description": "Phase 1: openai-codex auth bridge. Phase 2: local HTTP proxy routing model calls through gemini/claude CLIs (vllm provider).",
8
8
  "providers": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elvatis_com/openclaw-cli-bridge-elvatis",
3
- "version": "3.5.0",
3
+ "version": "3.5.1",
4
4
  "description": "Bridges gemini, claude, and codex CLI tools as OpenClaw model providers. Reads existing CLI auth without re-login.",
5
5
  "type": "module",
6
6
  "openclaw": {
package/src/cli-runner.ts CHANGED
@@ -1027,9 +1027,13 @@ export async function routeToCliRunner(
1027
1027
  let prompt = formatPrompt(messages, toolCount);
1028
1028
  const hasTools = toolCount > 0;
1029
1029
 
1030
- // Auto-detect project from prompt and set workdir + inject context
1030
+ // Auto-detect project from user messages only (not tool results which mention other projects)
1031
1031
  if (!opts.workdir) {
1032
- const detected = detectProjectFromPrompt(prompt);
1032
+ const userText = messages
1033
+ .filter((m) => m.role === "user")
1034
+ .map((m) => typeof m.content === "string" ? m.content : "")
1035
+ .join(" ");
1036
+ const detected = detectProjectFromPrompt(userText);
1033
1037
  if (detected) {
1034
1038
  opts = { ...opts, workdir: detected.path };
1035
1039
  prompt = `[Context: Working directory is ${detected.path}]\n\n${prompt}`;
@@ -448,6 +448,9 @@ async function handleRequest(
448
448
  const promptPreview = typeof lastUserMsg?.content === "string" ? lastUserMsg.content.slice(0, 80) : "";
449
449
 
450
450
  debugLog("REQ", `${model} start`, { msgs: cleanMessages.length, tools: tools?.length ?? 0, stream, media: mediaFiles.length, promptPreview: promptPreview.slice(0, 60) });
451
+ if (hasTools && tools!.length > 0) {
452
+ debugLog("TOOLS", `${tools!.length} tools available`, { names: tools!.map(t => t.function?.name ?? t.name ?? "?").join(", ") });
453
+ }
451
454
 
452
455
  // Track active request for dashboard
453
456
  activeRequests.set(id, { id, model, startedAt: Date.now(), messageCount: cleanMessages.length, toolCount: tools?.length ?? 0, promptPreview });
@@ -939,7 +942,14 @@ async function handleRequest(
939
942
  opts.warn(`[cli-bridge] ${model} failed (${reason}), trying fallback chain: ${fallbackChain.join(" → ")}`);
940
943
 
941
944
  let chainSuccess = false;
945
+ const lastMsg = cleanMessages[cleanMessages.length - 1];
946
+ const inToolLoop = hasTools && (lastMsg?.role === "tool" || lastMsg?.role === "function");
942
947
  for (const fallbackModel of fallbackChain) {
948
+ // Skip Haiku in tool loops — it consistently returns text instead of tool_calls, wasting ~8-12s
949
+ if (inToolLoop && fallbackModel.includes("haiku")) {
950
+ debugLog("FALLBACK-SKIP", `skipping ${fallbackModel} in tool loop (unreliable for tool_calls)`, {});
951
+ continue;
952
+ }
943
953
  debugLog("FALLBACK", `${model} → ${fallbackModel}`, { reason: isTimeout ? "timeout" : "error", primaryDuration: Math.round(primaryDuration / 1000), chain: fallbackChain });
944
954
  if (sseHeadersSent) {
945
955
  res.write(`: fallback — trying ${fallbackModel}\n\n`);
@@ -952,12 +962,9 @@ async function handleRequest(
952
962
  debugLog("FALLBACK-EMPTY", `${fallbackModel} returned empty`, {});
953
963
  throw new Error(`empty response from ${fallbackModel}`);
954
964
  }
955
- // If tools were requested and the last message was a tool result (gateway expects
956
- // tool continuation), but the fallback model returned text instead of tool_calls —
965
+ // If we're in a tool loop but the fallback returned text instead of tool_calls
957
966
  // it ignored the JSON format. Try next model in chain.
958
- const lastMsg = cleanMessages[cleanMessages.length - 1];
959
- const inToolLoop = lastMsg?.role === "tool" || lastMsg?.role === "function";
960
- if (hasTools && inToolLoop && !result.tool_calls?.length && result.content) {
967
+ if (inToolLoop && !result.tool_calls?.length && result.content) {
961
968
  debugLog("FALLBACK-NO-TOOLS", `${fallbackModel} returned text instead of tool_calls in tool loop`, { contentLen: result.content.length, preview: result.content.slice(0, 80) });
962
969
  throw new Error(`${fallbackModel} returned text instead of tool_calls`);
963
970
  }
@@ -188,6 +188,16 @@ export function parseToolCallResponse(text: string): CliToolResult {
188
188
  }
189
189
  }
190
190
 
191
+ // Last resort: try to rescue tool_calls from anywhere in the text
192
+ // Models sometimes output tool_calls JSON with surrounding text that breaks other strategies
193
+ if (trimmed.includes("tool_calls")) {
194
+ const rescued = tryRescueToolCallsFromContent(trimmed);
195
+ if (rescued) {
196
+ debugLog("PARSE", `rescue-from-raw → tool_calls`, { toolCalls: rescued.tool_calls?.length ?? 0 });
197
+ return rescued;
198
+ }
199
+ }
200
+
191
201
  // Fallback: treat entire text as content
192
202
  debugLog("PARSE", "no JSON found → raw content", { len: trimmed.length, preview });
193
203
  return { content: trimmed || null };
@@ -307,37 +317,49 @@ function tryExtractCodeBlock(text: string): string | null {
307
317
  return match?.[1]?.trim() ?? null;
308
318
  }
309
319
 
310
- /** Find the first { ... } JSON object in text (greedy, balanced braces). */
320
+ /** Find a balanced { ... } JSON object in text. Tries multiple start positions if the first fails to parse. */
311
321
  function tryExtractEmbeddedJson(text: string): string | null {
312
- const start = text.indexOf("{");
313
- if (start === -1) return null;
314
-
315
- let depth = 0;
316
- let inString = false;
317
- let escaped = false;
318
-
319
- for (let i = start; i < text.length; i++) {
320
- const ch = text[i];
321
- if (escaped) {
322
- escaped = false;
323
- continue;
324
- }
325
- if (ch === "\\") {
326
- escaped = true;
327
- continue;
328
- }
329
- if (ch === '"') {
330
- inString = !inString;
331
- continue;
332
- }
333
- if (inString) continue;
334
- if (ch === "{") depth++;
335
- if (ch === "}") {
336
- depth--;
337
- if (depth === 0) {
338
- return text.slice(start, i + 1);
322
+ let searchFrom = 0;
323
+ while (searchFrom < text.length) {
324
+ const start = text.indexOf("{", searchFrom);
325
+ if (start === -1) return null;
326
+
327
+ let depth = 0;
328
+ let inString = false;
329
+ let escaped = false;
330
+
331
+ for (let i = start; i < text.length; i++) {
332
+ const ch = text[i];
333
+ if (escaped) {
334
+ escaped = false;
335
+ continue;
336
+ }
337
+ if (ch === "\\") {
338
+ escaped = true;
339
+ continue;
340
+ }
341
+ if (ch === '"') {
342
+ inString = !inString;
343
+ continue;
344
+ }
345
+ if (inString) continue;
346
+ if (ch === "{") depth++;
347
+ if (ch === "}") {
348
+ depth--;
349
+ if (depth === 0) {
350
+ const candidate = text.slice(start, i + 1);
351
+ // Verify it actually parses as JSON before returning
352
+ try {
353
+ JSON.parse(candidate);
354
+ return candidate;
355
+ } catch {
356
+ // This balanced-brace block isn't valid JSON — try next { in text
357
+ break;
358
+ }
359
+ }
339
360
  }
340
361
  }
362
+ searchFrom = start + 1;
341
363
  }
342
364
  return null;
343
365
  }