openclaw-abacusai-auth 1.2.1 → 1.2.3

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;
@@ -388,7 +421,10 @@ function cleanSchema(schema: unknown): unknown {
388
421
  }
389
422
 
390
423
  /**
391
- * Strip `strict` field from tools and clean schemas - RouteLLM doesn't support them
424
+ * Strip `strict` field from tools, clean schemas, and promote name/parameters
425
+ * to the top level of each tool object for RouteLLM compatibility.
426
+ * RouteLLM expects `name` and `parameters` accessible at the tool level,
427
+ * not only nested under `function`.
392
428
  */
393
429
  function normalizeToolsForRouteLLM(tools: unknown[]): unknown[] {
394
430
  return tools.map((t) => {
@@ -408,12 +444,107 @@ function normalizeToolsForRouteLLM(tools: unknown[]): unknown[] {
408
444
  }
409
445
 
410
446
  copy.function = fn;
447
+
448
+ // Promote name and parameters to top level for RouteLLM
449
+ if (fn.name && !copy.name) {
450
+ copy.name = fn.name;
451
+ }
452
+ if (fn.parameters !== undefined && copy.parameters === undefined) {
453
+ copy.parameters = fn.parameters;
454
+ }
455
+ if (fn.description && !copy.description) {
456
+ copy.description = fn.description;
457
+ }
411
458
  }
412
459
 
413
460
  return copy;
414
461
  });
415
462
  }
416
463
 
464
+ /**
465
+ * Normalize tool_calls in request messages for RouteLLM.
466
+ * RouteLLM expects `name` and `parameters` at the top level of each tool_call,
467
+ * but OpenClaw sends them nested under `function` (OpenAI standard format).
468
+ */
469
+ function normalizeMessagesForRouteLLM(messages: unknown[]): unknown[] {
470
+ return messages.map((msg) => {
471
+ if (!msg || typeof msg !== "object") return msg;
472
+ const m = msg as Record<string, unknown>;
473
+ if (m.role !== "assistant" || !Array.isArray(m.tool_calls)) return msg;
474
+
475
+ const normalized = { ...m };
476
+ normalized.tool_calls = (m.tool_calls as unknown[]).map((tc) => {
477
+ if (!tc || typeof tc !== "object") return tc;
478
+ const call = { ...(tc as Record<string, unknown>) };
479
+
480
+ // Extract name from function.name if not already at top level
481
+ if (!call.name && call.function && typeof call.function === "object") {
482
+ const fn = call.function as Record<string, unknown>;
483
+ call.name = fn.name;
484
+ }
485
+
486
+ // Extract parameters from function.arguments if not already at top level
487
+ if (call.parameters === undefined && call.function && typeof call.function === "object") {
488
+ const fn = call.function as Record<string, unknown>;
489
+ const args = fn.arguments;
490
+ if (typeof args === "string") {
491
+ try { call.parameters = JSON.parse(args); } catch { call.parameters = args; }
492
+ } else if (args !== undefined) {
493
+ call.parameters = args;
494
+ }
495
+ }
496
+
497
+ return call;
498
+ });
499
+ return normalized;
500
+ });
501
+ }
502
+
503
+ /**
504
+ * Normalize a single tool_call from RouteLLM response to OpenAI standard format.
505
+ * RouteLLM may return tool_calls with flat `name`/`parameters` instead of nested `function`.
506
+ */
507
+ function normalizeResponseToolCall(tc: Record<string, unknown>): Record<string, unknown> {
508
+ // Already in standard format
509
+ if (tc.function && typeof tc.function === "object") {
510
+ return tc;
511
+ }
512
+ // Flat format: { name, parameters/arguments } → { function: { name, arguments } }
513
+ if (tc.name) {
514
+ const args = tc.arguments ?? tc.parameters ?? "{}";
515
+ const argsStr = typeof args === "string" ? args : JSON.stringify(args);
516
+ return {
517
+ id: tc.id ?? tc.call_id ?? `call_${crypto.randomUUID()}`,
518
+ type: "function",
519
+ function: { name: tc.name, arguments: argsStr },
520
+ };
521
+ }
522
+ return tc;
523
+ }
524
+
525
+ /**
526
+ * Normalize tool_calls in a parsed response JSON (non-streaming).
527
+ */
528
+ function normalizeResponseToolCalls(json: Record<string, unknown>): Record<string, unknown> {
529
+ if (!Array.isArray(json.choices)) return json;
530
+ json.choices = (json.choices as unknown[]).map((c) => {
531
+ if (!c || typeof c !== "object") return c;
532
+ const choice = { ...(c as Record<string, unknown>) };
533
+ const message = choice.message;
534
+ if (message && typeof message === "object") {
535
+ const msg = { ...(message as Record<string, unknown>) };
536
+ if (Array.isArray(msg.tool_calls)) {
537
+ msg.tool_calls = (msg.tool_calls as unknown[]).map((tc) =>
538
+ tc && typeof tc === "object" ? normalizeResponseToolCall(tc as Record<string, unknown>) : tc
539
+ );
540
+ }
541
+ choice.message = msg;
542
+ }
543
+ return choice;
544
+ });
545
+ return json;
546
+ }
547
+
417
548
  async function handleProxyRequest(req: IncomingMessage, res: ServerResponse) {
418
549
  const path = req.url ?? "/";
419
550
  const target = `${ROUTELLM_BASE}${path}`;
@@ -431,6 +562,10 @@ async function handleProxyRequest(req: IncomingMessage, res: ServerResponse) {
431
562
  if (Array.isArray(parsed.tools)) {
432
563
  parsed.tools = normalizeToolsForRouteLLM(parsed.tools);
433
564
  }
565
+ // Normalize tool_calls in messages: add top-level name/parameters for RouteLLM
566
+ if (Array.isArray(parsed.messages)) {
567
+ parsed.messages = normalizeMessagesForRouteLLM(parsed.messages);
568
+ }
434
569
  body = JSON.stringify(parsed);
435
570
  }
436
571
 
@@ -476,7 +611,7 @@ async function handleProxyRequest(req: IncomingMessage, res: ServerResponse) {
476
611
  let buffer = "";
477
612
 
478
613
  const pump = async () => {
479
- for (;;) {
614
+ for (; ;) {
480
615
  const { done, value } = await reader.read();
481
616
  if (done) {
482
617
  // Process any remaining buffer
@@ -506,17 +641,19 @@ async function handleProxyRequest(req: IncomingMessage, res: ServerResponse) {
506
641
  await release();
507
642
  });
508
643
  } else {
509
- // Non-streaming response - add id and object fields
644
+ // Non-streaming response - add id and object fields, normalize tool_calls
510
645
  const data = await upstream.text();
511
646
  await release();
512
647
  try {
513
- const json = JSON.parse(data) as Record<string, unknown>;
648
+ let json = JSON.parse(data) as Record<string, unknown>;
514
649
  if (!("id" in json)) {
515
650
  json.id = chunkId;
516
651
  }
517
652
  if (!("object" in json)) {
518
653
  json.object = "chat.completion";
519
654
  }
655
+ // Normalize tool_calls to standard OpenAI format
656
+ json = normalizeResponseToolCalls(json);
520
657
  res.writeHead(upstream.status, { "Content-Type": "application/json" });
521
658
  res.end(JSON.stringify(json));
522
659
  } catch {
@@ -604,7 +741,7 @@ interface PluginAuthContext {
604
741
  }
605
742
 
606
743
  const abacusaiPlugin = {
607
- id: "openclaw-abacusai-auth",
744
+ id: "abacusai-auth",
608
745
  name: "AbacusAI Auth",
609
746
  description: "AbacusAI RouteLLM provider plugin with direct connection and schema normalization",
610
747
  configSchema: emptyPluginConfigSchema(),
@@ -750,12 +887,6 @@ const abacusaiPlugin = {
750
887
  api: "openai-completions",
751
888
  auth: "token",
752
889
  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
890
  },
760
891
  },
761
892
  },
@@ -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.3",
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
+ }