jangada-ai 0.0.0 → 1.0.0
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/LICENSE +0 -0
- package/README.md +31 -3
- package/dist/anthropic-BlqXqf9N.js +555 -0
- package/dist/anthropic-CYKRUh9e.cjs +555 -0
- package/dist/base-8WLtVj6O.js +254 -0
- package/dist/base-bplcDr6J.cjs +277 -0
- package/dist/bedrock-Dxpg5jFC.cjs +475 -0
- package/dist/bedrock-infoTG03.js +475 -0
- package/dist/client-BFoano5Y.js +792 -0
- package/dist/client-BmJIzbSw.d.ts +357 -0
- package/dist/client-C7tNbVHu.cjs +851 -0
- package/dist/client-CoQl5mod.d.ts +584 -0
- package/dist/client-DgU6xftB.d.cts +584 -0
- package/dist/client-Y9MDtyQ4.d.cts +357 -0
- package/dist/context-C7qrmHGJ.js +299 -0
- package/dist/context-DN___2Xb.d.cts +56 -0
- package/dist/context-DN___2Xb.d.ts +56 -0
- package/dist/context-JTZvYRrC.cjs +388 -0
- package/dist/env-B1ffNsxb.cjs +26 -0
- package/dist/env-BH4ulZ4_.js +15 -0
- package/dist/errors-BJIPDLwX.cjs +320 -0
- package/dist/errors-hp-4UePr.js +207 -0
- package/dist/eval/index.cjs +275 -0
- package/dist/eval/index.d.cts +87 -0
- package/dist/eval/index.d.ts +87 -0
- package/dist/eval/index.js +270 -0
- package/dist/files-DhchXoMT.cjs +363 -0
- package/dist/files-lcQv3hAq.js +340 -0
- package/dist/gemini-Dj5mMQH_.cjs +859 -0
- package/dist/gemini-UV1iF24m.js +858 -0
- package/dist/gemini-schema-BLKtypnG.js +21 -0
- package/dist/gemini-schema-BwcTPvV1.cjs +26 -0
- package/dist/index.cjs +4878 -4
- package/dist/index.d.cts +774 -1
- package/dist/index.d.ts +774 -1
- package/dist/index.js +4768 -4
- package/dist/mcp/index.cjs +21 -0
- package/dist/mcp/index.d.cts +4 -0
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/index.js +4 -0
- package/dist/mistral-CdX-9oKB.cjs +882 -0
- package/dist/mistral-NBf2U9m7.js +882 -0
- package/dist/node/index.cjs +1023 -0
- package/dist/node/index.d.cts +312 -0
- package/dist/node/index.d.ts +312 -0
- package/dist/node/index.js +1015 -0
- package/dist/observability-D1Eiz3hh.cjs +369 -0
- package/dist/observability-DLUNn71i.d.cts +306 -0
- package/dist/observability-DLUNn71i.d.ts +306 -0
- package/dist/observability-DQW4up31.js +316 -0
- package/dist/ollama-CURVIQcv.cjs +627 -0
- package/dist/ollama-CpdaRdg-.js +627 -0
- package/dist/openai-compat-BWdb9UUW.cjs +1288 -0
- package/dist/openai-compat-Qlv_JgU5.js +1284 -0
- package/dist/prebuilt/index.cjs +1645 -0
- package/dist/prebuilt/index.d.cts +293 -0
- package/dist/prebuilt/index.d.ts +293 -0
- package/dist/prebuilt/index.js +1594 -0
- package/dist/rag/index.cjs +1140 -0
- package/dist/rag/index.d.cts +387 -0
- package/dist/rag/index.d.ts +387 -0
- package/dist/rag/index.js +1122 -0
- package/dist/schema-BBiyhRZk.d.cts +63 -0
- package/dist/schema-BBiyhRZk.d.ts +63 -0
- package/dist/schema-DCaQOk49.js +104 -0
- package/dist/schema-qS-xTTIs.cjs +121 -0
- package/dist/server-B_XDzBbl.d.ts +198 -0
- package/dist/server-C8hINWgp.d.cts +198 -0
- package/dist/server-CKj0B5mk.cjs +820 -0
- package/dist/server-DOnplmbD.js +791 -0
- package/dist/tools-4-yoaMWI.d.ts +147 -0
- package/dist/tools-DjzQr-Np.d.cts +147 -0
- package/dist/version-Ch4xT-gQ.cjs +292 -0
- package/dist/version-CrHu1Pq_.js +185 -0
- package/package.json +110 -7
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -1,18 +1,46 @@
|
|
|
1
1
|
# jangada (JS/TS)
|
|
2
2
|
|
|
3
3
|
Camada fina e adaptável sobre os SDKs de LLM, em TypeScript. Troque provider,
|
|
4
|
-
modelo e chave sem mudar o resto do código
|
|
4
|
+
modelo e chave sem mudar o resto do código, com retry e fallback por tipo de
|
|
5
|
+
erro, structured output com Zod, tools, streaming e custo em cada resposta.
|
|
5
6
|
|
|
6
7
|
Port da lib Python [`jangada-ai`](https://pypi.org/project/jangada-ai/).
|
|
7
8
|
Documentação: [docs.jangada.dev.br](https://docs.jangada.dev.br).
|
|
8
9
|
|
|
9
|
-
>
|
|
10
|
+
> Versão 1.0.0: paridade com a lib Python (providers, tools, MCP, agentes, RAG, eval, A2A e mais).
|
|
10
11
|
|
|
11
12
|
```bash
|
|
12
13
|
npm install jangada-ai
|
|
14
|
+
npm install openai # e/ou @anthropic-ai/sdk, @google/genai, zod
|
|
13
15
|
```
|
|
14
16
|
|
|
15
|
-
Roda em Node 22+, Bun, Deno e runtimes edge
|
|
17
|
+
Roda em Node 22+, Bun, Deno e runtimes edge. O núcleo não usa `fs`; leitura de
|
|
18
|
+
arquivo e `.env` ficam em `jangada-ai/node`.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { LLM } from "jangada-ai";
|
|
22
|
+
import "jangada-ai/node"; // carrega o .env
|
|
23
|
+
|
|
24
|
+
const llm = new LLM("openai", "gpt-4o-mini", {
|
|
25
|
+
fallbacks: [new LLM("anthropic", "claude-haiku-4-5")],
|
|
26
|
+
});
|
|
27
|
+
const comp = await llm.complete("Olá, {{ nome }}!", { variables: { nome: "Ana" } });
|
|
28
|
+
console.log(comp.text, comp.cost);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Providers: `openai`, `azure`, `groq`, `openrouter`, `deepseek`, `anthropic`,
|
|
32
|
+
`gemini`, `vertex`, `bedrock`, `mistral`, `ollama`.
|
|
33
|
+
|
|
34
|
+
Subpaths: `jangada-ai/node` (arquivos, `.env`, `serveMcp`, `serveA2a`),
|
|
35
|
+
`jangada-ai/mcp`, `jangada-ai/rag`, `jangada-ai/eval`, `jangada-ai/prebuilt` e
|
|
36
|
+
`jangada-ai/a2a`. Exemplos em [`examples/`](examples/).
|
|
37
|
+
|
|
38
|
+
## Desenvolvimento
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm run check # lint + tipos + testes + build
|
|
42
|
+
npm run test:live # APIs reais (precisa das chaves no ambiente)
|
|
43
|
+
```
|
|
16
44
|
|
|
17
45
|
## Licença
|
|
18
46
|
|
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
import { _ as UnsupportedError, d as ProviderError, l as OutputValidationError, m as ServerError, s as LLMError, v as classify } from "./errors-hp-4UePr.js";
|
|
2
|
+
import { s as splitMcp } from "./client-BFoano5Y.js";
|
|
3
|
+
import { d as imageBase64, s as Completion, u as ToolCall } from "./context-C7qrmHGJ.js";
|
|
4
|
+
import { c as NativeTool, i as splitTools } from "./version-CrHu1Pq_.js";
|
|
5
|
+
import { n as applyProfile, t as Provider } from "./base-8WLtVj6O.js";
|
|
6
|
+
// src/providers/anthropic.ts
|
|
7
|
+
/**
|
|
8
|
+
* Provider Anthropic (Claude): texto, vision, tools, tools nativas (server tools),
|
|
9
|
+
* streaming e structured output por tool-forcing. SDK: `@anthropic-ai/sdk`
|
|
10
|
+
* (peer dependency opcional).
|
|
11
|
+
*
|
|
12
|
+
* Structured output força uma ferramenta `extract` cujo `input_schema` é o JSON
|
|
13
|
+
* Schema. Nos modelos que REJEITAM forced tool use (Opus 5.5, Sonnet 5.5, Fable 5.1,
|
|
14
|
+
* Mythos 5.1) cai para `tool_choice: auto` + strict tool use, a migração oficial.
|
|
15
|
+
*
|
|
16
|
+
* MCP remoto: `mcpServers` com `MCPServer` vai na Messages API (`mcp_servers` +
|
|
17
|
+
* `mcp_toolset`, header beta `mcp-client-2025-11-20`). Sessão client-side não dá.
|
|
18
|
+
*
|
|
19
|
+
* Espelha `jangada_ai/providers/anthropic.py`.
|
|
20
|
+
*/
|
|
21
|
+
const THINKING_KEY = "anthropic_thinking_blocks";
|
|
22
|
+
const CONTENT_KEY = "anthropic_content";
|
|
23
|
+
const CONTAINER_KEY = "anthropic_container";
|
|
24
|
+
const MAX_PAUSE = 5;
|
|
25
|
+
const MCP_BETA = "mcp-client-2025-11-20";
|
|
26
|
+
const NATIVE_VERSIONS = {
|
|
27
|
+
web_search: ["web_search_20250305", "web_search"],
|
|
28
|
+
web_fetch: ["web_fetch_20250910", "web_fetch"],
|
|
29
|
+
code_execution: ["code_execution_20250825", "code_execution"]
|
|
30
|
+
};
|
|
31
|
+
const NATIVE_OPTS = {
|
|
32
|
+
maxUses: "max_uses",
|
|
33
|
+
allowedDomains: "allowed_domains",
|
|
34
|
+
blockedDomains: "blocked_domains",
|
|
35
|
+
userLocation: "user_location"
|
|
36
|
+
};
|
|
37
|
+
const PLAIN_BLOCKS = /* @__PURE__ */ new Set([
|
|
38
|
+
"text",
|
|
39
|
+
"tool_use",
|
|
40
|
+
"thinking",
|
|
41
|
+
"redacted_thinking"
|
|
42
|
+
]);
|
|
43
|
+
const NO_FORCED_TOOLS = /(?:opus-5[.-]5|sonnet-5[.-]5|fable-5[.-]1|mythos-5[.-]1)/;
|
|
44
|
+
function get(obj, key) {
|
|
45
|
+
return obj !== null && typeof obj === "object" ? obj[key] : void 0;
|
|
46
|
+
}
|
|
47
|
+
function n(v) {
|
|
48
|
+
return typeof v === "number" && Number.isInteger(v) ? v : 0;
|
|
49
|
+
}
|
|
50
|
+
/** strict tool use exige `additionalProperties: false` em todo objeto (não mexe em `required`). */
|
|
51
|
+
function strictSchema(node) {
|
|
52
|
+
if (Array.isArray(node)) return node.map(strictSchema);
|
|
53
|
+
if (node && typeof node === "object") {
|
|
54
|
+
const out = {};
|
|
55
|
+
for (const [k, v] of Object.entries(node)) out[k] = strictSchema(v);
|
|
56
|
+
if (out.type === "object" && !("additionalProperties" in out)) out.additionalProperties = false;
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
return node;
|
|
60
|
+
}
|
|
61
|
+
/** Bloco da resposta como objeto simples, reenviável exatamente como veio. */
|
|
62
|
+
function dumpBlock(b) {
|
|
63
|
+
return JSON.parse(JSON.stringify(b));
|
|
64
|
+
}
|
|
65
|
+
function systemText(content) {
|
|
66
|
+
if (typeof content === "string") return content;
|
|
67
|
+
return content.map((p) => p.type === "text" ? p.text : "").filter(Boolean).join("\n");
|
|
68
|
+
}
|
|
69
|
+
var AnthropicProvider = class AnthropicProvider extends Provider {
|
|
70
|
+
name = "anthropic";
|
|
71
|
+
envKey = "ANTHROPIC_API_KEY";
|
|
72
|
+
nativeToolKinds = /* @__PURE__ */ new Set([
|
|
73
|
+
"web_search",
|
|
74
|
+
"web_fetch",
|
|
75
|
+
"code_execution"
|
|
76
|
+
]);
|
|
77
|
+
nativeToolPassthrough = true;
|
|
78
|
+
nativeToolAliases = ["claude"];
|
|
79
|
+
supportsMcp = true;
|
|
80
|
+
async buildClient() {
|
|
81
|
+
let mod;
|
|
82
|
+
try {
|
|
83
|
+
mod = await import("@anthropic-ai/sdk");
|
|
84
|
+
} catch (e) {
|
|
85
|
+
throw new Error("Instale o SDK da Anthropic: npm install @anthropic-ai/sdk", { cause: e });
|
|
86
|
+
}
|
|
87
|
+
return new mod.default({
|
|
88
|
+
apiKey: this.resolveApiKey(),
|
|
89
|
+
...this.clientOptions
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
content(content) {
|
|
93
|
+
if (typeof content === "string") return content;
|
|
94
|
+
const blocks = [];
|
|
95
|
+
let thinking = [];
|
|
96
|
+
for (const p of content) if (p.type === "text") {
|
|
97
|
+
if (p.text) blocks.push({
|
|
98
|
+
type: "text",
|
|
99
|
+
text: p.text
|
|
100
|
+
});
|
|
101
|
+
} else if (p.type === "image") blocks.push({
|
|
102
|
+
type: "image",
|
|
103
|
+
source: {
|
|
104
|
+
type: "base64",
|
|
105
|
+
media_type: p.mimeType,
|
|
106
|
+
data: imageBase64(p)
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
else if (p.type === "tool_call") {
|
|
110
|
+
if (!thinking.length) thinking = [...p.metadata?.[THINKING_KEY] ?? []];
|
|
111
|
+
blocks.push({
|
|
112
|
+
type: "tool_use",
|
|
113
|
+
id: p.id,
|
|
114
|
+
name: p.name,
|
|
115
|
+
input: p.args
|
|
116
|
+
});
|
|
117
|
+
} else if (p.type === "tool_result") blocks.push({
|
|
118
|
+
type: "tool_result",
|
|
119
|
+
tool_use_id: p.toolCallId,
|
|
120
|
+
content: p.content,
|
|
121
|
+
...p.isError ? { is_error: true } : {}
|
|
122
|
+
});
|
|
123
|
+
return [...thinking, ...blocks];
|
|
124
|
+
}
|
|
125
|
+
split(messages) {
|
|
126
|
+
const system = messages.filter((m) => m.role === "system").map((m) => systemText(m.content)).filter(Boolean).join("\n\n") || void 0;
|
|
127
|
+
const convo = [];
|
|
128
|
+
for (const m of messages) {
|
|
129
|
+
if (m.role === "system") continue;
|
|
130
|
+
const brutos = m.metadata?.[CONTENT_KEY];
|
|
131
|
+
if (m.role === "assistant" && brutos?.length) {
|
|
132
|
+
convo.push({
|
|
133
|
+
role: "assistant",
|
|
134
|
+
content: [...brutos]
|
|
135
|
+
});
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
convo.push({
|
|
139
|
+
role: m.role === "tool" ? "user" : m.role,
|
|
140
|
+
content: this.content(m.content)
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
system,
|
|
145
|
+
convo
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** Canônico → nativo: stop → stop_sequences; seed não existe na Anthropic. */
|
|
149
|
+
translate(params) {
|
|
150
|
+
const map = {
|
|
151
|
+
maxTokens: "max_tokens",
|
|
152
|
+
topP: "top_p",
|
|
153
|
+
topK: "top_k",
|
|
154
|
+
stop: "stop_sequences"
|
|
155
|
+
};
|
|
156
|
+
const out = {};
|
|
157
|
+
for (const [k, v] of Object.entries(params)) {
|
|
158
|
+
if (v === void 0 || v === null || k === "seed") continue;
|
|
159
|
+
const key = map[k] ?? k;
|
|
160
|
+
out[key] = key === "stop_sequences" && typeof v === "string" ? [v] : v;
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
formatTools(tools) {
|
|
165
|
+
const { functions, natives } = splitTools(tools);
|
|
166
|
+
const out = functions.map((t) => ({
|
|
167
|
+
name: t.name,
|
|
168
|
+
description: t.description,
|
|
169
|
+
input_schema: t.parameters
|
|
170
|
+
}));
|
|
171
|
+
for (const nt of natives) out.push(this.nativeTool(nt));
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
/** NativeTool canônica → server tool da Messages API. */
|
|
175
|
+
nativeTool(nt) {
|
|
176
|
+
if (nt.kind === "native") return nt.spec;
|
|
177
|
+
const raw = nt.optsFor("anthropic");
|
|
178
|
+
const o = {};
|
|
179
|
+
for (const [k, v] of Object.entries(raw)) o[NATIVE_OPTS[k] ?? k] = v;
|
|
180
|
+
const [tipoPadrao, nome] = NATIVE_VERSIONS[nt.kind];
|
|
181
|
+
const tipo = o.version ?? tipoPadrao;
|
|
182
|
+
delete o.version;
|
|
183
|
+
delete o.container;
|
|
184
|
+
if (o.files !== void 0) throw new UnsupportedError("codeExecution({ files }) não existe na Anthropic: envie os arquivos como blocos container_upload (Files API) na mensagem.", { provider: this.name });
|
|
185
|
+
const loc = o.user_location;
|
|
186
|
+
if (loc && typeof loc === "object" && !("type" in loc)) o.user_location = {
|
|
187
|
+
type: "approximate",
|
|
188
|
+
...loc
|
|
189
|
+
};
|
|
190
|
+
if (o.allowed_domains && o.blocked_domains) throw new UnsupportedError(`${nt.kind}: a Anthropic não aceita allowedDomains e blockedDomains juntos (400). Use só um.`, { provider: this.name });
|
|
191
|
+
return {
|
|
192
|
+
type: tipo,
|
|
193
|
+
name: nome,
|
|
194
|
+
...o
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/** Container do code_execution: o explícito ou o último do histórico. */
|
|
198
|
+
static containerFrom(messages, tools) {
|
|
199
|
+
for (const t of tools) if (t instanceof NativeTool && t.kind === "code_execution") {
|
|
200
|
+
const explicito = t.options.container;
|
|
201
|
+
if (typeof explicito === "string" && explicito) return explicito;
|
|
202
|
+
for (const m of [...messages].reverse()) {
|
|
203
|
+
const cid = m.metadata?.[CONTAINER_KEY];
|
|
204
|
+
if (typeof cid === "string" && cid) return cid;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
static toolChoice(tc) {
|
|
209
|
+
if (tc === "required" || tc === "any") return { type: "any" };
|
|
210
|
+
if (tc === "auto" || tc === "none") return { type: tc };
|
|
211
|
+
if (typeof tc === "string") return {
|
|
212
|
+
type: "tool",
|
|
213
|
+
name: tc
|
|
214
|
+
};
|
|
215
|
+
return tc;
|
|
216
|
+
}
|
|
217
|
+
forbidsForcedTools() {
|
|
218
|
+
return NO_FORCED_TOOLS.test(this.model ?? "");
|
|
219
|
+
}
|
|
220
|
+
kwargs(messages, options) {
|
|
221
|
+
const { system, convo } = this.split(messages);
|
|
222
|
+
const native = applyProfile(this.name, this.model, this.translate(options.params), this.profileModel ? { profileModel: this.profileModel } : void 0).params;
|
|
223
|
+
native.max_tokens ??= 8192;
|
|
224
|
+
const kw = {
|
|
225
|
+
model: this.model,
|
|
226
|
+
messages: convo,
|
|
227
|
+
...native
|
|
228
|
+
};
|
|
229
|
+
if (system) kw.system = system;
|
|
230
|
+
const tools = options.tools;
|
|
231
|
+
if (tools?.length) {
|
|
232
|
+
kw.tools = this.formatTools(tools);
|
|
233
|
+
if (options.toolChoice !== void 0 && options.toolChoice !== null) {
|
|
234
|
+
const tc = AnthropicProvider.toolChoice(options.toolChoice);
|
|
235
|
+
if (this.forbidsForcedTools() && (tc.type === "any" || tc.type === "tool")) throw new UnsupportedError(`${this.model} não suporta forced tool use (toolChoice=${JSON.stringify(options.toolChoice)}): a API retorna 400. Use toolChoice 'auto' (com strict tool use, se quiser garantir o schema) ou structured output via parse().`, { provider: this.name });
|
|
236
|
+
kw.tool_choice = tc;
|
|
237
|
+
}
|
|
238
|
+
const container = AnthropicProvider.containerFrom(messages, tools);
|
|
239
|
+
if (container) kw.container = container;
|
|
240
|
+
}
|
|
241
|
+
if (options.mcpServers?.length) this.applyMcp(kw, options.mcpServers);
|
|
242
|
+
return kw;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* MCP remoto via Messages API (server-side, beta). Sessão client-side não dá.
|
|
246
|
+
* Allowlist no shape `mcp-client-2025-11-20`: `default_config` desabilitado e cada
|
|
247
|
+
* tool permitida habilitada em `configs`.
|
|
248
|
+
*/
|
|
249
|
+
applyMcp(kw, mcpServers) {
|
|
250
|
+
const { remote, sessions } = splitMcp(mcpServers);
|
|
251
|
+
if (sessions.length) throw new UnsupportedError("Anthropic só aceita MCP remoto por URL (passe MCPServer). Para MCP client-side (sessão), use o Gemini ou MCPClient + runAgent.", { provider: this.name });
|
|
252
|
+
kw.mcp_servers = remote.map((s) => ({
|
|
253
|
+
type: "url",
|
|
254
|
+
url: s.url,
|
|
255
|
+
name: s.name,
|
|
256
|
+
...s.authorizationToken ? { authorization_token: s.authorizationToken } : {}
|
|
257
|
+
}));
|
|
258
|
+
const toolsets = remote.map((s) => ({
|
|
259
|
+
type: "mcp_toolset",
|
|
260
|
+
mcp_server_name: s.name,
|
|
261
|
+
...s.allowedTools?.length ? {
|
|
262
|
+
default_config: { enabled: false },
|
|
263
|
+
configs: Object.fromEntries(s.allowedTools.map((t) => [t, { enabled: true }]))
|
|
264
|
+
} : {}
|
|
265
|
+
}));
|
|
266
|
+
kw.tools = [...kw.tools ?? [], ...toolsets];
|
|
267
|
+
}
|
|
268
|
+
/** Opções da requisição: `signal` e, com MCP, o header beta. */
|
|
269
|
+
reqOptions(options) {
|
|
270
|
+
const o = { signal: options.signal };
|
|
271
|
+
if (options.mcpServers?.length) o.headers = { "anthropic-beta": MCP_BETA };
|
|
272
|
+
return o;
|
|
273
|
+
}
|
|
274
|
+
static thinkingBlocks(content) {
|
|
275
|
+
const out = [];
|
|
276
|
+
for (const b of content) {
|
|
277
|
+
const tipo = get(b, "type");
|
|
278
|
+
if (tipo === "thinking") out.push({
|
|
279
|
+
type: "thinking",
|
|
280
|
+
thinking: get(b, "thinking") ?? "",
|
|
281
|
+
signature: get(b, "signature") ?? ""
|
|
282
|
+
});
|
|
283
|
+
else if (tipo === "redacted_thinking") out.push({
|
|
284
|
+
type: "redacted_thinking",
|
|
285
|
+
data: get(b, "data") ?? ""
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
completion(resp) {
|
|
291
|
+
try {
|
|
292
|
+
return this.buildCompletion(resp);
|
|
293
|
+
} catch (e) {
|
|
294
|
+
if (e instanceof LLMError) throw e;
|
|
295
|
+
throw new ProviderError(`[${this.name}] resposta em formato inesperado: ${e instanceof Error ? `${e.name}: ${e.message}` : e}`, {
|
|
296
|
+
provider: this.name,
|
|
297
|
+
original: e
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
buildCompletion(resp) {
|
|
302
|
+
const content = get(resp, "content");
|
|
303
|
+
if (!Array.isArray(content)) throw new ServerError(`[${this.name}] resposta sem conteúdo (content=${content}).`, { provider: this.name });
|
|
304
|
+
const text = content.map((b) => get(b, "type") === "text" ? get(b, "text") ?? "" : "").join("");
|
|
305
|
+
const toolCalls = content.filter((b) => get(b, "type") === "tool_use" && get(b, "name")).map((b) => new ToolCall(String(get(b, "id") ?? ""), String(get(b, "name")), { ...get(b, "input") ?? {} }));
|
|
306
|
+
const thinking = AnthropicProvider.thinkingBlocks(content);
|
|
307
|
+
if (thinking.length && toolCalls[0]) toolCalls[0].metadata[THINKING_KEY] = thinking;
|
|
308
|
+
const { citations, serverCalls, preservar } = AnthropicProvider.nativeOutputs(content);
|
|
309
|
+
const meta = {};
|
|
310
|
+
if (preservar) meta[CONTENT_KEY] = content.map(dumpBlock);
|
|
311
|
+
const cid = get(get(resp, "container"), "id");
|
|
312
|
+
if (typeof cid === "string" && cid) meta[CONTAINER_KEY] = cid;
|
|
313
|
+
let usage = null;
|
|
314
|
+
const u = get(resp, "usage");
|
|
315
|
+
if (u) {
|
|
316
|
+
const cacheWrite = n(get(u, "cache_creation_input_tokens"));
|
|
317
|
+
const cacheRead = n(get(u, "cache_read_input_tokens"));
|
|
318
|
+
usage = {
|
|
319
|
+
inputTokens: n(get(u, "input_tokens")) + cacheWrite + cacheRead,
|
|
320
|
+
outputTokens: n(get(u, "output_tokens"))
|
|
321
|
+
};
|
|
322
|
+
if (cacheRead) usage.cacheReadTokens = cacheRead;
|
|
323
|
+
if (cacheWrite) usage.cacheWriteTokens = cacheWrite;
|
|
324
|
+
const stu = get(u, "server_tool_use");
|
|
325
|
+
if (stu) {
|
|
326
|
+
const reqs = {};
|
|
327
|
+
const ws = n(get(stu, "web_search_requests"));
|
|
328
|
+
const wf = n(get(stu, "web_fetch_requests"));
|
|
329
|
+
if (ws) reqs.web_search = ws;
|
|
330
|
+
if (wf) reqs.web_fetch = wf;
|
|
331
|
+
if (Object.keys(reqs).length) usage.serverToolRequests = reqs;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const stop = get(resp, "stop_reason") ?? null;
|
|
335
|
+
return new Completion({
|
|
336
|
+
text,
|
|
337
|
+
model: this.model,
|
|
338
|
+
provider: this.name,
|
|
339
|
+
raw: resp,
|
|
340
|
+
usage,
|
|
341
|
+
toolCalls,
|
|
342
|
+
finishReason: stop === "max_tokens" ? "length" : stop,
|
|
343
|
+
citations,
|
|
344
|
+
serverToolCalls: serverCalls,
|
|
345
|
+
metadata: meta
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Citações dos blocos de texto + execuções de server tools (`server_tool_use`
|
|
350
|
+
* casado com o `*_tool_result` pelo `tool_use_id`). `preservar` diz se o turno
|
|
351
|
+
* tem blocos que precisam voltar intactos no histórico.
|
|
352
|
+
*/
|
|
353
|
+
static nativeOutputs(content) {
|
|
354
|
+
const citations = [];
|
|
355
|
+
const calls = /* @__PURE__ */ new Map();
|
|
356
|
+
const ordem = [];
|
|
357
|
+
let preservar = false;
|
|
358
|
+
let pos = 0;
|
|
359
|
+
for (const b of content) {
|
|
360
|
+
const tipo = get(b, "type");
|
|
361
|
+
if (tipo === "text") {
|
|
362
|
+
const txt = get(b, "text") ?? "";
|
|
363
|
+
const cits = get(b, "citations") ?? [];
|
|
364
|
+
if (cits.length) preservar = true;
|
|
365
|
+
for (const c of cits) {
|
|
366
|
+
const ct = String(get(c, "type") ?? "");
|
|
367
|
+
citations.push({
|
|
368
|
+
url: get(c, "url") ?? null,
|
|
369
|
+
title: get(c, "title") ?? null,
|
|
370
|
+
citedText: get(c, "cited_text") ?? null,
|
|
371
|
+
start: pos,
|
|
372
|
+
end: pos + txt.length,
|
|
373
|
+
source: ct.includes("web") ? "web" : ct || null
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
pos += txt.length;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (typeof tipo === "string" && PLAIN_BLOCKS.has(tipo)) continue;
|
|
380
|
+
preservar = true;
|
|
381
|
+
if (tipo === "server_tool_use") {
|
|
382
|
+
const id = get(b, "id") ?? null;
|
|
383
|
+
const stc = {
|
|
384
|
+
type: get(b, "name") || "server_tool",
|
|
385
|
+
input: get(b, "input"),
|
|
386
|
+
id
|
|
387
|
+
};
|
|
388
|
+
calls.set(id ?? String(calls.size), stc);
|
|
389
|
+
ordem.push(stc);
|
|
390
|
+
} else if (typeof tipo === "string" && tipo.endsWith("_tool_result")) {
|
|
391
|
+
const alvoId = get(b, "tool_use_id");
|
|
392
|
+
const saida = dumpBlock(b).content;
|
|
393
|
+
const alvo = alvoId ? calls.get(alvoId) : void 0;
|
|
394
|
+
if (alvo) alvo.output = saida;
|
|
395
|
+
else ordem.push({
|
|
396
|
+
type: tipo.slice(0, -12),
|
|
397
|
+
output: saida,
|
|
398
|
+
id: alvoId ?? null
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
citations,
|
|
404
|
+
serverCalls: ordem,
|
|
405
|
+
preservar
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
static mergeUsage(total, resp) {
|
|
409
|
+
const u = get(resp, "usage");
|
|
410
|
+
if (!u) return;
|
|
411
|
+
for (const k of [
|
|
412
|
+
"input_tokens",
|
|
413
|
+
"output_tokens",
|
|
414
|
+
"cache_creation_input_tokens",
|
|
415
|
+
"cache_read_input_tokens"
|
|
416
|
+
]) total[k] = (total[k] ?? 0) + n(get(u, k));
|
|
417
|
+
const stu = get(u, "server_tool_use");
|
|
418
|
+
for (const k of ["web_search_requests", "web_fetch_requests"]) total[k] = (total[k] ?? 0) + n(get(stu, k));
|
|
419
|
+
}
|
|
420
|
+
/** Resposta final com o conteúdo de TODAS as rodadas e o usage somado. */
|
|
421
|
+
static finalAfterPause(resp, acum, total) {
|
|
422
|
+
if (!acum.length) return resp;
|
|
423
|
+
AnthropicProvider.mergeUsage(total, resp);
|
|
424
|
+
return {
|
|
425
|
+
content: [...acum, ...get(resp, "content") ?? []],
|
|
426
|
+
usage: {
|
|
427
|
+
input_tokens: total.input_tokens ?? 0,
|
|
428
|
+
output_tokens: total.output_tokens ?? 0,
|
|
429
|
+
cache_creation_input_tokens: total.cache_creation_input_tokens ?? 0,
|
|
430
|
+
cache_read_input_tokens: total.cache_read_input_tokens ?? 0,
|
|
431
|
+
server_tool_use: {
|
|
432
|
+
web_search_requests: total.web_search_requests ?? 0,
|
|
433
|
+
web_fetch_requests: total.web_fetch_requests ?? 0
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
stop_reason: get(resp, "stop_reason"),
|
|
437
|
+
container: get(resp, "container"),
|
|
438
|
+
raw_last: resp
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
static continueAfterPause(kw, resp) {
|
|
442
|
+
const content = (get(resp, "content") ?? []).map(dumpBlock);
|
|
443
|
+
return {
|
|
444
|
+
...kw,
|
|
445
|
+
messages: [...kw.messages, {
|
|
446
|
+
role: "assistant",
|
|
447
|
+
content
|
|
448
|
+
}]
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
async complete(messages, options) {
|
|
452
|
+
const client = await this.client();
|
|
453
|
+
let kw = this.kwargs(messages, options);
|
|
454
|
+
const acum = [];
|
|
455
|
+
const total = {};
|
|
456
|
+
let resp;
|
|
457
|
+
for (let i = 0; i <= MAX_PAUSE; i++) {
|
|
458
|
+
try {
|
|
459
|
+
resp = await client.messages.create(kw, this.reqOptions(options));
|
|
460
|
+
} catch (e) {
|
|
461
|
+
throw this.err(e, options);
|
|
462
|
+
}
|
|
463
|
+
if (get(resp, "stop_reason") !== "pause_turn") break;
|
|
464
|
+
acum.push(...get(resp, "content") ?? []);
|
|
465
|
+
AnthropicProvider.mergeUsage(total, resp);
|
|
466
|
+
kw = AnthropicProvider.continueAfterPause(kw, resp);
|
|
467
|
+
}
|
|
468
|
+
return this.completion(AnthropicProvider.finalAfterPause(resp, acum, total));
|
|
469
|
+
}
|
|
470
|
+
toolKwargs(messages, schema, options) {
|
|
471
|
+
const kw = this.kwargs(messages, {
|
|
472
|
+
...options,
|
|
473
|
+
tools: void 0,
|
|
474
|
+
toolChoice: void 0,
|
|
475
|
+
mcpServers: void 0
|
|
476
|
+
});
|
|
477
|
+
const extract = {
|
|
478
|
+
name: "extract",
|
|
479
|
+
description: `Retorne um objeto ${schema.name} estruturado.`,
|
|
480
|
+
input_schema: schema.jsonSchema
|
|
481
|
+
};
|
|
482
|
+
if (this.forbidsForcedTools()) {
|
|
483
|
+
extract.strict = true;
|
|
484
|
+
extract.input_schema = strictSchema(extract.input_schema);
|
|
485
|
+
kw.tool_choice = { type: "auto" };
|
|
486
|
+
} else kw.tool_choice = {
|
|
487
|
+
type: "tool",
|
|
488
|
+
name: "extract"
|
|
489
|
+
};
|
|
490
|
+
kw.tools = [extract];
|
|
491
|
+
delete kw.thinking;
|
|
492
|
+
return kw;
|
|
493
|
+
}
|
|
494
|
+
async parse(messages, schema, options) {
|
|
495
|
+
const client = await this.client();
|
|
496
|
+
const kw = this.toolKwargs(messages, schema, options);
|
|
497
|
+
let resp;
|
|
498
|
+
try {
|
|
499
|
+
resp = await client.messages.create(kw, { signal: options.signal });
|
|
500
|
+
} catch (e) {
|
|
501
|
+
throw this.err(e, options);
|
|
502
|
+
}
|
|
503
|
+
const comp = this.completion(resp);
|
|
504
|
+
Provider.checkTruncation(comp, this.name);
|
|
505
|
+
const toolUse = (get(resp, "content") ?? []).find((b) => get(b, "type") === "tool_use");
|
|
506
|
+
if (!toolUse) throw new OutputValidationError(`[${this.name}] o modelo não devolveu o objeto estruturado (stop_reason=${JSON.stringify(comp.finishReason ?? "sem tool_use")}): ${JSON.stringify(comp.text.slice(0, 200))}`, { provider: this.name });
|
|
507
|
+
const input = get(toolUse, "input");
|
|
508
|
+
comp.text = JSON.stringify(input);
|
|
509
|
+
comp.parsed = await schema.validate(input);
|
|
510
|
+
comp.toolCalls = [];
|
|
511
|
+
return comp;
|
|
512
|
+
}
|
|
513
|
+
async *stream(messages, options) {
|
|
514
|
+
for await (const ev of this.streamEvents(messages, options)) if (ev.type === "text") yield ev.text;
|
|
515
|
+
}
|
|
516
|
+
async *streamEvents(messages, options) {
|
|
517
|
+
const client = await this.client();
|
|
518
|
+
let kw = this.kwargs(messages, options);
|
|
519
|
+
const acum = [];
|
|
520
|
+
const total = {};
|
|
521
|
+
let resp;
|
|
522
|
+
for (let i = 0; i <= MAX_PAUSE; i++) {
|
|
523
|
+
try {
|
|
524
|
+
const s = client.messages.stream(kw, this.reqOptions(options));
|
|
525
|
+
for await (const ev of s) {
|
|
526
|
+
if (get(ev, "type") !== "content_block_delta") continue;
|
|
527
|
+
const delta = get(ev, "delta");
|
|
528
|
+
if (get(delta, "type") === "text_delta") {
|
|
529
|
+
const t = get(delta, "text");
|
|
530
|
+
if (typeof t === "string" && t) yield {
|
|
531
|
+
type: "text",
|
|
532
|
+
text: t
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
resp = await s.finalMessage();
|
|
537
|
+
} catch (e) {
|
|
538
|
+
throw this.err(e, options);
|
|
539
|
+
}
|
|
540
|
+
if (get(resp, "stop_reason") !== "pause_turn") break;
|
|
541
|
+
acum.push(...get(resp, "content") ?? []);
|
|
542
|
+
AnthropicProvider.mergeUsage(total, resp);
|
|
543
|
+
kw = AnthropicProvider.continueAfterPause(kw, resp);
|
|
544
|
+
}
|
|
545
|
+
yield {
|
|
546
|
+
type: "done",
|
|
547
|
+
text: "",
|
|
548
|
+
completion: this.completion(AnthropicProvider.finalAfterPause(resp, acum, total))
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
err(e, options) {
|
|
552
|
+
return options.signal?.aborted ? e : classify(e, this.name);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
export { AnthropicProvider };
|