@anteros/core 0.0.42 → 0.0.44
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/database/replication.ts +614 -26
- package/lib/agent.ts +279 -39
- package/lib/agentMemory.ts +103 -0
- package/lib/agents.ts +25 -2
- package/lib/define.ts +1 -1
- package/lib/mcpClient.ts +128 -0
- package/lib/providers.ts +169 -66
- package/lib/skills.ts +481 -0
- package/package.json +1 -1
- package/server/agents.ts +27 -6
- package/server/api.ts +74 -1
- package/tests/agent-mcp.test.ts +116 -0
- package/tests/agent-memory.test.ts +6 -6
- package/tests/agent-provider.test.ts +288 -0
- package/tests/agent-runtime.test.ts +369 -0
- package/tests/agent-skills.test.ts +177 -0
- package/tests/agent-summarize.test.ts +124 -0
- package/tests/agent-working-memory.test.ts +5 -3
- package/tests/agent.test.ts +18 -16
- package/tests/agents-api.test.ts +1 -1
- package/tests/api-private-fields.test.ts +196 -0
- package/tests/file-collection-replication.test.ts +2 -0
- package/tests/fixtures/agent-tenant/agents/support.agent.ts +1 -1
- package/tests/fixtures/agent-tenant/agents/weather.agent.ts +1 -1
- package/tests/fixtures/agents-api/agents/assistant.agent.ts +1 -1
- package/tests/fixtures/agents-api/agents/plain.agent.ts +1 -1
- package/tests/fixtures/agents-api/agents/private.agent.ts +1 -1
- package/tests/fixtures/repl-only/app.ts +3 -0
- package/tests/fixtures/replication-meta/agents/chat.agent.ts +1 -1
- package/tests/fixtures/skills-tenant/agents/docs.agent.ts +23 -0
- package/tests/fixtures/skills-tenant/agents/incomplete.agent.ts +14 -0
- package/tests/fixtures/skills-tenant/broken/bad-name.md +6 -0
- package/tests/fixtures/skills-tenant/broken/dup-a.md +6 -0
- package/tests/fixtures/skills-tenant/broken/dup-b.md +6 -0
- package/tests/fixtures/skills-tenant/broken/empty-body.md +4 -0
- package/tests/fixtures/skills-tenant/broken/long-compat.md +7 -0
- package/tests/fixtures/skills-tenant/broken/long-description.md +6 -0
- package/tests/fixtures/skills-tenant/broken/mismatch/SKILL.md +6 -0
- package/tests/fixtures/skills-tenant/broken/no-description.md +5 -0
- package/tests/fixtures/skills-tenant/broken/no-name.md +5 -0
- package/tests/fixtures/skills-tenant/broken/plain.md +4 -0
- package/tests/fixtures/skills-tenant/broken/unknown-key.md +9 -0
- package/tests/fixtures/skills-tenant/docs/pdf-forms.md +18 -0
- package/tests/fixtures/skills-tenant/guide/ml.md +9 -0
- package/tests/fixtures/skills-tenant/guides/pdf/SKILL.md +9 -0
- package/tests/fixtures/skills-tenant/runbooks/incident.md +9 -0
- package/tests/fixtures/skills-tenant/runbooks/notes.txt +2 -0
- package/tests/replication-change-stream.test.ts +240 -0
- package/tests/replication-control.test.ts +2 -0
- package/tests/replication-initial-sync.test.ts +2 -0
- package/tests/replication-meta.test.ts +3 -0
- package/tests/replication.test.ts +2 -0
- package/tests/vars-replication.test.ts +2 -0
- package/types/agent.d.ts +237 -7
- package/types/replication.d.ts +41 -4
package/lib/providers.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AppError } from "./error";
|
|
2
|
+
import { logger } from "../utils/logger";
|
|
2
3
|
import type {
|
|
3
|
-
|
|
4
|
+
AgentProtocol,
|
|
4
5
|
AgentMediaPart,
|
|
5
6
|
AgentMessage,
|
|
6
7
|
AgentProvider,
|
|
@@ -48,6 +49,12 @@ export type ModelChatRequest = {
|
|
|
48
49
|
signal?: AbortSignal;
|
|
49
50
|
/** Ask for a JSON object (OpenAI-compatible providers: `response_format`). */
|
|
50
51
|
json?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Ask for a JSON object **matching this schema** — what the provider can really
|
|
54
|
+
* enforce: `response_format.json_schema` on OpenAI-compatible gateways, a forced
|
|
55
|
+
* tool on Anthropic (which has no such field). Takes precedence over `json`.
|
|
56
|
+
*/
|
|
57
|
+
jsonSchema?: { name: string; description?: string; schema: any };
|
|
51
58
|
};
|
|
52
59
|
|
|
53
60
|
export type ModelChatResponse = {
|
|
@@ -76,18 +83,19 @@ export type ModelStreamEvent =
|
|
|
76
83
|
| { type: "finish"; finishReason: string };
|
|
77
84
|
|
|
78
85
|
export type ModelAdapter = {
|
|
79
|
-
|
|
86
|
+
/** The wire protocol this adapter speaks. */
|
|
87
|
+
protocol: AgentProtocol;
|
|
80
88
|
model: string;
|
|
81
89
|
chat(req: ModelChatRequest): Promise<ModelChatResponse>;
|
|
82
90
|
chatStream(req: ModelChatRequest): AsyncGenerator<ModelStreamEvent, void, unknown>;
|
|
83
91
|
};
|
|
84
92
|
|
|
85
|
-
const DEFAULT_BASE_URL: Record<
|
|
93
|
+
const DEFAULT_BASE_URL: Record<AgentProtocol, string> = {
|
|
86
94
|
openai: "https://api.openai.com/v1",
|
|
87
95
|
anthropic: "https://api.anthropic.com",
|
|
88
96
|
};
|
|
89
97
|
|
|
90
|
-
const ENV_API_KEY: Record<
|
|
98
|
+
const ENV_API_KEY: Record<AgentProtocol, string[]> = {
|
|
91
99
|
openai: ["ANTEROS_AI_API_KEY", "OPENAI_API_KEY"],
|
|
92
100
|
anthropic: ["ANTEROS_AI_API_KEY", "ANTHROPIC_API_KEY"],
|
|
93
101
|
};
|
|
@@ -100,25 +108,33 @@ const EMPTY_USAGE: AgentUsage = { inputTokens: 0, outputTokens: 0, totalTokens:
|
|
|
100
108
|
|
|
101
109
|
// ─── Configuration resolution ────────────────────────────────────────────
|
|
102
110
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
/**
|
|
112
|
+
* The wire protocol to speak. `protocol` is canonical; `compatible` is the
|
|
113
|
+
* deprecated alias kept for definitions written before the rename — `protocol`
|
|
114
|
+
* wins when both are set, so an override never silently loses to the old field.
|
|
115
|
+
*/
|
|
116
|
+
function resolveProtocol(provider: AgentProvider): AgentProtocol {
|
|
117
|
+
const protocol = provider.protocol ?? provider.compatible ?? "openai";
|
|
118
|
+
if (protocol !== "openai" && protocol !== "anthropic") {
|
|
119
|
+
throw new AppError(`Unknown provider 'protocol': '${protocol}' — expected 'openai' or 'anthropic'`, {
|
|
107
120
|
status: 500,
|
|
108
121
|
code: "AGENT_PROVIDER_INVALID",
|
|
109
122
|
});
|
|
110
123
|
}
|
|
111
|
-
return
|
|
124
|
+
return protocol;
|
|
112
125
|
}
|
|
113
126
|
|
|
114
|
-
|
|
127
|
+
/** @deprecated Renamed to `resolveProtocol`. */
|
|
128
|
+
const resolveCompatible = resolveProtocol;
|
|
129
|
+
|
|
130
|
+
function resolveApiKey(provider: AgentProvider, protocol: AgentProtocol): string {
|
|
115
131
|
if (provider.apiKey) return provider.apiKey;
|
|
116
|
-
for (const name of ENV_API_KEY[
|
|
132
|
+
for (const name of ENV_API_KEY[protocol]) {
|
|
117
133
|
const value = Bun.env?.[name] ?? process.env?.[name];
|
|
118
134
|
if (value) return value;
|
|
119
135
|
}
|
|
120
136
|
throw new AppError(
|
|
121
|
-
`No API key for provider '${provider.model}' (
|
|
137
|
+
`No API key for provider '${provider.model}' (protocol: ${protocol}) — pass \`provider.apiKey\` or set ${ENV_API_KEY[protocol].join(" / ")}`,
|
|
122
138
|
{ status: 500, code: "AGENT_PROVIDER_NO_API_KEY" },
|
|
123
139
|
);
|
|
124
140
|
}
|
|
@@ -128,10 +144,10 @@ function resolveApiKey(provider: AgentProvider, compatible: AgentCompatible): st
|
|
|
128
144
|
* (`http://localhost:11434` → `http://localhost:11434/v1` for OpenAI-compatible
|
|
129
145
|
* servers), an explicit path is used verbatim.
|
|
130
146
|
*/
|
|
131
|
-
function resolveBaseUrl(provider: AgentProvider,
|
|
132
|
-
const raw = provider.baseUrl ?? DEFAULT_BASE_URL[
|
|
147
|
+
function resolveBaseUrl(provider: AgentProvider, protocol: AgentProtocol): string {
|
|
148
|
+
const raw = provider.baseUrl ?? DEFAULT_BASE_URL[protocol];
|
|
133
149
|
const trimmed = raw.replace(/\/+$/, "");
|
|
134
|
-
if (
|
|
150
|
+
if (protocol !== "openai") return trimmed;
|
|
135
151
|
try {
|
|
136
152
|
const url = new URL(trimmed);
|
|
137
153
|
if (url.pathname === "" || url.pathname === "/") url.pathname = "/v1";
|
|
@@ -335,6 +351,14 @@ function thinkingOf(thinking?: AgentThinking): { effort: "low" | "medium" | "hig
|
|
|
335
351
|
return { effort, budgetTokens };
|
|
336
352
|
}
|
|
337
353
|
|
|
354
|
+
/** The maximum, forced-tool name an Anthropic run uses for structured output. */
|
|
355
|
+
const SCHEMA_TOOL = "structured_output";
|
|
356
|
+
/**
|
|
357
|
+
* A gateway that does not know `response_format.json_schema` refuses the request
|
|
358
|
+
* with a 400 that names it — that is the only case we downgrade for.
|
|
359
|
+
*/
|
|
360
|
+
const SCHEMA_UNSUPPORTED = /response_format|json_schema|json mode/i;
|
|
361
|
+
|
|
338
362
|
/** `tool_choice` — the one place the two vocabularies differ. */
|
|
339
363
|
function openaiToolChoice(choice?: AgentToolChoice): any | undefined {
|
|
340
364
|
if (!choice || choice === "auto") return undefined;
|
|
@@ -372,10 +396,10 @@ function normalizeFinishReason(reason: string | null | undefined): string {
|
|
|
372
396
|
}
|
|
373
397
|
}
|
|
374
398
|
|
|
375
|
-
function mapUsage(raw: any,
|
|
399
|
+
function mapUsage(raw: any, protocol: AgentProtocol): AgentUsage {
|
|
376
400
|
if (!raw) return { ...EMPTY_USAGE };
|
|
377
|
-
const input =
|
|
378
|
-
const output =
|
|
401
|
+
const input = protocol === "anthropic" ? raw.input_tokens : raw.prompt_tokens;
|
|
402
|
+
const output = protocol === "anthropic" ? raw.output_tokens : raw.completion_tokens;
|
|
379
403
|
const inputTokens = Number(input ?? 0);
|
|
380
404
|
const outputTokens = Number(output ?? 0);
|
|
381
405
|
return {
|
|
@@ -407,7 +431,7 @@ async function fetchWithRetry(
|
|
|
407
431
|
url: string,
|
|
408
432
|
init: RequestInit,
|
|
409
433
|
provider: AgentProvider,
|
|
410
|
-
|
|
434
|
+
protocol: AgentProtocol,
|
|
411
435
|
): Promise<Response> {
|
|
412
436
|
const options = provider.options ?? {};
|
|
413
437
|
const retries = options.retries ?? DEFAULT_RETRIES;
|
|
@@ -425,7 +449,7 @@ async function fetchWithRetry(
|
|
|
425
449
|
lastError = new AppError(`Provider error (${response.status}): ${providerErrorMessage(body, response.status)}`, {
|
|
426
450
|
status: 502,
|
|
427
451
|
code: "AGENT_PROVIDER_ERROR",
|
|
428
|
-
reason: { provider:
|
|
452
|
+
reason: { provider: protocol, status: response.status },
|
|
429
453
|
});
|
|
430
454
|
} else {
|
|
431
455
|
return response;
|
|
@@ -487,30 +511,30 @@ async function* sseData(body: ReadableStream<Uint8Array>): AsyncGenerator<string
|
|
|
487
511
|
}
|
|
488
512
|
|
|
489
513
|
/** An error event sent by the provider in the middle of a stream. */
|
|
490
|
-
function streamError(payload: any,
|
|
514
|
+
function streamError(payload: any, protocol: AgentProtocol): AppError | null {
|
|
491
515
|
const message = payload?.error?.message ?? (typeof payload?.error === "string" ? payload.error : null);
|
|
492
516
|
if (!message) return null;
|
|
493
517
|
return new AppError(`Provider stream error: ${message}`, {
|
|
494
518
|
status: 502,
|
|
495
519
|
code: "AGENT_PROVIDER_ERROR",
|
|
496
|
-
reason: { provider:
|
|
520
|
+
reason: { provider: protocol },
|
|
497
521
|
});
|
|
498
522
|
}
|
|
499
523
|
|
|
500
524
|
// ─── OpenAI-compatible adapter ───────────────────────────────────────────
|
|
501
525
|
|
|
502
|
-
function openaiAdapter(provider: AgentProvider,
|
|
503
|
-
const baseUrl = resolveBaseUrl(provider,
|
|
526
|
+
function openaiAdapter(provider: AgentProvider, protocol: AgentProtocol): ModelAdapter {
|
|
527
|
+
const baseUrl = resolveBaseUrl(provider, protocol);
|
|
504
528
|
const url = `${baseUrl}/chat/completions`;
|
|
505
529
|
const options = provider.options ?? {};
|
|
506
530
|
|
|
507
531
|
const headers = (): Record<string, string> => ({
|
|
508
532
|
"Content-Type": "application/json",
|
|
509
|
-
Authorization: `Bearer ${resolveApiKey(provider,
|
|
533
|
+
Authorization: `Bearer ${resolveApiKey(provider, protocol)}`,
|
|
510
534
|
...(provider.headers ?? {}),
|
|
511
535
|
});
|
|
512
536
|
|
|
513
|
-
const body = (req: ModelChatRequest, stream: boolean): any => {
|
|
537
|
+
const body = (req: ModelChatRequest, stream: boolean, downgrade = false): any => {
|
|
514
538
|
const payload: any = {
|
|
515
539
|
model: provider.model,
|
|
516
540
|
messages: openaiMessages(req.system, req.messages),
|
|
@@ -525,7 +549,20 @@ function openaiAdapter(provider: AgentProvider, compatible: AgentCompatible): Mo
|
|
|
525
549
|
const temperature = req.temperature ?? options.temperature;
|
|
526
550
|
if (temperature !== undefined) payload.temperature = temperature;
|
|
527
551
|
if (options.topP !== undefined) payload.top_p = options.topP;
|
|
528
|
-
|
|
552
|
+
// `json_schema` is the real constraint (the gateway validates against it);
|
|
553
|
+
// `json_object` only says "an object" and leaves the shape to the prompt
|
|
554
|
+
if (req.jsonSchema && !downgrade) {
|
|
555
|
+
payload.response_format = {
|
|
556
|
+
type: "json_schema",
|
|
557
|
+
json_schema: {
|
|
558
|
+
name: req.jsonSchema.name,
|
|
559
|
+
...(req.jsonSchema.description ? { description: req.jsonSchema.description } : {}),
|
|
560
|
+
schema: req.jsonSchema.schema,
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
} else if (req.json) {
|
|
564
|
+
payload.response_format = { type: "json_object" };
|
|
565
|
+
}
|
|
529
566
|
if (stream) {
|
|
530
567
|
payload.stream = true;
|
|
531
568
|
payload.stream_options = { include_usage: true };
|
|
@@ -533,24 +570,49 @@ function openaiAdapter(provider: AgentProvider, compatible: AgentCompatible): Mo
|
|
|
533
570
|
return payload;
|
|
534
571
|
};
|
|
535
572
|
|
|
573
|
+
/**
|
|
574
|
+
* POST the request, downgrading a rejected `json_schema` **once**. OpenAI-compatible
|
|
575
|
+
* gateways are of very uneven quality: one that refuses the field still answers
|
|
576
|
+
* `json_object`, and the schema is then only enforced by the prompt and the repair
|
|
577
|
+
* turn — better than failing the run.
|
|
578
|
+
*/
|
|
579
|
+
const post = async (req: ModelChatRequest, stream: boolean): Promise<Response> => {
|
|
580
|
+
const send = (downgrade: boolean) => fetchWithRetry(url, {
|
|
581
|
+
method: "POST",
|
|
582
|
+
headers: headers(),
|
|
583
|
+
body: JSON.stringify(body(req, stream, downgrade)),
|
|
584
|
+
signal: req.signal,
|
|
585
|
+
}, provider, protocol);
|
|
586
|
+
|
|
587
|
+
const response = await send(false);
|
|
588
|
+
if (response.ok || !req.jsonSchema || response.status !== 400) return response;
|
|
589
|
+
|
|
590
|
+
const text = await response.text().catch(() => "");
|
|
591
|
+
// Not about the schema — hand the error back as it came
|
|
592
|
+
if (!SCHEMA_UNSUPPORTED.test(text)) {
|
|
593
|
+
return new Response(text, { status: response.status, headers: { "Content-Type": "application/json" } });
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
logger.file("warn", "agent: provider refused json_schema, retrying with json_object", {
|
|
597
|
+
model: provider.model,
|
|
598
|
+
provider: protocol,
|
|
599
|
+
});
|
|
600
|
+
return send(true);
|
|
601
|
+
};
|
|
602
|
+
|
|
536
603
|
return {
|
|
537
|
-
|
|
604
|
+
protocol,
|
|
538
605
|
model: provider.model,
|
|
539
606
|
|
|
540
607
|
async chat(req) {
|
|
541
|
-
const response = await
|
|
542
|
-
method: "POST",
|
|
543
|
-
headers: headers(),
|
|
544
|
-
body: JSON.stringify(body(req, false)),
|
|
545
|
-
signal: req.signal,
|
|
546
|
-
}, provider, compatible);
|
|
608
|
+
const response = await post(req, false);
|
|
547
609
|
|
|
548
610
|
if (!response.ok) {
|
|
549
611
|
const text = await response.text().catch(() => "");
|
|
550
612
|
throw new AppError(`Provider error (${response.status}): ${providerErrorMessage(text, response.status)}`, {
|
|
551
613
|
status: 502,
|
|
552
614
|
code: "AGENT_PROVIDER_ERROR",
|
|
553
|
-
reason: { provider:
|
|
615
|
+
reason: { provider: protocol, status: response.status },
|
|
554
616
|
});
|
|
555
617
|
}
|
|
556
618
|
|
|
@@ -571,25 +633,20 @@ function openaiAdapter(provider: AgentProvider, compatible: AgentCompatible): Mo
|
|
|
571
633
|
: {}),
|
|
572
634
|
toolCalls,
|
|
573
635
|
finishReason: normalizeFinishReason(choice?.finish_reason),
|
|
574
|
-
usage: mapUsage(raw?.usage,
|
|
636
|
+
usage: mapUsage(raw?.usage, protocol),
|
|
575
637
|
raw,
|
|
576
638
|
};
|
|
577
639
|
},
|
|
578
640
|
|
|
579
641
|
async *chatStream(req) {
|
|
580
|
-
const response = await
|
|
581
|
-
method: "POST",
|
|
582
|
-
headers: headers(),
|
|
583
|
-
body: JSON.stringify(body(req, true)),
|
|
584
|
-
signal: req.signal,
|
|
585
|
-
}, provider, compatible);
|
|
642
|
+
const response = await post(req, true);
|
|
586
643
|
|
|
587
644
|
if (!response.ok || !response.body) {
|
|
588
645
|
const text = await response.text().catch(() => "");
|
|
589
646
|
throw new AppError(`Provider error (${response.status}): ${providerErrorMessage(text, response.status)}`, {
|
|
590
647
|
status: 502,
|
|
591
648
|
code: "AGENT_PROVIDER_ERROR",
|
|
592
|
-
reason: { provider:
|
|
649
|
+
reason: { provider: protocol, status: response.status },
|
|
593
650
|
});
|
|
594
651
|
}
|
|
595
652
|
|
|
@@ -601,7 +658,7 @@ function openaiAdapter(provider: AgentProvider, compatible: AgentCompatible): Mo
|
|
|
601
658
|
} catch {
|
|
602
659
|
continue;
|
|
603
660
|
}
|
|
604
|
-
const error = streamError(payload,
|
|
661
|
+
const error = streamError(payload, protocol);
|
|
605
662
|
if (error) throw error;
|
|
606
663
|
|
|
607
664
|
const choice = payload?.choices?.[0];
|
|
@@ -625,7 +682,7 @@ function openaiAdapter(provider: AgentProvider, compatible: AgentCompatible): Mo
|
|
|
625
682
|
};
|
|
626
683
|
}
|
|
627
684
|
}
|
|
628
|
-
if (payload?.usage) yield { type: "usage", usage: mapUsage(payload.usage,
|
|
685
|
+
if (payload?.usage) yield { type: "usage", usage: mapUsage(payload.usage, protocol) };
|
|
629
686
|
if (choice?.finish_reason) yield { type: "finish", finishReason: normalizeFinishReason(choice.finish_reason) };
|
|
630
687
|
}
|
|
631
688
|
},
|
|
@@ -634,18 +691,25 @@ function openaiAdapter(provider: AgentProvider, compatible: AgentCompatible): Mo
|
|
|
634
691
|
|
|
635
692
|
// ─── Anthropic adapter ───────────────────────────────────────────────────
|
|
636
693
|
|
|
637
|
-
function anthropicAdapter(provider: AgentProvider,
|
|
638
|
-
const baseUrl = resolveBaseUrl(provider,
|
|
694
|
+
function anthropicAdapter(provider: AgentProvider, protocol: AgentProtocol): ModelAdapter {
|
|
695
|
+
const baseUrl = resolveBaseUrl(provider, protocol);
|
|
639
696
|
const url = `${baseUrl}/v1/messages`;
|
|
640
697
|
const options = provider.options ?? {};
|
|
641
698
|
|
|
642
699
|
const headers = (): Record<string, string> => ({
|
|
643
700
|
"Content-Type": "application/json",
|
|
644
|
-
"x-api-key": resolveApiKey(provider,
|
|
701
|
+
"x-api-key": resolveApiKey(provider, protocol),
|
|
645
702
|
"anthropic-version": "2023-06-01",
|
|
646
703
|
...(provider.headers ?? {}),
|
|
647
704
|
});
|
|
648
705
|
|
|
706
|
+
/**
|
|
707
|
+
* Does this run ask for a schema, and can Anthropic honour it? A forced tool is
|
|
708
|
+
* incompatible with extended thinking (the API refuses `tool_choice` then) — the
|
|
709
|
+
* schema is still asked for in the prompt, and the repair turn still validates.
|
|
710
|
+
*/
|
|
711
|
+
const wantsSchema = (req: ModelChatRequest): boolean => !!req.jsonSchema && !thinkingOf(req.thinking);
|
|
712
|
+
|
|
649
713
|
const body = (req: ModelChatRequest, stream: boolean): any => {
|
|
650
714
|
const converted = anthropicMessages(req.messages);
|
|
651
715
|
const system = [req.system, converted.system].filter(Boolean).join("\n\n");
|
|
@@ -661,10 +725,27 @@ function anthropicAdapter(provider: AgentProvider, compatible: AgentCompatible):
|
|
|
661
725
|
? thinking.budgetTokens + 1_024
|
|
662
726
|
: maxTokens,
|
|
663
727
|
};
|
|
664
|
-
if (system)
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
728
|
+
if (system) {
|
|
729
|
+
// A prompt-cache breakpoint: everything up to here (instructions, skills,
|
|
730
|
+
// working memory, tools) is billed once instead of on every run
|
|
731
|
+
payload.system = options.cache
|
|
732
|
+
? [{ type: "text", text: system, cache_control: { type: "ephemeral" } }]
|
|
733
|
+
: system;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// The Messages API has no `response_format`: the schema is carried by a tool
|
|
737
|
+
// the model is **forced** to call, and the adapter turns that call into text
|
|
738
|
+
const schemaTool = wantsSchema(req)
|
|
739
|
+
? [{ name: SCHEMA_TOOL, description: "Return the answer as a JSON object matching the schema.", input_schema: req.jsonSchema!.schema }]
|
|
740
|
+
: [];
|
|
741
|
+
const specs = [...(req.tools?.length ? anthropicToolSpecs(req.tools) : []), ...schemaTool];
|
|
742
|
+
if (specs.length) {
|
|
743
|
+
payload.tools = options.cache
|
|
744
|
+
? [...specs.slice(0, -1), { ...specs[specs.length - 1]!, cache_control: { type: "ephemeral" } }]
|
|
745
|
+
: specs;
|
|
746
|
+
const toolChoice = schemaTool.length
|
|
747
|
+
? { type: "tool", name: SCHEMA_TOOL }
|
|
748
|
+
: anthropicToolChoice(req.toolChoice);
|
|
668
749
|
if (toolChoice) payload.tool_choice = toolChoice;
|
|
669
750
|
}
|
|
670
751
|
if (thinking) payload.thinking = { type: "enabled", budget_tokens: thinking.budgetTokens };
|
|
@@ -677,7 +758,7 @@ function anthropicAdapter(provider: AgentProvider, compatible: AgentCompatible):
|
|
|
677
758
|
};
|
|
678
759
|
|
|
679
760
|
return {
|
|
680
|
-
|
|
761
|
+
protocol,
|
|
681
762
|
model: provider.model,
|
|
682
763
|
|
|
683
764
|
async chat(req) {
|
|
@@ -686,20 +767,20 @@ function anthropicAdapter(provider: AgentProvider, compatible: AgentCompatible):
|
|
|
686
767
|
headers: headers(),
|
|
687
768
|
body: JSON.stringify(body(req, false)),
|
|
688
769
|
signal: req.signal,
|
|
689
|
-
}, provider,
|
|
770
|
+
}, provider, protocol);
|
|
690
771
|
|
|
691
772
|
if (!response.ok) {
|
|
692
773
|
const text = await response.text().catch(() => "");
|
|
693
774
|
throw new AppError(`Provider error (${response.status}): ${providerErrorMessage(text, response.status)}`, {
|
|
694
775
|
status: 502,
|
|
695
776
|
code: "AGENT_PROVIDER_ERROR",
|
|
696
|
-
reason: { provider:
|
|
777
|
+
reason: { provider: protocol, status: response.status },
|
|
697
778
|
});
|
|
698
779
|
}
|
|
699
780
|
|
|
700
781
|
const raw: any = await response.json();
|
|
701
782
|
const blocks: any[] = Array.isArray(raw?.content) ? raw.content : [];
|
|
702
|
-
const
|
|
783
|
+
const calls = blocks
|
|
703
784
|
.filter((block) => block?.type === "tool_use")
|
|
704
785
|
.map((block: any, index: number) => ({
|
|
705
786
|
id: block?.id ?? `toolu_${index}`,
|
|
@@ -707,8 +788,15 @@ function anthropicAdapter(provider: AgentProvider, compatible: AgentCompatible):
|
|
|
707
788
|
argsText: JSON.stringify(block?.input ?? {}),
|
|
708
789
|
}));
|
|
709
790
|
|
|
791
|
+
// The forced schema tool **is** the answer: it becomes the text, so the
|
|
792
|
+
// runtime never sees a tool call it would try to execute
|
|
793
|
+
const forced = wantsSchema(req) ? calls.find((call) => call.name === SCHEMA_TOOL) : undefined;
|
|
794
|
+
const toolCalls = forced ? calls.filter((call) => call !== forced) : calls;
|
|
795
|
+
|
|
710
796
|
return {
|
|
711
|
-
text:
|
|
797
|
+
text: forced
|
|
798
|
+
? forced.argsText
|
|
799
|
+
: blocks.filter((block) => block?.type === "text").map((block) => block.text).join(""),
|
|
712
800
|
toolCalls,
|
|
713
801
|
// The raw blocks are kept: a tool round must echo the **signed** ones back
|
|
714
802
|
...(blocks.some((block) => block?.type === "thinking" || block?.type === "redacted_thinking")
|
|
@@ -722,7 +810,7 @@ function anthropicAdapter(provider: AgentProvider, compatible: AgentCompatible):
|
|
|
722
810
|
}
|
|
723
811
|
: {}),
|
|
724
812
|
finishReason: normalizeFinishReason(raw?.stop_reason),
|
|
725
|
-
usage: mapUsage(raw?.usage,
|
|
813
|
+
usage: mapUsage(raw?.usage, protocol),
|
|
726
814
|
raw,
|
|
727
815
|
};
|
|
728
816
|
},
|
|
@@ -731,20 +819,22 @@ function anthropicAdapter(provider: AgentProvider, compatible: AgentCompatible):
|
|
|
731
819
|
// Thinking blocks, accumulated by index — signatures included, so the run
|
|
732
820
|
// can echo them back if the model then asks for a tool
|
|
733
821
|
const thinking = new Map<number, any>();
|
|
822
|
+
// Index of the forced schema tool, whose deltas are the answer itself
|
|
823
|
+
let schemaBlock: number | null = null;
|
|
734
824
|
|
|
735
825
|
const response = await fetchWithRetry(url, {
|
|
736
826
|
method: "POST",
|
|
737
827
|
headers: headers(),
|
|
738
828
|
body: JSON.stringify(body(req, true)),
|
|
739
829
|
signal: req.signal,
|
|
740
|
-
}, provider,
|
|
830
|
+
}, provider, protocol);
|
|
741
831
|
|
|
742
832
|
if (!response.ok || !response.body) {
|
|
743
833
|
const text = await response.text().catch(() => "");
|
|
744
834
|
throw new AppError(`Provider error (${response.status}): ${providerErrorMessage(text, response.status)}`, {
|
|
745
835
|
status: 502,
|
|
746
836
|
code: "AGENT_PROVIDER_ERROR",
|
|
747
|
-
reason: { provider:
|
|
837
|
+
reason: { provider: protocol, status: response.status },
|
|
748
838
|
});
|
|
749
839
|
}
|
|
750
840
|
|
|
@@ -755,7 +845,7 @@ function anthropicAdapter(provider: AgentProvider, compatible: AgentCompatible):
|
|
|
755
845
|
} catch {
|
|
756
846
|
continue;
|
|
757
847
|
}
|
|
758
|
-
const error = streamError(payload,
|
|
848
|
+
const error = streamError(payload, protocol);
|
|
759
849
|
if (error) throw error;
|
|
760
850
|
|
|
761
851
|
switch (payload?.type) {
|
|
@@ -770,6 +860,12 @@ function anthropicAdapter(provider: AgentProvider, compatible: AgentCompatible):
|
|
|
770
860
|
const block = payload?.content_block;
|
|
771
861
|
const index = payload?.index ?? 0;
|
|
772
862
|
if (block?.type === "tool_use") {
|
|
863
|
+
// A forced schema call streams its JSON like any tool call — and
|
|
864
|
+
// is surfaced as text instead, see `chat()`
|
|
865
|
+
if (wantsSchema(req) && block.name === SCHEMA_TOOL) {
|
|
866
|
+
schemaBlock = index;
|
|
867
|
+
break;
|
|
868
|
+
}
|
|
773
869
|
yield {
|
|
774
870
|
type: "tool_call",
|
|
775
871
|
index,
|
|
@@ -799,6 +895,11 @@ function anthropicAdapter(provider: AgentProvider, compatible: AgentCompatible):
|
|
|
799
895
|
block.signature = delta.signature;
|
|
800
896
|
thinking.set(index, block);
|
|
801
897
|
} else if (delta?.type === "input_json_delta" && delta.partial_json) {
|
|
898
|
+
// The schema tool's JSON is the answer, token by token
|
|
899
|
+
if (index === schemaBlock) {
|
|
900
|
+
yield { type: "text", text: delta.partial_json };
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
802
903
|
yield { type: "tool_call", index, argsText: delta.partial_json };
|
|
803
904
|
}
|
|
804
905
|
break;
|
|
@@ -816,7 +917,7 @@ function anthropicAdapter(provider: AgentProvider, compatible: AgentCompatible):
|
|
|
816
917
|
throw new AppError(`Provider stream error: ${payload?.error?.message ?? "unknown"}`, {
|
|
817
918
|
status: 502,
|
|
818
919
|
code: "AGENT_PROVIDER_ERROR",
|
|
819
|
-
reason: { provider:
|
|
920
|
+
reason: { provider: protocol },
|
|
820
921
|
});
|
|
821
922
|
}
|
|
822
923
|
}
|
|
@@ -842,15 +943,17 @@ function resolveProvider(provider: AgentProvider): ModelAdapter {
|
|
|
842
943
|
if (!provider?.model) {
|
|
843
944
|
throw new AppError("`provider.model` is required", { status: 500, code: "AGENT_PROVIDER_INVALID" });
|
|
844
945
|
}
|
|
845
|
-
const
|
|
846
|
-
return
|
|
847
|
-
? anthropicAdapter(provider,
|
|
848
|
-
: openaiAdapter(provider,
|
|
946
|
+
const protocol = resolveProtocol(provider);
|
|
947
|
+
return protocol === "anthropic"
|
|
948
|
+
? anthropicAdapter(provider, protocol)
|
|
949
|
+
: openaiAdapter(provider, protocol);
|
|
849
950
|
}
|
|
850
951
|
|
|
851
952
|
export {
|
|
852
953
|
EMPTY_USAGE,
|
|
853
954
|
DEFAULT_MAX_TOKENS,
|
|
955
|
+
resolveProtocol,
|
|
956
|
+
/** @deprecated Renamed to `resolveProtocol`. */
|
|
854
957
|
resolveCompatible,
|
|
855
958
|
resolveApiKey,
|
|
856
959
|
resolveBaseUrl,
|