@p-sw/brainbox 0.1.2-alpha.10 → 0.1.2-alpha.12

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 (2) hide show
  1. package/dist/index.js +369 -305
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -489,15 +489,100 @@ function stripThinkTags(content) {
489
489
  }
490
490
  function parseModelJson(content) {
491
491
  const cleaned = stripThinkTags(content);
492
- try {
493
- return JSON.parse(cleaned);
494
- } catch (first) {
495
- const fence = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/i);
496
- if (fence?.[1]) {
497
- return JSON.parse(fence[1].trim());
492
+ const candidates = [cleaned];
493
+ const fence = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/i);
494
+ if (fence?.[1])
495
+ candidates.push(fence[1].trim());
496
+ const extracted = extractJsonSlice(cleaned);
497
+ if (extracted)
498
+ candidates.push(extracted);
499
+ let lastErr;
500
+ for (const candidate of candidates) {
501
+ try {
502
+ return decodeJsonValue(candidate);
503
+ } catch (err) {
504
+ lastErr = err;
505
+ }
506
+ }
507
+ throw lastErr instanceof Error ? lastErr : new Error("Failed to parse model JSON");
508
+ }
509
+ function decodeJsonValue(text) {
510
+ let value = JSON.parse(text);
511
+ for (let i = 0;i < 2 && typeof value === "string"; i++) {
512
+ const inner = value.trim();
513
+ if (!(inner.startsWith("{") && inner.endsWith("}") || inner.startsWith("[") && inner.endsWith("]") || inner.startsWith('"') && inner.endsWith('"'))) {
514
+ break;
515
+ }
516
+ value = JSON.parse(inner);
517
+ }
518
+ return value;
519
+ }
520
+ function extractJsonSlice(text) {
521
+ const start = text.search(/[\{\[]/);
522
+ if (start < 0)
523
+ return;
524
+ const open = text[start];
525
+ const close = open === "{" ? "}" : "]";
526
+ let depth = 0;
527
+ let inString = false;
528
+ let escape = false;
529
+ for (let i = start;i < text.length; i++) {
530
+ const ch = text[i];
531
+ if (inString) {
532
+ if (escape) {
533
+ escape = false;
534
+ } else if (ch === "\\") {
535
+ escape = true;
536
+ } else if (ch === '"') {
537
+ inString = false;
538
+ }
539
+ continue;
498
540
  }
499
- throw first;
541
+ if (ch === '"') {
542
+ inString = true;
543
+ continue;
544
+ }
545
+ if (ch === open)
546
+ depth++;
547
+ else if (ch === close) {
548
+ depth--;
549
+ if (depth === 0)
550
+ return text.slice(start, i + 1);
551
+ }
552
+ }
553
+ return;
554
+ }
555
+ function schemaToolName(name) {
556
+ const cleaned = name.replace(/[^a-zA-Z0-9_]/g, "_").replace(/^(\d)/, "_$1");
557
+ return cleaned.length > 0 ? cleaned : "submit_result";
558
+ }
559
+ function buildStructuredJsonRequest(options) {
560
+ const toolName = schemaToolName(options.jsonSchemaName);
561
+ return {
562
+ toolName,
563
+ tool: {
564
+ name: toolName,
565
+ description: `Submit the structured ${options.jsonSchemaName} result.`,
566
+ parameters: options.jsonSchema ?? {
567
+ type: "object",
568
+ additionalProperties: true
569
+ }
570
+ },
571
+ instruction: `${options.instruction}
572
+
573
+ You MUST call the \`${toolName}\` tool exactly once with the complete answer.
574
+ Do not write the JSON as plain text or inside a markdown code fence.`
575
+ };
576
+ }
577
+ function parseStructuredJsonResult(choice, toolName) {
578
+ const call = choice.message.toolCalls?.find((c) => c.function.name === toolName) ?? choice.message.toolCalls?.[0];
579
+ if (call?.function.arguments) {
580
+ return parseModelJson(call.function.arguments);
500
581
  }
582
+ if (choice.message.content) {
583
+ return parseModelJson(choice.message.content);
584
+ }
585
+ throw new Error("Empty response from model");
501
586
  }
502
587
  function readAuthString(auth, key, envName) {
503
588
  const fromAuth = typeof auth?.[key] === "string" ? auth[key] : undefined;
@@ -550,7 +635,7 @@ class LLMExecutor {
550
635
  });
551
636
  };
552
637
  if (conv.provider === id.provider) {
553
- return withLlmLogging(build(conv.provider));
638
+ return build(conv.provider);
554
639
  }
555
640
  const modelToProvider = {
556
641
  [conv.model]: conv.provider,
@@ -561,7 +646,7 @@ class LLMExecutor {
561
646
  [id.provider]: build(id.provider)
562
647
  };
563
648
  const fallback = instances[conv.provider];
564
- return withLlmLogging(new class extends LLMExecutor {
649
+ return new class extends LLMExecutor {
565
650
  providerName = "dispatch";
566
651
  models = {
567
652
  conversation: conv.model,
@@ -575,126 +660,21 @@ class LLMExecutor {
575
660
  const exec = instances[modelToProvider[model] ?? ""] ?? fallback;
576
661
  return exec.chatWithTools(model, options);
577
662
  }
578
- });
579
- }
580
- }
581
- function formatPayload(value) {
582
- if (typeof value === "string")
583
- return value;
584
- try {
585
- return JSON.stringify(value, null, 2);
586
- } catch {
587
- return String(value);
663
+ };
588
664
  }
589
665
  }
590
- function resolveCaller(options, fallback) {
666
+ function resolveLlmCaller(options, fallback = "llm") {
591
667
  return options.caller ?? options.jsonSchemaName ?? fallback;
592
668
  }
593
- function withLlmLogging(inner) {
594
- return new class extends LLMExecutor {
595
- providerName = inner.providerName;
596
- models = inner.models;
597
- async call(model, options) {
598
- const jsonMode = "jsonSchemaName" in options;
599
- const caller = resolveCaller({
600
- caller: options.caller,
601
- jsonSchemaName: jsonMode ? options.jsonSchemaName : undefined
602
- }, "call");
603
- const requestBody = [
604
- `provider: ${inner.providerName}`,
605
- `model: ${model}`,
606
- `caller: ${caller}`,
607
- `reasoningEffort: ${options.reasoningEffort ?? "-"}`,
608
- `jsonSchemaName: ${jsonMode ? options.jsonSchemaName : "-"}`,
609
- "",
610
- "--- instruction ---",
611
- options.instruction,
612
- "",
613
- "--- message ---",
614
- options.message,
615
- ...jsonMode ? ["", "--- jsonSchema ---", formatPayload(options.jsonSchema)] : []
616
- ].join(`
617
- `);
618
- try {
619
- const result = await inner.call(model, options);
620
- if (isLlmLogEnabled()) {
621
- writeLlmExchange(caller, [
622
- "======== REQUEST ========",
623
- requestBody,
624
- "",
625
- "======== RESPONSE ========",
626
- formatPayload(result),
627
- ""
628
- ].join(`
629
- `));
630
- }
631
- return result;
632
- } catch (err) {
633
- const reason = err instanceof Error ? err.message : String(err);
634
- if (isLlmLogEnabled()) {
635
- writeLlmExchange(caller, [
636
- "======== REQUEST ========",
637
- requestBody,
638
- "",
639
- "======== ERROR ========",
640
- reason,
641
- ""
642
- ].join(`
643
- `));
644
- }
645
- throw err;
646
- }
647
- }
648
- async chatWithTools(model, options) {
649
- const caller = resolveCaller(options, "chat");
650
- const requestBody = [
651
- `provider: ${inner.providerName}`,
652
- `model: ${model}`,
653
- `caller: ${caller}`,
654
- `reasoningEffort: ${options.reasoningEffort ?? "-"}`,
655
- `parallelToolCalls: ${options.parallelToolCalls ?? false}`,
656
- "",
657
- "--- instruction ---",
658
- options.instruction,
659
- "",
660
- "--- messages ---",
661
- formatPayload(options.messages),
662
- "",
663
- "--- tools ---",
664
- formatPayload(options.tools)
665
- ].join(`
669
+ function logLlmWire(caller, request, response) {
670
+ if (!isLlmLogEnabled())
671
+ return;
672
+ writeLlmExchange(caller, `======== REQUEST ========
673
+ ${request}
674
+
675
+ ======== RESPONSE ========
676
+ ${response}
666
677
  `);
667
- try {
668
- const result = await inner.chatWithTools(model, options);
669
- if (isLlmLogEnabled()) {
670
- writeLlmExchange(caller, [
671
- "======== REQUEST ========",
672
- requestBody,
673
- "",
674
- "======== RESPONSE ========",
675
- formatPayload(result),
676
- ""
677
- ].join(`
678
- `));
679
- }
680
- return result;
681
- } catch (err) {
682
- const reason = err instanceof Error ? err.message : String(err);
683
- if (isLlmLogEnabled()) {
684
- writeLlmExchange(caller, [
685
- "======== REQUEST ========",
686
- requestBody,
687
- "",
688
- "======== ERROR ========",
689
- reason,
690
- ""
691
- ].join(`
692
- `));
693
- }
694
- throw err;
695
- }
696
- }
697
- };
698
678
  }
699
679
  function listProviderNames() {
700
680
  return LLMExecutor.listProviderNames();
@@ -764,27 +744,27 @@ class OpenRouterExecutor extends LLMExecutor {
764
744
  async call(model, options) {
765
745
  const jsonMode = "jsonSchemaName" in options;
766
746
  log3.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
767
- const result = await this.client.chat.send({
768
- chatRequest: {
769
- model,
770
- messages: [
771
- { role: "system", content: options.instruction },
772
- { role: "user", content: options.message }
773
- ],
774
- reasoning: {
775
- effort: options.reasoningEffort ?? (model === this.models.identity ? REASONING_EFFORT_MAP.medium : REASONING_EFFORT_MAP.none)
776
- },
777
- responseFormat: jsonMode ? {
778
- type: "json_schema",
779
- jsonSchema: {
780
- name: options.jsonSchemaName,
781
- schema: options.jsonSchema,
782
- strict: true
783
- }
784
- } : { type: "text" },
785
- stream: false
786
- }
787
- });
747
+ const chatRequest = {
748
+ model,
749
+ messages: [
750
+ { role: "system", content: options.instruction },
751
+ { role: "user", content: options.message }
752
+ ],
753
+ reasoning: {
754
+ effort: options.reasoningEffort ?? (model === this.models.identity ? REASONING_EFFORT_MAP.medium : REASONING_EFFORT_MAP.none)
755
+ },
756
+ responseFormat: jsonMode ? {
757
+ type: "json_schema",
758
+ jsonSchema: {
759
+ name: options.jsonSchemaName,
760
+ schema: options.jsonSchema,
761
+ strict: true
762
+ }
763
+ } : { type: "text" },
764
+ stream: false
765
+ };
766
+ const result = await this.client.chat.send({ chatRequest });
767
+ logLlmWire(resolveLlmCaller(options), JSON.stringify(chatRequest), JSON.stringify(result));
788
768
  const raw = result.choices[0]?.message?.content;
789
769
  const content = typeof raw === "string" ? stripThinkTags(raw) : raw;
790
770
  if (!content) {
@@ -796,22 +776,22 @@ class OpenRouterExecutor extends LLMExecutor {
796
776
  }
797
777
  async chatWithTools(model, options) {
798
778
  log3.debug(`chatWithTools: model=${model} msgs=${options.messages.length} tools=${options.tools.length}`);
799
- const result = await this.client.chat.send({
800
- chatRequest: {
801
- model,
802
- messages: [
803
- { role: "system", content: options.instruction },
804
- ...options.messages.map(toOrMessage)
805
- ],
806
- reasoning: {
807
- effort: options.reasoningEffort ?? (model === this.models.identity ? REASONING_EFFORT_MAP.medium : REASONING_EFFORT_MAP.none)
808
- },
809
- responseFormat: { type: "text" },
810
- tools: options.tools.map(toOrTool),
811
- parallelToolCalls: options.parallelToolCalls ?? false,
812
- stream: false
813
- }
814
- });
779
+ const chatRequest = {
780
+ model,
781
+ messages: [
782
+ { role: "system", content: options.instruction },
783
+ ...options.messages.map(toOrMessage)
784
+ ],
785
+ reasoning: {
786
+ effort: options.reasoningEffort ?? (model === this.models.identity ? REASONING_EFFORT_MAP.medium : REASONING_EFFORT_MAP.none)
787
+ },
788
+ responseFormat: { type: "text" },
789
+ tools: options.tools.map(toOrTool),
790
+ parallelToolCalls: options.parallelToolCalls ?? false,
791
+ stream: false
792
+ };
793
+ const result = await this.client.chat.send({ chatRequest });
794
+ logLlmWire(resolveLlmCaller(options), JSON.stringify(chatRequest), JSON.stringify(result));
815
795
  const choice = result.choices[0];
816
796
  if (!choice) {
817
797
  log3.debug(`chatWithTools: no choice in response`);
@@ -886,6 +866,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
886
866
  chatPath;
887
867
  noBearerPrefix;
888
868
  reasoningEffortInQuery;
869
+ supportsResponseFormat;
889
870
  constructor(opts) {
890
871
  super();
891
872
  this.providerName = opts.providerName;
@@ -895,6 +876,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
895
876
  this.chatPath = opts.chatPath ?? "/chat/completions";
896
877
  this.noBearerPrefix = opts.noBearerPrefix ?? false;
897
878
  this.reasoningEffortInQuery = opts.reasoningEffortInQuery ?? false;
879
+ this.supportsResponseFormat = opts.supportsResponseFormat ?? true;
898
880
  this.models = {
899
881
  conversation: opts.conversationModel,
900
882
  identity: opts.identityModel
@@ -925,7 +907,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
925
907
  }
926
908
  return url.toString();
927
909
  }
928
- async sendRequest(body, reasoningEffort) {
910
+ async sendRequest(body, reasoningEffort, caller) {
929
911
  const modelName = body["model"];
930
912
  const modelStr = typeof modelName === "string" ? modelName : "";
931
913
  const url = this.buildRequestUrl(modelStr, reasoningEffort);
@@ -935,24 +917,40 @@ class OpenAICompatibleExecutor extends LLMExecutor {
935
917
  ...authHeader,
936
918
  ...this.defaultHeaders
937
919
  };
920
+ const requestRaw = JSON.stringify(body);
938
921
  const res = await fetch(url, {
939
922
  method: "POST",
940
923
  headers,
941
- body: JSON.stringify(body)
924
+ body: requestRaw
942
925
  });
926
+ const responseRaw = await res.text().catch(() => "");
927
+ logLlmWire(caller, requestRaw, responseRaw);
943
928
  if (!res.ok) {
944
- const text = await res.text().catch(() => "");
945
- log4.error(`${this.providerName}: HTTP ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
929
+ log4.error(`${this.providerName}: HTTP ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
946
930
  throw new Error(`${this.providerName} request failed: ${res.status} ${res.statusText}`);
947
931
  }
948
- const data = await res.json();
932
+ let data;
933
+ try {
934
+ data = JSON.parse(responseRaw);
935
+ } catch {
936
+ throw new Error(`${this.providerName}: invalid JSON response`);
937
+ }
949
938
  if (data.error) {
950
939
  log4.error(`${this.providerName}: API error ${data.error.type ?? ""} ${data.error.message ?? ""}`);
951
940
  throw new Error(`${this.providerName} API error: ${data.error.message ?? "unknown"}`);
952
941
  }
942
+ const baseCode = data.base_resp?.status_code;
943
+ if (typeof baseCode === "number" && baseCode !== 0) {
944
+ const msg = data.base_resp?.status_msg?.trim() || `status_code ${baseCode}`;
945
+ log4.error(`${this.providerName}: base_resp ${baseCode} ${msg}`);
946
+ throw new Error(`${this.providerName} API error: ${msg}`);
947
+ }
953
948
  return data;
954
949
  }
955
950
  async call(model, options) {
951
+ if ("jsonSchemaName" in options && !this.supportsResponseFormat) {
952
+ return this.callJsonViaTool(model, options);
953
+ }
956
954
  const jsonMode = "jsonSchemaName" in options;
957
955
  const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
958
956
  log4.debug(`call: provider=${this.providerName} model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
@@ -965,16 +963,32 @@ class OpenAICompatibleExecutor extends LLMExecutor {
965
963
  responseFormat: jsonMode ? buildResponseFormat(options.jsonSchemaName, options.jsonSchema) : undefined,
966
964
  reasoningEffort: reasoning
967
965
  });
968
- const data = await this.sendRequest(body, options.reasoningEffort);
969
- const raw = data.choices?.[0]?.message?.content;
966
+ const data = await this.sendRequest(body, options.reasoningEffort, resolveLlmCaller(options));
967
+ const choice = data.choices?.[0];
968
+ const raw = choice?.message?.content;
970
969
  const content = typeof raw === "string" ? stripThinkTags(raw) : raw;
971
970
  if (!content) {
972
- log4.debug(`call: empty content in choice 0`);
973
- throw new Error("Empty response from model");
971
+ const finish = choice?.finish_reason ?? "no-choice";
972
+ const reasoningLen = typeof choice?.message?.reasoning_content === "string" ? choice.message.reasoning_content.length : 0;
973
+ log4.debug(`call: empty content in choice 0 finish_reason=${finish} reasoning_len=${reasoningLen} rawType=${raw === null ? "null" : typeof raw}`);
974
+ throw new Error(reasoningLen > 0 ? `Empty response from model (finish_reason=${finish}; reasoning present but no content)` : "Empty response from model");
974
975
  }
975
976
  log4.debug(`call: response ${content.length} chars`);
976
977
  return jsonMode ? parseModelJson(content) : content;
977
978
  }
979
+ async callJsonViaTool(model, options) {
980
+ const { toolName, tool, instruction } = buildStructuredJsonRequest(options);
981
+ log4.debug(`callJsonViaTool: provider=${this.providerName} model=${model} tool=${toolName}`);
982
+ const choice = await this.chatWithTools(model, {
983
+ caller: options.caller ?? options.jsonSchemaName,
984
+ instruction,
985
+ messages: [{ role: "user", content: options.message }],
986
+ tools: [tool],
987
+ reasoningEffort: "none",
988
+ parallelToolCalls: false
989
+ });
990
+ return parseStructuredJsonResult(choice, toolName);
991
+ }
978
992
  async chatWithTools(model, options) {
979
993
  const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
980
994
  log4.debug(`chatWithTools: provider=${this.providerName} model=${model} msgs=${options.messages.length} tools=${options.tools.length}`);
@@ -988,7 +1002,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
988
1002
  parallelToolCalls: options.parallelToolCalls ?? false,
989
1003
  reasoningEffort: reasoning
990
1004
  });
991
- const data = await this.sendRequest(body, options.reasoningEffort);
1005
+ const data = await this.sendRequest(body, options.reasoningEffort, resolveLlmCaller(options));
992
1006
  const choice = data.choices?.[0];
993
1007
  if (!choice) {
994
1008
  log4.debug(`chatWithTools: no choice in response`);
@@ -1290,10 +1304,15 @@ class ZenMuxExecutor extends OpenAICompatibleExecutor {
1290
1304
  // src/provider/providers/minimax.ts
1291
1305
  class MiniMaxBase extends OpenAICompatibleExecutor {
1292
1306
  buildBody(opts) {
1307
+ const jsonMode = opts.responseFormat !== undefined;
1293
1308
  const body = super.buildBody(opts);
1294
1309
  delete body["reasoning_effort"];
1310
+ delete body["response_format"];
1311
+ delete body["parallel_tool_calls"];
1295
1312
  body["reasoning_split"] = true;
1296
- body["thinking"] = opts.reasoningEffort && opts.reasoningEffort !== "none" ? { type: "adaptive" } : { type: "disabled" };
1313
+ body["max_completion_tokens"] = 16384;
1314
+ const wantThink = !jsonMode && opts.reasoningEffort !== undefined && opts.reasoningEffort !== "none";
1315
+ body["thinking"] = wantThink ? { type: "adaptive" } : { type: "disabled" };
1297
1316
  return body;
1298
1317
  }
1299
1318
  }
@@ -1303,7 +1322,8 @@ function minimaxOpts(providerName, baseURL, opts) {
1303
1322
  baseURL,
1304
1323
  apiKey: opts.apiKey,
1305
1324
  conversationModel: opts.conversationModel,
1306
- identityModel: opts.identityModel
1325
+ identityModel: opts.identityModel,
1326
+ supportsResponseFormat: false
1307
1327
  };
1308
1328
  }
1309
1329
 
@@ -1379,7 +1399,8 @@ class LmStudioExecutor extends OpenAICompatibleExecutor {
1379
1399
  baseURL: "http://127.0.0.1:1234/v1",
1380
1400
  apiKey: opts.apiKey || "lm-studio",
1381
1401
  conversationModel: opts.conversationModel,
1382
- identityModel: opts.identityModel
1402
+ identityModel: opts.identityModel,
1403
+ supportsResponseFormat: false
1383
1404
  });
1384
1405
  }
1385
1406
  }
@@ -1392,7 +1413,8 @@ class OllamaExecutor extends OpenAICompatibleExecutor {
1392
1413
  baseURL: "http://127.0.0.1:11434/v1",
1393
1414
  apiKey: opts.apiKey || "ollama",
1394
1415
  conversationModel: opts.conversationModel,
1395
- identityModel: opts.identityModel
1416
+ identityModel: opts.identityModel,
1417
+ supportsResponseFormat: false
1396
1418
  });
1397
1419
  }
1398
1420
  }
@@ -1418,7 +1440,8 @@ class LlamaCppExecutor extends OpenAICompatibleExecutor {
1418
1440
  baseURL: "http://127.0.0.1:8080/v1",
1419
1441
  apiKey: opts.apiKey || "llama.cpp",
1420
1442
  conversationModel: opts.conversationModel,
1421
- identityModel: opts.identityModel
1443
+ identityModel: opts.identityModel,
1444
+ supportsResponseFormat: false
1422
1445
  });
1423
1446
  }
1424
1447
  }
@@ -1501,21 +1524,23 @@ class CloudflareWorkersExecutor extends LLMExecutor {
1501
1524
  const accountId = readAuthString(opts.auth, "accountId", "CLOUDFLARE_ACCOUNT_ID");
1502
1525
  this.baseURL = accountId ? `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run` : "https://api.cloudflare.com/client/v4/accounts/__cf_account_id__/ai/run";
1503
1526
  }
1504
- async run(model, body) {
1527
+ async run(model, body, caller) {
1505
1528
  const url = `${this.baseURL}/${encodeURIComponent(model)}`;
1529
+ const requestRaw = JSON.stringify(body);
1506
1530
  const res = await fetch(url, {
1507
1531
  method: "POST",
1508
1532
  headers: {
1509
1533
  Authorization: `Bearer ${this.apiKey}`,
1510
1534
  "Content-Type": "application/json"
1511
1535
  },
1512
- body: JSON.stringify(body)
1536
+ body: requestRaw
1513
1537
  });
1538
+ const responseRaw = await res.text().catch(() => "");
1539
+ logLlmWire(caller, requestRaw, responseRaw);
1514
1540
  if (!res.ok) {
1515
- const text = await res.text().catch(() => "");
1516
- throw new Error(`cloudflare-workers request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
1541
+ throw new Error(`cloudflare-workers request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
1517
1542
  }
1518
- return await res.json();
1543
+ return JSON.parse(responseRaw);
1519
1544
  }
1520
1545
  async call(model, options) {
1521
1546
  const jsonMode = "jsonSchemaName" in options;
@@ -1536,7 +1561,7 @@ class CloudflareWorkersExecutor extends LLMExecutor {
1536
1561
  if (reasoning !== "none") {
1537
1562
  body["reasoning_effort"] = reasoning;
1538
1563
  }
1539
- const data = await this.run(model, body);
1564
+ const data = await this.run(model, body, resolveLlmCaller(options));
1540
1565
  if (data.errors && data.errors.length > 0) {
1541
1566
  throw new Error(`cloudflare-workers API error: ${data.errors.map((e) => e.message).join("; ")}`);
1542
1567
  }
@@ -1565,7 +1590,7 @@ class CloudflareWorkersExecutor extends LLMExecutor {
1565
1590
  if (reasoning !== "none") {
1566
1591
  body["reasoning_effort"] = reasoning;
1567
1592
  }
1568
- const data = await this.run(model, body);
1593
+ const data = await this.run(model, body, resolveLlmCaller(options));
1569
1594
  if (data.errors && data.errors.length > 0) {
1570
1595
  throw new Error(`cloudflare-workers API error: ${data.errors.map((e) => e.message).join("; ")}`);
1571
1596
  }
@@ -1655,17 +1680,17 @@ var AnthropicAuthSchema = z4.object({
1655
1680
  apiVersion: z4.string().optional()
1656
1681
  }).loose();
1657
1682
  function toAnthropicMessages(messages) {
1658
- let system;
1659
- const msgs = [];
1683
+ let system2;
1684
+ const msgs2 = [];
1660
1685
  for (const m of messages) {
1661
1686
  if (m.role === "system") {
1662
- system = (system ? system + `
1687
+ system2 = (system2 ? system2 + `
1663
1688
 
1664
1689
  ` : "") + m.content;
1665
1690
  continue;
1666
1691
  }
1667
1692
  if (m.role === "user") {
1668
- msgs.push({ role: "user", content: m.content });
1693
+ msgs2.push({ role: "user", content: m.content });
1669
1694
  continue;
1670
1695
  }
1671
1696
  if (m.role === "assistant") {
@@ -1688,11 +1713,11 @@ function toAnthropicMessages(messages) {
1688
1713
  });
1689
1714
  }
1690
1715
  }
1691
- msgs.push({ role: "assistant", content: blocks });
1716
+ msgs2.push({ role: "assistant", content: blocks });
1692
1717
  continue;
1693
1718
  }
1694
1719
  if (m.role === "tool") {
1695
- msgs.push({
1720
+ msgs2.push({
1696
1721
  role: "user",
1697
1722
  content: [
1698
1723
  {
@@ -1704,7 +1729,7 @@ function toAnthropicMessages(messages) {
1704
1729
  });
1705
1730
  }
1706
1731
  }
1707
- return { system, msgs };
1732
+ return { system: system2, msgs: msgs2 };
1708
1733
  }
1709
1734
  function toAnthropicTool(t) {
1710
1735
  return {
@@ -1732,7 +1757,8 @@ class AnthropicExecutor extends LLMExecutor {
1732
1757
  this.baseURL = (extra.baseURL ?? readAuthString(opts.auth, "baseURL", "ANTHROPIC_BASE_URL") ?? "https://api.anthropic.com").replace(/\/v1\/?$/, "");
1733
1758
  this.apiVersion = extra.apiVersion ?? "2023-06-01";
1734
1759
  }
1735
- async send(body) {
1760
+ async send(body, caller) {
1761
+ const requestRaw = JSON.stringify(body);
1736
1762
  const res = await fetch(`${this.baseURL}/v1/messages`, {
1737
1763
  method: "POST",
1738
1764
  headers: {
@@ -1740,19 +1766,35 @@ class AnthropicExecutor extends LLMExecutor {
1740
1766
  "anthropic-version": this.apiVersion,
1741
1767
  "Content-Type": "application/json"
1742
1768
  },
1743
- body: JSON.stringify(body)
1769
+ body: requestRaw
1744
1770
  });
1771
+ const responseRaw = await res.text().catch(() => "");
1772
+ logLlmWire(caller, requestRaw, responseRaw);
1745
1773
  if (!res.ok) {
1746
- const text = await res.text().catch(() => "");
1747
- throw new Error(`anthropic request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
1774
+ throw new Error(`anthropic request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
1748
1775
  }
1749
- return await res.json();
1776
+ return JSON.parse(responseRaw);
1750
1777
  }
1751
1778
  async call(model, options) {
1752
- const jsonMode = "jsonSchemaName" in options;
1779
+ if ("jsonSchemaName" in options) {
1780
+ const { toolName, tool, instruction } = buildStructuredJsonRequest({
1781
+ instruction: options.instruction,
1782
+ jsonSchemaName: options.jsonSchemaName,
1783
+ jsonSchema: options.jsonSchema
1784
+ });
1785
+ const choice = await this.chatWithTools(model, {
1786
+ caller: options.caller ?? options.jsonSchemaName,
1787
+ instruction,
1788
+ messages: [{ role: "user", content: options.message }],
1789
+ tools: [tool],
1790
+ reasoningEffort: "none",
1791
+ toolChoice: { type: "tool", name: toolName }
1792
+ });
1793
+ return parseStructuredJsonResult(choice, toolName);
1794
+ }
1753
1795
  const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
1754
1796
  const log5 = logger.child("llm:anthropic");
1755
- log5.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
1797
+ log5.debug(`call: model=${model} jsonSchema=- msgLen=${options.message.length}`);
1756
1798
  const outputCap = 4096;
1757
1799
  const body = {
1758
1800
  model,
@@ -1768,7 +1810,7 @@ class AnthropicExecutor extends LLMExecutor {
1768
1810
  budget_tokens: budget
1769
1811
  };
1770
1812
  }
1771
- const data = await this.send(body);
1813
+ const data = await this.send(body, resolveLlmCaller(options));
1772
1814
  if (data.error) {
1773
1815
  throw new Error(`anthropic API error: ${data.error.message ?? "unknown"}`);
1774
1816
  }
@@ -1776,21 +1818,27 @@ class AnthropicExecutor extends LLMExecutor {
1776
1818
  if (!text) {
1777
1819
  throw new Error("Empty response from model");
1778
1820
  }
1779
- return jsonMode ? parseModelJson(text) : text;
1821
+ return text;
1780
1822
  }
1781
1823
  async chatWithTools(model, options) {
1782
1824
  const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
1783
1825
  const log5 = logger.child("llm:anthropic");
1784
1826
  log5.debug(`chatWithTools: model=${model} msgs=${options.messages.length} tools=${options.tools.length}`);
1785
- const { system, msgs } = toAnthropicMessages(options.messages);
1827
+ const { system: system2, msgs: msgs2 } = toAnthropicMessages(options.messages);
1786
1828
  const outputCap = 4096;
1787
1829
  const body = {
1788
1830
  model,
1789
1831
  max_tokens: outputCap,
1790
- system: system ?? options.instruction,
1791
- messages: msgs,
1832
+ system: system2 ?? options.instruction,
1833
+ messages: msgs2,
1792
1834
  tools: options.tools.map(toAnthropicTool)
1793
1835
  };
1836
+ if (options.toolChoice) {
1837
+ body["tool_choice"] = {
1838
+ type: "tool",
1839
+ name: options.toolChoice.name
1840
+ };
1841
+ }
1794
1842
  if (reasoning !== "none") {
1795
1843
  const budget = REASONING_BUDGET[reasoning];
1796
1844
  body["max_tokens"] = budget + outputCap;
@@ -1799,7 +1847,7 @@ class AnthropicExecutor extends LLMExecutor {
1799
1847
  budget_tokens: budget
1800
1848
  };
1801
1849
  }
1802
- const data = await this.send(body);
1850
+ const data = await this.send(body, resolveLlmCaller(options));
1803
1851
  if (data.error) {
1804
1852
  throw new Error(`anthropic API error: ${data.error.message ?? "unknown"}`);
1805
1853
  }
@@ -1869,58 +1917,6 @@ function signRequest(opts) {
1869
1917
  headers["X-Amz-Security-Token"] = opts.sessionToken;
1870
1918
  return headers;
1871
1919
  }
1872
- function toAnthropicMessages2(messages) {
1873
- let system;
1874
- const msgs = [];
1875
- for (const m of messages) {
1876
- if (m.role === "system") {
1877
- system = (system ? system + `
1878
-
1879
- ` : "") + m.content;
1880
- continue;
1881
- }
1882
- if (m.role === "user") {
1883
- msgs.push({ role: "user", content: [{ type: "text", text: m.content }] });
1884
- continue;
1885
- }
1886
- if (m.role === "assistant") {
1887
- const blocks = [];
1888
- if (m.content)
1889
- blocks.push({ type: "text", text: m.content });
1890
- if (m.toolCalls) {
1891
- for (const c of m.toolCalls) {
1892
- let input = {};
1893
- try {
1894
- input = c.function.arguments ? JSON.parse(c.function.arguments) : {};
1895
- } catch {
1896
- input = {};
1897
- }
1898
- blocks.push({
1899
- type: "tool_use",
1900
- id: c.id,
1901
- name: c.function.name,
1902
- input
1903
- });
1904
- }
1905
- }
1906
- msgs.push({ role: "assistant", content: blocks });
1907
- continue;
1908
- }
1909
- if (m.role === "tool") {
1910
- msgs.push({
1911
- role: "user",
1912
- content: [
1913
- {
1914
- type: "tool_result",
1915
- tool_use_id: m.toolCallId,
1916
- content: [{ type: "text", text: m.content }]
1917
- }
1918
- ]
1919
- });
1920
- }
1921
- }
1922
- return { system, msgs };
1923
- }
1924
1920
  function toAnthropicTool2(t) {
1925
1921
  return {
1926
1922
  name: t.name,
@@ -1962,7 +1958,7 @@ class BedrockExecutor extends LLMExecutor {
1962
1958
  this.sessionToken = readAuthString(opts.auth, "sessionToken", "AWS_SESSION_TOKEN");
1963
1959
  this.baseURL = `https://bedrock-runtime.${this.region}.amazonaws.com`;
1964
1960
  }
1965
- async invoke(model, body) {
1961
+ async invoke(model, body, caller) {
1966
1962
  const bodyStr = JSON.stringify(body);
1967
1963
  const path = `/model/${encodeURIComponent(model)}/invoke`;
1968
1964
  const headers = signRequest({
@@ -1981,16 +1977,32 @@ class BedrockExecutor extends LLMExecutor {
1981
1977
  headers,
1982
1978
  body: bodyStr
1983
1979
  });
1980
+ const responseRaw = await res.text().catch(() => "");
1981
+ logLlmWire(caller, bodyStr, responseRaw);
1984
1982
  if (!res.ok) {
1985
- const text = await res.text().catch(() => "");
1986
- throw new Error(`bedrock request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
1983
+ throw new Error(`bedrock request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
1987
1984
  }
1988
- return await res.json();
1985
+ return JSON.parse(responseRaw);
1989
1986
  }
1990
1987
  async call(model, options) {
1991
- const jsonMode = "jsonSchemaName" in options;
1992
1988
  const log5 = logger.child("llm:bedrock");
1993
- log5.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
1989
+ if ("jsonSchemaName" in options) {
1990
+ log5.debug(`call: model=${model} jsonSchema=${options.jsonSchemaName} via tool`);
1991
+ const { toolName, tool, instruction } = buildStructuredJsonRequest({
1992
+ instruction: options.instruction,
1993
+ jsonSchemaName: options.jsonSchemaName,
1994
+ jsonSchema: options.jsonSchema
1995
+ });
1996
+ const choice = await this.chatWithTools(model, {
1997
+ caller: options.caller ?? options.jsonSchemaName,
1998
+ instruction,
1999
+ messages: [{ role: "user", content: options.message }],
2000
+ tools: [tool],
2001
+ toolChoice: { type: "tool", name: toolName }
2002
+ });
2003
+ return parseStructuredJsonResult(choice, toolName);
2004
+ }
2005
+ log5.debug(`call: model=${model} jsonSchema=- msgLen=${options.message.length}`);
1994
2006
  if (!model.startsWith("anthropic.") && !model.startsWith("us.anthropic.")) {
1995
2007
  throw new Error(`bedrock provider currently only supports Anthropic models (got ${model})`);
1996
2008
  }
@@ -2000,12 +2012,12 @@ class BedrockExecutor extends LLMExecutor {
2000
2012
  system: options.instruction,
2001
2013
  messages: [{ role: "user", content: options.message }]
2002
2014
  };
2003
- const data = await this.invoke(model, body);
2015
+ const data = await this.invoke(model, body, resolveLlmCaller(options));
2004
2016
  const { text } = extractAnthropicContent(data);
2005
2017
  if (!text) {
2006
2018
  throw new Error("Empty response from model");
2007
2019
  }
2008
- return jsonMode ? parseModelJson(text) : text;
2020
+ return text;
2009
2021
  }
2010
2022
  async chatWithTools(model, options) {
2011
2023
  const log5 = logger.child("llm:bedrock");
@@ -2013,7 +2025,6 @@ class BedrockExecutor extends LLMExecutor {
2013
2025
  if (!model.startsWith("anthropic.") && !model.startsWith("us.anthropic.")) {
2014
2026
  throw new Error(`bedrock provider currently only supports Anthropic models (got ${model})`);
2015
2027
  }
2016
- const { system, msgs } = toAnthropicMessages2(options.messages);
2017
2028
  const body = {
2018
2029
  anthropic_version: "bedrock-2023-05-31",
2019
2030
  max_tokens: 4096,
@@ -2021,7 +2032,13 @@ class BedrockExecutor extends LLMExecutor {
2021
2032
  messages: msgs,
2022
2033
  tools: options.tools.map(toAnthropicTool2)
2023
2034
  };
2024
- const data = await this.invoke(model, body);
2035
+ if (options.toolChoice) {
2036
+ body.tool_choice = {
2037
+ type: "tool",
2038
+ name: options.toolChoice.name
2039
+ };
2040
+ }
2041
+ const data = await this.invoke(model, body, resolveLlmCaller(options));
2025
2042
  const { text, toolCalls } = extractAnthropicContent(data);
2026
2043
  return { message: { content: text || undefined, toolCalls } };
2027
2044
  }
@@ -2137,21 +2154,23 @@ class VertexExecutor extends LLMExecutor {
2137
2154
  this.region = extra.region ?? readAuthString(opts.auth, "region", "GOOGLE_CLOUD_REGION") ?? "us-central1";
2138
2155
  this.baseURL = this.project ? `https://${this.region}-aiplatform.googleapis.com/v1/projects/${this.project}/locations/${this.region}/publishers/google/models` : "https://us-central1-aiplatform.googleapis.com/v1";
2139
2156
  }
2140
- async generate(model, body) {
2157
+ async generate(model, body, caller) {
2141
2158
  const url = `${this.baseURL}/${encodeURIComponent(model)}:generateContent`;
2159
+ const requestRaw = JSON.stringify(body);
2142
2160
  const res = await fetch(url, {
2143
2161
  method: "POST",
2144
2162
  headers: {
2145
2163
  Authorization: `Bearer ${this.apiKey}`,
2146
2164
  "Content-Type": "application/json"
2147
2165
  },
2148
- body: JSON.stringify(body)
2166
+ body: requestRaw
2149
2167
  });
2168
+ const responseRaw = await res.text().catch(() => "");
2169
+ logLlmWire(caller, requestRaw, responseRaw);
2150
2170
  if (!res.ok) {
2151
- const text = await res.text().catch(() => "");
2152
- throw new Error(`vertex request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
2171
+ throw new Error(`vertex request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
2153
2172
  }
2154
- return await res.json();
2173
+ return JSON.parse(responseRaw);
2155
2174
  }
2156
2175
  async call(model, options) {
2157
2176
  const jsonMode = "jsonSchemaName" in options;
@@ -2172,7 +2191,7 @@ class VertexExecutor extends LLMExecutor {
2172
2191
  responseSchema: options.jsonSchema
2173
2192
  };
2174
2193
  }
2175
- const data = await this.generate(model, body);
2194
+ const data = await this.generate(model, body, resolveLlmCaller(options));
2176
2195
  if (data.error) {
2177
2196
  throw new Error(`vertex API error: ${data.error.message ?? "unknown"}`);
2178
2197
  }
@@ -2191,7 +2210,7 @@ class VertexExecutor extends LLMExecutor {
2191
2210
  systemInstruction: { parts: [{ text: options.instruction }] },
2192
2211
  tools: options.tools.length > 0 ? [toGeminiTools(options.tools)] : undefined
2193
2212
  };
2194
- const data = await this.generate(model, body);
2213
+ const data = await this.generate(model, body, resolveLlmCaller(options));
2195
2214
  if (data.error) {
2196
2215
  throw new Error(`vertex API error: ${data.error.message ?? "unknown"}`);
2197
2216
  }
@@ -2267,25 +2286,41 @@ class GitLabDuoExecutor extends LLMExecutor {
2267
2286
  const extra = parsed.success ? parsed.data : {};
2268
2287
  this.baseURL = (extra.baseURL ?? readAuthString(opts.auth, "baseURL", "GITLAB_BASE_URL") ?? "https://gitlab.com/api/v4/ai/llm/proxy").replace(/\/+$/, "");
2269
2288
  }
2270
- async send(body) {
2289
+ async send(body, caller) {
2290
+ const requestRaw = JSON.stringify(body);
2271
2291
  const res = await fetch(this.baseURL, {
2272
2292
  method: "POST",
2273
2293
  headers: {
2274
2294
  Authorization: `Bearer ${this.apiKey}`,
2275
2295
  "Content-Type": "application/json"
2276
2296
  },
2277
- body: JSON.stringify(body)
2297
+ body: requestRaw
2278
2298
  });
2299
+ const responseRaw = await res.text().catch(() => "");
2300
+ logLlmWire(caller, requestRaw, responseRaw);
2279
2301
  if (!res.ok) {
2280
- const text = await res.text().catch(() => "");
2281
- throw new Error(`gitlab-duo request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
2302
+ throw new Error(`gitlab-duo request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
2282
2303
  }
2283
- return await res.json();
2304
+ return JSON.parse(responseRaw);
2284
2305
  }
2285
2306
  async call(model, options) {
2286
- const jsonMode = "jsonSchemaName" in options;
2287
2307
  const log5 = logger.child("llm:gitlab-duo");
2288
- log5.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
2308
+ if ("jsonSchemaName" in options) {
2309
+ log5.debug(`call: model=${model} jsonSchema=${options.jsonSchemaName} via tool`);
2310
+ const { toolName, tool, instruction } = buildStructuredJsonRequest({
2311
+ instruction: options.instruction,
2312
+ jsonSchemaName: options.jsonSchemaName,
2313
+ jsonSchema: options.jsonSchema
2314
+ });
2315
+ const choice = await this.chatWithTools(model, {
2316
+ caller: options.caller ?? options.jsonSchemaName,
2317
+ instruction,
2318
+ messages: [{ role: "user", content: options.message }],
2319
+ tools: [tool]
2320
+ });
2321
+ return parseStructuredJsonResult(choice, toolName);
2322
+ }
2323
+ log5.debug(`call: model=${model} jsonSchema=- msgLen=${options.message.length}`);
2289
2324
  const body = {
2290
2325
  model,
2291
2326
  messages: [
@@ -2293,7 +2328,7 @@ class GitLabDuoExecutor extends LLMExecutor {
2293
2328
  { role: "user", content: options.message }
2294
2329
  ]
2295
2330
  };
2296
- const data = await this.send(body);
2331
+ const data = await this.send(body, resolveLlmCaller(options));
2297
2332
  if (data.error) {
2298
2333
  throw new Error(`gitlab-duo API error: ${data.error.message ?? "unknown"}`);
2299
2334
  }
@@ -2301,7 +2336,7 @@ class GitLabDuoExecutor extends LLMExecutor {
2301
2336
  if (!content) {
2302
2337
  throw new Error("Empty response from model");
2303
2338
  }
2304
- return jsonMode ? parseModelJson(content) : content;
2339
+ return content;
2305
2340
  }
2306
2341
  async chatWithTools(model, options) {
2307
2342
  const log5 = logger.child("llm:gitlab-duo");
@@ -2314,7 +2349,7 @@ class GitLabDuoExecutor extends LLMExecutor {
2314
2349
  ],
2315
2350
  tools: options.tools.map(toTool)
2316
2351
  };
2317
- const data = await this.send(body);
2352
+ const data = await this.send(body, resolveLlmCaller(options));
2318
2353
  if (data.error) {
2319
2354
  throw new Error(`gitlab-duo API error: ${data.error.message ?? "unknown"}`);
2320
2355
  }
@@ -2382,26 +2417,43 @@ class SnowflakeCortexExecutor extends LLMExecutor {
2382
2417
  const account = extra.account ?? readAuthString(opts.auth, "account", "SNOWFLAKE_ACCOUNT") ?? "";
2383
2418
  this.baseURL = account ? `https://${account}.snowflakecomputing.com/api/v2/cortex/inference:complete` : "https://__account__.snowflakecomputing.com/api/v2/cortex/inference:complete";
2384
2419
  }
2385
- async send(body) {
2420
+ async send(body, caller) {
2421
+ const requestRaw = JSON.stringify(body);
2386
2422
  const res = await fetch(this.baseURL, {
2387
2423
  method: "POST",
2388
2424
  headers: {
2389
2425
  Authorization: `Bearer ${this.apiKey}`,
2390
2426
  "Content-Type": "application/json"
2391
2427
  },
2392
- body: JSON.stringify(body)
2428
+ body: requestRaw
2393
2429
  });
2430
+ const responseRaw = await res.text().catch(() => "");
2431
+ logLlmWire(caller, requestRaw, responseRaw);
2394
2432
  if (!res.ok) {
2395
- const text = await res.text().catch(() => "");
2396
- throw new Error(`snowflake-cortex request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
2433
+ throw new Error(`snowflake-cortex request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
2397
2434
  }
2398
- return await res.json();
2435
+ return JSON.parse(responseRaw);
2399
2436
  }
2400
2437
  async call(model, options) {
2401
- const jsonMode = "jsonSchemaName" in options;
2402
- const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
2403
2438
  const log5 = logger.child("llm:snowflake-cortex");
2404
- log5.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
2439
+ if ("jsonSchemaName" in options) {
2440
+ log5.debug(`call: model=${model} jsonSchema=${options.jsonSchemaName} via tool`);
2441
+ const { toolName, tool, instruction } = buildStructuredJsonRequest({
2442
+ instruction: options.instruction,
2443
+ jsonSchemaName: options.jsonSchemaName,
2444
+ jsonSchema: options.jsonSchema
2445
+ });
2446
+ const choice = await this.chatWithTools(model, {
2447
+ caller: options.caller ?? options.jsonSchemaName,
2448
+ instruction,
2449
+ messages: [{ role: "user", content: options.message }],
2450
+ tools: [tool],
2451
+ reasoningEffort: "none"
2452
+ });
2453
+ return parseStructuredJsonResult(choice, toolName);
2454
+ }
2455
+ const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
2456
+ log5.debug(`call: model=${model} jsonSchema=- msgLen=${options.message.length}`);
2405
2457
  const body = {
2406
2458
  model,
2407
2459
  messages: [
@@ -2412,7 +2464,7 @@ class SnowflakeCortexExecutor extends LLMExecutor {
2412
2464
  if (reasoning !== "none") {
2413
2465
  body["reasoning_effort"] = reasoning;
2414
2466
  }
2415
- const data = await this.send(body);
2467
+ const data = await this.send(body, resolveLlmCaller(options));
2416
2468
  if (data.error) {
2417
2469
  throw new Error(`snowflake-cortex API error: ${data.error.message ?? "unknown"}`);
2418
2470
  }
@@ -2420,7 +2472,7 @@ class SnowflakeCortexExecutor extends LLMExecutor {
2420
2472
  if (!content) {
2421
2473
  throw new Error("Empty response from model");
2422
2474
  }
2423
- return jsonMode ? parseModelJson(content) : content;
2475
+ return content;
2424
2476
  }
2425
2477
  async chatWithTools(model, options) {
2426
2478
  const log5 = logger.child("llm:snowflake-cortex");
@@ -2433,7 +2485,7 @@ class SnowflakeCortexExecutor extends LLMExecutor {
2433
2485
  ],
2434
2486
  tools: options.tools.map(toCortexTool)
2435
2487
  };
2436
- const data = await this.send(body);
2488
+ const data = await this.send(body, resolveLlmCaller(options));
2437
2489
  if (data.error) {
2438
2490
  throw new Error(`snowflake-cortex API error: ${data.error.message ?? "unknown"}`);
2439
2491
  }
@@ -2822,7 +2874,11 @@ class Brain {
2822
2874
  async regenerateSchedules() {
2823
2875
  const today = new Date;
2824
2876
  const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
2825
- await this.createMonthlySchedule(nextMonth);
2877
+ const monthly = await this.createMonthlySchedule(nextMonth);
2878
+ if (!monthly) {
2879
+ log7.debug(`regenerateSchedules: skip daily — monthly schedule generation failed`);
2880
+ return;
2881
+ }
2826
2882
  const tomorrow = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
2827
2883
  await this.createDailySchedule(tomorrow);
2828
2884
  await this.createDailySchedule(today);
@@ -3704,7 +3760,7 @@ class BaseChannel {
3704
3760
  static all() {
3705
3761
  return Array.from(BaseChannel.activeChannels.values());
3706
3762
  }
3707
- static async forceDo(brainId, action) {
3763
+ static forceDo(brainId, action) {
3708
3764
  const channel = BaseChannel.activeChannels.get(brainId);
3709
3765
  if (!channel) {
3710
3766
  return {
@@ -3713,12 +3769,20 @@ class BaseChannel {
3713
3769
  };
3714
3770
  }
3715
3771
  const displayName = channel.brain.brainbase.displayName;
3716
- logger.info(`do ${action}: forcing for "${displayName}" (${brainId})`);
3717
- if (action === "generateSchedule") {
3718
- await channel.regenerateSchedules();
3719
- } else {
3720
- await channel.runSleepMemory(true);
3721
- }
3772
+ logger.info(`do ${action}: queued for "${displayName}" (${brainId})`);
3773
+ (async () => {
3774
+ try {
3775
+ if (action === "generateSchedule") {
3776
+ await channel.regenerateSchedules();
3777
+ } else {
3778
+ await channel.runSleepMemory(true);
3779
+ }
3780
+ logger.success(`do ${action}: done for "${displayName}" (${brainId})`);
3781
+ } catch (error) {
3782
+ const reason = error instanceof Error ? error.message : String(error);
3783
+ logger.error(`do ${action}: failed for "${displayName}" (${brainId}): ${reason}`);
3784
+ }
3785
+ })();
3722
3786
  return { ok: true, displayName };
3723
3787
  }
3724
3788
  static async view(brainId, thing) {
@@ -4303,7 +4367,7 @@ defineCommand({
4303
4367
  if (typeof brainId !== "string" || brainId.trim().length === 0) {
4304
4368
  return { ok: false, error: "missing brainId" };
4305
4369
  }
4306
- const result = await BaseChannel.forceDo(brainId.trim(), action);
4370
+ const result = BaseChannel.forceDo(brainId.trim(), action);
4307
4371
  if (!result.ok)
4308
4372
  return result;
4309
4373
  return {
@@ -4380,8 +4444,8 @@ async function startChannels() {
4380
4444
  async function daemon() {
4381
4445
  const logDir = join5(config.brainboxRoot, "logs");
4382
4446
  logger.configure({ logDir });
4383
- configureLlmLog(join5(logDir, "llm"));
4384
- logger.debug(`daemon: boot (logDir=${logDir}, llmLogDir=${join5(logDir, "llm")})`);
4447
+ configureLlmLog(config.debug ? join5(logDir, "llm") : undefined);
4448
+ logger.debug(`daemon: boot (debug=${config.debug}, logDir=${logDir}, llmLog=${config.debug})`);
4385
4449
  const started = await startChannels();
4386
4450
  if (started === 0) {
4387
4451
  logger.info("No activated brains with channels. Daemon idling.");
@@ -4559,7 +4623,7 @@ async function doAction(action, brainId) {
4559
4623
  args: { action, brainId }
4560
4624
  });
4561
4625
  const name = response.result?.displayName ?? brainId;
4562
- logger.success(`Forced ${action} for "${name}" (${brainId}).`);
4626
+ logger.success(`Successfully sent ${action} for "${name}" (${brainId}).`);
4563
4627
  }
4564
4628
  async function viewThing(thing, brainId) {
4565
4629
  if (!VIEW_THINGS.includes(thing)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@p-sw/brainbox",
3
- "version": "0.1.2-alpha.10",
3
+ "version": "0.1.2-alpha.12",
4
4
  "module": "dist/index.js",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",