openclaw-abacusai-auth 1.2.1 → 1.2.2

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/index.ts CHANGED
@@ -290,7 +290,8 @@ function generateChunkId(): string {
290
290
  }
291
291
 
292
292
  /**
293
- * Normalize SSE chunk to add missing id and object fields
293
+ * Normalize SSE chunk to add missing id and object fields,
294
+ * and normalize any tool_calls in delta to standard OpenAI format.
294
295
  */
295
296
  function normalizeSseChunk(line: string, chunkId: string): string {
296
297
  if (!line.startsWith("data: ") || line === "data: [DONE]") {
@@ -304,6 +305,38 @@ function normalizeSseChunk(line: string, chunkId: string): string {
304
305
  if (!("object" in json)) {
305
306
  json.object = "chat.completion.chunk";
306
307
  }
308
+ // Normalize tool_calls in streaming delta
309
+ if (Array.isArray(json.choices)) {
310
+ json.choices = (json.choices as unknown[]).map((c) => {
311
+ if (!c || typeof c !== "object") return c;
312
+ const choice = c as Record<string, unknown>;
313
+ const delta = choice.delta;
314
+ if (delta && typeof delta === "object") {
315
+ const d = delta as Record<string, unknown>;
316
+ if (Array.isArray(d.tool_calls)) {
317
+ d.tool_calls = (d.tool_calls as unknown[]).map((tc) => {
318
+ if (!tc || typeof tc !== "object") return tc;
319
+ const call = tc as Record<string, unknown>;
320
+ // If flat format (name at top level, no function), convert
321
+ if (call.name && !call.function) {
322
+ const args = call.arguments ?? call.parameters ?? "";
323
+ return {
324
+ index: call.index,
325
+ id: call.id ?? call.call_id ?? `call_${crypto.randomUUID()}`,
326
+ type: "function",
327
+ function: {
328
+ name: call.name,
329
+ arguments: typeof args === "string" ? args : JSON.stringify(args),
330
+ },
331
+ };
332
+ }
333
+ return call;
334
+ });
335
+ }
336
+ }
337
+ return choice;
338
+ });
339
+ }
307
340
  return `data: ${JSON.stringify(json)}`;
308
341
  } catch {
309
342
  return line;
@@ -414,6 +447,90 @@ function normalizeToolsForRouteLLM(tools: unknown[]): unknown[] {
414
447
  });
415
448
  }
416
449
 
450
+ /**
451
+ * Normalize tool_calls in request messages for RouteLLM.
452
+ * RouteLLM expects `name` and `parameters` at the top level of each tool_call,
453
+ * but OpenClaw sends them nested under `function` (OpenAI standard format).
454
+ */
455
+ function normalizeMessagesForRouteLLM(messages: unknown[]): unknown[] {
456
+ return messages.map((msg) => {
457
+ if (!msg || typeof msg !== "object") return msg;
458
+ const m = msg as Record<string, unknown>;
459
+ if (m.role !== "assistant" || !Array.isArray(m.tool_calls)) return msg;
460
+
461
+ const normalized = { ...m };
462
+ normalized.tool_calls = (m.tool_calls as unknown[]).map((tc) => {
463
+ if (!tc || typeof tc !== "object") return tc;
464
+ const call = { ...(tc as Record<string, unknown>) };
465
+
466
+ // Extract name from function.name if not already at top level
467
+ if (!call.name && call.function && typeof call.function === "object") {
468
+ const fn = call.function as Record<string, unknown>;
469
+ call.name = fn.name;
470
+ }
471
+
472
+ // Extract parameters from function.arguments if not already at top level
473
+ if (call.parameters === undefined && call.function && typeof call.function === "object") {
474
+ const fn = call.function as Record<string, unknown>;
475
+ const args = fn.arguments;
476
+ if (typeof args === "string") {
477
+ try { call.parameters = JSON.parse(args); } catch { call.parameters = args; }
478
+ } else if (args !== undefined) {
479
+ call.parameters = args;
480
+ }
481
+ }
482
+
483
+ return call;
484
+ });
485
+ return normalized;
486
+ });
487
+ }
488
+
489
+ /**
490
+ * Normalize a single tool_call from RouteLLM response to OpenAI standard format.
491
+ * RouteLLM may return tool_calls with flat `name`/`parameters` instead of nested `function`.
492
+ */
493
+ function normalizeResponseToolCall(tc: Record<string, unknown>): Record<string, unknown> {
494
+ // Already in standard format
495
+ if (tc.function && typeof tc.function === "object") {
496
+ return tc;
497
+ }
498
+ // Flat format: { name, parameters/arguments } → { function: { name, arguments } }
499
+ if (tc.name) {
500
+ const args = tc.arguments ?? tc.parameters ?? "{}";
501
+ const argsStr = typeof args === "string" ? args : JSON.stringify(args);
502
+ return {
503
+ id: tc.id ?? tc.call_id ?? `call_${crypto.randomUUID()}`,
504
+ type: "function",
505
+ function: { name: tc.name, arguments: argsStr },
506
+ };
507
+ }
508
+ return tc;
509
+ }
510
+
511
+ /**
512
+ * Normalize tool_calls in a parsed response JSON (non-streaming).
513
+ */
514
+ function normalizeResponseToolCalls(json: Record<string, unknown>): Record<string, unknown> {
515
+ if (!Array.isArray(json.choices)) return json;
516
+ json.choices = (json.choices as unknown[]).map((c) => {
517
+ if (!c || typeof c !== "object") return c;
518
+ const choice = { ...(c as Record<string, unknown>) };
519
+ const message = choice.message;
520
+ if (message && typeof message === "object") {
521
+ const msg = { ...(message as Record<string, unknown>) };
522
+ if (Array.isArray(msg.tool_calls)) {
523
+ msg.tool_calls = (msg.tool_calls as unknown[]).map((tc) =>
524
+ tc && typeof tc === "object" ? normalizeResponseToolCall(tc as Record<string, unknown>) : tc
525
+ );
526
+ }
527
+ choice.message = msg;
528
+ }
529
+ return choice;
530
+ });
531
+ return json;
532
+ }
533
+
417
534
  async function handleProxyRequest(req: IncomingMessage, res: ServerResponse) {
418
535
  const path = req.url ?? "/";
419
536
  const target = `${ROUTELLM_BASE}${path}`;
@@ -431,6 +548,10 @@ async function handleProxyRequest(req: IncomingMessage, res: ServerResponse) {
431
548
  if (Array.isArray(parsed.tools)) {
432
549
  parsed.tools = normalizeToolsForRouteLLM(parsed.tools);
433
550
  }
551
+ // Normalize tool_calls in messages: add top-level name/parameters for RouteLLM
552
+ if (Array.isArray(parsed.messages)) {
553
+ parsed.messages = normalizeMessagesForRouteLLM(parsed.messages);
554
+ }
434
555
  body = JSON.stringify(parsed);
435
556
  }
436
557
 
@@ -476,7 +597,7 @@ async function handleProxyRequest(req: IncomingMessage, res: ServerResponse) {
476
597
  let buffer = "";
477
598
 
478
599
  const pump = async () => {
479
- for (;;) {
600
+ for (; ;) {
480
601
  const { done, value } = await reader.read();
481
602
  if (done) {
482
603
  // Process any remaining buffer
@@ -506,17 +627,19 @@ async function handleProxyRequest(req: IncomingMessage, res: ServerResponse) {
506
627
  await release();
507
628
  });
508
629
  } else {
509
- // Non-streaming response - add id and object fields
630
+ // Non-streaming response - add id and object fields, normalize tool_calls
510
631
  const data = await upstream.text();
511
632
  await release();
512
633
  try {
513
- const json = JSON.parse(data) as Record<string, unknown>;
634
+ let json = JSON.parse(data) as Record<string, unknown>;
514
635
  if (!("id" in json)) {
515
636
  json.id = chunkId;
516
637
  }
517
638
  if (!("object" in json)) {
518
639
  json.object = "chat.completion";
519
640
  }
641
+ // Normalize tool_calls to standard OpenAI format
642
+ json = normalizeResponseToolCalls(json);
520
643
  res.writeHead(upstream.status, { "Content-Type": "application/json" });
521
644
  res.end(JSON.stringify(json));
522
645
  } catch {
@@ -604,7 +727,7 @@ interface PluginAuthContext {
604
727
  }
605
728
 
606
729
  const abacusaiPlugin = {
607
- id: "openclaw-abacusai-auth",
730
+ id: "abacusai-auth",
608
731
  name: "AbacusAI Auth",
609
732
  description: "AbacusAI RouteLLM provider plugin with direct connection and schema normalization",
610
733
  configSchema: emptyPluginConfigSchema(),
@@ -750,12 +873,6 @@ const abacusaiPlugin = {
750
873
  api: "openai-completions",
751
874
  auth: "token",
752
875
  models: modelIds.map((id) => buildModelDefinition(id)),
753
- // Note: compat options are kept for future OpenClaw core support
754
- // Currently, schema cleaning is done by the local proxy
755
- compat: {
756
- requiresAdditionalPropertiesFalse: true,
757
- supportsStrictMode: false,
758
- },
759
876
  },
760
877
  },
761
878
  },
@@ -1,9 +1,11 @@
1
1
  {
2
- "id": "openclaw-abacusai-auth",
3
- "providers": ["abacusai"],
2
+ "id": "abacusai-auth",
3
+ "providers": [
4
+ "abacusai"
5
+ ],
4
6
  "configSchema": {
5
7
  "type": "object",
6
8
  "additionalProperties": false,
7
9
  "properties": {}
8
10
  }
9
- }
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-abacusai-auth",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "OpenClaw AbacusAI provider plugin - Third-party plugin for AbacusAI RouteLLM integration",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -26,4 +26,4 @@
26
26
  "./index.ts"
27
27
  ]
28
28
  }
29
- }
29
+ }