@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.
- package/dist/index.js +369 -305
- 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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
666
|
+
function resolveLlmCaller(options, fallback = "llm") {
|
|
591
667
|
return options.caller ?? options.jsonSchemaName ?? fallback;
|
|
592
668
|
}
|
|
593
|
-
function
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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:
|
|
924
|
+
body: requestRaw
|
|
942
925
|
});
|
|
926
|
+
const responseRaw = await res.text().catch(() => "");
|
|
927
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
943
928
|
if (!res.ok) {
|
|
944
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
973
|
-
|
|
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["
|
|
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:
|
|
1536
|
+
body: requestRaw
|
|
1513
1537
|
});
|
|
1538
|
+
const responseRaw = await res.text().catch(() => "");
|
|
1539
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
1514
1540
|
if (!res.ok) {
|
|
1515
|
-
|
|
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
|
|
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
|
|
1659
|
-
const
|
|
1683
|
+
let system2;
|
|
1684
|
+
const msgs2 = [];
|
|
1660
1685
|
for (const m of messages) {
|
|
1661
1686
|
if (m.role === "system") {
|
|
1662
|
-
|
|
1687
|
+
system2 = (system2 ? system2 + `
|
|
1663
1688
|
|
|
1664
1689
|
` : "") + m.content;
|
|
1665
1690
|
continue;
|
|
1666
1691
|
}
|
|
1667
1692
|
if (m.role === "user") {
|
|
1668
|
-
|
|
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
|
-
|
|
1716
|
+
msgs2.push({ role: "assistant", content: blocks });
|
|
1692
1717
|
continue;
|
|
1693
1718
|
}
|
|
1694
1719
|
if (m.role === "tool") {
|
|
1695
|
-
|
|
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:
|
|
1769
|
+
body: requestRaw
|
|
1744
1770
|
});
|
|
1771
|
+
const responseRaw = await res.text().catch(() => "");
|
|
1772
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
1745
1773
|
if (!res.ok) {
|
|
1746
|
-
|
|
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
|
|
1776
|
+
return JSON.parse(responseRaw);
|
|
1750
1777
|
}
|
|
1751
1778
|
async call(model, options) {
|
|
1752
|
-
|
|
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
|
|
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
|
|
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:
|
|
1791
|
-
messages:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
2166
|
+
body: requestRaw
|
|
2149
2167
|
});
|
|
2168
|
+
const responseRaw = await res.text().catch(() => "");
|
|
2169
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
2150
2170
|
if (!res.ok) {
|
|
2151
|
-
|
|
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
|
|
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:
|
|
2297
|
+
body: requestRaw
|
|
2278
2298
|
});
|
|
2299
|
+
const responseRaw = await res.text().catch(() => "");
|
|
2300
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
2279
2301
|
if (!res.ok) {
|
|
2280
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
2428
|
+
body: requestRaw
|
|
2393
2429
|
});
|
|
2430
|
+
const responseRaw = await res.text().catch(() => "");
|
|
2431
|
+
logLlmWire(caller, requestRaw, responseRaw);
|
|
2394
2432
|
if (!res.ok) {
|
|
2395
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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}:
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
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 =
|
|
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 (
|
|
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(`
|
|
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)) {
|