@ggui-ai/negotiator 0.28.0 → 0.29.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.
@@ -1,86 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Repair bench CLI — live round-trip-quality probe.
4
- *
5
- * Runs the production forgiving-handshake create-path
6
- * (`ensureConformingContract`) over REPAIR_CORPUS and reports the
7
- * round-trip-usable rate — the contract-quality number the shape bench
8
- * (bench-synth) cannot see. Reads ~/.ggui/credentials.json (or
9
- * ANTHROPIC_API_KEY) for the key.
10
- *
11
- * Costs ~$0.001 per repaired entry on Haiku 4.5 (clean drafts hit the
12
- * lint-clean fast path and make NO LLM call). NOT run in CI; opt-in.
13
- *
14
- * Usage:
15
- * pnpm -F @ggui-ai/negotiator bench-repair
16
- * pnpm -F @ggui-ai/negotiator bench-repair -- --limit 1
17
- * pnpm -F @ggui-ai/negotiator bench-repair -- --json > repair-report.json
18
- * ANTHROPIC_API_KEY=sk-... pnpm -F @ggui-ai/negotiator bench-repair
19
- *
20
- * Bench-only — not exported from the package index.
21
- */
22
- import { evaluateRepairCorpus, formatRepairBenchReport, } from './run-repair-bench.js';
23
- import { DEFAULT_MODEL, HAIKU_4_5_PRICE_INPUT_PER_TOKEN, HAIKU_4_5_PRICE_OUTPUT_PER_TOKEN, buildAnthropicLlmCaller, getTokenUsage, resolveAnthropicKey, } from './cli-llm.js';
24
- function parseArgs(argv) {
25
- let limit;
26
- let model = DEFAULT_MODEL;
27
- let json = false;
28
- for (let i = 0; i < argv.length; i++) {
29
- const a = argv[i];
30
- if (a === '--limit' && argv[i + 1]) {
31
- limit = Number(argv[++i]);
32
- }
33
- else if (a === '--model' && argv[i + 1]) {
34
- model = argv[++i];
35
- }
36
- else if (a === '--json') {
37
- json = true;
38
- }
39
- }
40
- const result = { model, json };
41
- if (limit !== undefined)
42
- result.limit = limit;
43
- return result;
44
- }
45
- async function main() {
46
- const args = parseArgs(process.argv.slice(2));
47
- const apiKey = resolveAnthropicKey('bench-repair');
48
- const llm = buildAnthropicLlmCaller(apiKey, args.model);
49
- if (!args.json) {
50
- process.stdout.write(`bench-repair: model=${args.model}\n\n`);
51
- }
52
- const report = await evaluateRepairCorpus({ llm }, {
53
- ...(args.limit !== undefined ? { limit: args.limit } : {}),
54
- onProgress: args.json
55
- ? undefined
56
- : (outcome, idx, total) => {
57
- const pass = outcome.roundTrip !== null
58
- ? outcome.roundTrip.pass
59
- : outcome.shape.pass;
60
- const status = pass ? 'OK ' : 'NO ';
61
- const id = outcome.entry.id.padEnd(22);
62
- const origin = `origin:${outcome.origin}`.padEnd(13);
63
- process.stdout.write(`[${status}] ${(idx + 1).toString().padStart(2)}/${total} ${origin} ${id} ${outcome.latencyMs}ms\n`);
64
- },
65
- });
66
- if (args.json) {
67
- process.stdout.write(JSON.stringify(report, null, 2));
68
- process.stdout.write('\n');
69
- return;
70
- }
71
- process.stdout.write('\n');
72
- process.stdout.write(formatRepairBenchReport(report));
73
- process.stdout.write('\n\n');
74
- const usage = getTokenUsage();
75
- const totalCost = usage.input * HAIKU_4_5_PRICE_INPUT_PER_TOKEN +
76
- usage.output * HAIKU_4_5_PRICE_OUTPUT_PER_TOKEN;
77
- // Clean drafts (origin agent) make no LLM call — cost is per repaired entry.
78
- const callsMade = report.totals.originSynth;
79
- const costPerCall = callsMade === 0 ? 0 : totalCost / callsMade;
80
- process.stdout.write(`Tokens: input=${usage.input} output=${usage.output}\n`);
81
- process.stdout.write(`Cost: total=$${totalCost.toFixed(4)} per-repair=$${costPerCall.toFixed(4)}\n`);
82
- }
83
- main().catch((err) => {
84
- process.stderr.write(`bench-repair failed: ${err instanceof Error ? err.message : String(err)}\n`);
85
- process.exit(1);
86
- });
@@ -1,103 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Rerank quality probe CLI.
4
- *
5
- * Reads `~/.ggui/credentials.json` for the Anthropic API key, runs
6
- * the probe against `claude-haiku-5-5` (ggui#1804), prints the report. Its readings are not comparable to runs made before the switch.
7
- *
8
- * Usage:
9
- * pnpm -F @ggui-ai/negotiator probe-rerank
10
- * ANTHROPIC_API_KEY=sk-... pnpm -F @ggui-ai/negotiator probe-rerank
11
- * pnpm -F @ggui-ai/negotiator probe-rerank -- --limit 5
12
- *
13
- * Cost: ~$0.025 for the full 25-pair run with Haiku 4.5.
14
- *
15
- * Eval-only — not exported from the package index.
16
- */
17
- import { runProbe, formatReport } from './run-probe.js';
18
- import {
19
- buildAnthropicLlmCaller,
20
- getTokenUsage,
21
- resolveAnthropicKey,
22
- } from '../synth-bench/cli-llm.js';
23
-
24
- const DEFAULT_MODEL = 'claude-haiku-5-5';
25
-
26
- function parseArgs(argv: readonly string[]): { limit?: number; threshold?: number; model: string } {
27
- let limit: number | undefined;
28
- let threshold: number | undefined;
29
- let model = DEFAULT_MODEL;
30
- for (let i = 0; i < argv.length; i++) {
31
- const a = argv[i];
32
- if (a === '--limit' && argv[i + 1]) {
33
- limit = Number(argv[++i]);
34
- } else if (a === '--threshold' && argv[i + 1]) {
35
- threshold = Number(argv[++i]);
36
- } else if (a === '--model' && argv[i + 1]) {
37
- model = argv[++i]!;
38
- }
39
- }
40
- const result: { limit?: number; threshold?: number; model: string } = { model };
41
- if (limit !== undefined) result.limit = limit;
42
- if (threshold !== undefined) result.threshold = threshold;
43
- return result;
44
- }
45
-
46
- // Approximate Haiku 4.5 pricing as of 2026-05.
47
- // Input: $1.00 / Mtok, output: $5.00 / Mtok.
48
- const HAIKU_4_5_PRICE_INPUT_PER_TOKEN = 1.0 / 1_000_000;
49
- const HAIKU_4_5_PRICE_OUTPUT_PER_TOKEN = 5.0 / 1_000_000;
50
-
51
- async function main(): Promise<void> {
52
- const args = parseArgs(process.argv.slice(2));
53
- const apiKey = resolveAnthropicKey('probe-rerank');
54
- const llm = buildAnthropicLlmCaller(apiKey, args.model);
55
-
56
- process.stdout.write(`probe: model=${args.model}\n\n`);
57
- const report = await runProbe(
58
- { llm },
59
- {
60
- ...(args.limit !== undefined ? { limit: args.limit } : {}),
61
- ...(args.threshold !== undefined ? { threshold: args.threshold } : {}),
62
- onProgress: (outcome, idx, total) => {
63
- const status = outcome.correct ? 'OK ' : 'NO ';
64
- const conf = outcome.decision.confidence.toFixed(2);
65
- const id = outcome.pair.id.padEnd(24);
66
- process.stdout.write(
67
- `[${status}] ${(idx + 1).toString().padStart(2)}/${total} ${id} conf=${conf} pred=${outcome.predictedMatchId ?? 'null'} gold=${outcome.pair.goldMatchId ?? 'null'}\n`,
68
- );
69
- },
70
- },
71
- );
72
-
73
- process.stdout.write('\n');
74
- process.stdout.write(formatReport(report));
75
- process.stdout.write('\n\n');
76
-
77
- const usage = getTokenUsage();
78
- const totalCost =
79
- usage.input * HAIKU_4_5_PRICE_INPUT_PER_TOKEN +
80
- usage.output * HAIKU_4_5_PRICE_OUTPUT_PER_TOKEN;
81
- const callsMade = report.outcomes.filter(
82
- // ggui#1235 — `reason` is optional on the seam; a judge with no prose
83
- // still made a call.
84
- (o) => !/short-circuited/.test(o.decision.reason ?? ''),
85
- ).length;
86
- const costPerCall = callsMade === 0 ? 0 : totalCost / callsMade;
87
- process.stdout.write(
88
- `Tokens: input=${usage.input} · output=${usage.output}\n`,
89
- );
90
- process.stdout.write(
91
- `Cost: total=$${totalCost.toFixed(4)} · per-call=$${costPerCall.toFixed(4)}\n`,
92
- );
93
- process.stdout.write(
94
- ` G4 cost ≤ $0.002/call → ${costPerCall <= 0.002 ? 'PASS' : 'FAIL'} ($${costPerCall.toFixed(4)})\n`,
95
- );
96
- }
97
-
98
- main().catch((err) => {
99
- process.stderr.write(
100
- `probe-rerank failed: ${err instanceof Error ? err.message : String(err)}\n`,
101
- );
102
- process.exit(1);
103
- });
@@ -1,213 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Blueprint cache-key stability probe.
4
- *
5
- * The handshake blueprint cache hits ONLY on exact canonical-contract
6
- * equality: `blueprintKey(contract)` = sha256 of `canonicalizeContracts`
7
- * (RFC 8785), which preserves spec placement, action/slot/tool NAMES and
8
- * schema shapes (it only normalizes key order / whitespace and strips
9
- * `description`/`usage`). The semantic/fuzzy match is disabled whenever a
10
- * contract is present. So two requests for the SAME intent reuse a cached
11
- * blueprint only if the LLM authored a byte-identical canonical contract
12
- * both times.
13
- *
14
- * This probe synthesizes one intent N times and reports:
15
- * 1. how many DISTINCT blueprintKeys result (distinct > 1 ⇒ the
16
- * exact-key cache structurally cannot hit on re-runs);
17
- * 2. the KEY-AFFECTING diff — every leaf path where the runs disagree
18
- * (descriptions/usage stripped, since the key ignores them) — so we
19
- * can see WHETHER the variance is trivial (normalizable: required
20
- * order, additionalProperties, optional fields) or semantic
21
- * (property names / structure → only a semantic match can bridge it).
22
- *
23
- * Usage:
24
- * pnpm -F @ggui-ai/negotiator cache-probe -- --intent "my todo items" --n 5
25
- * pnpm -F @ggui-ai/negotiator cache-probe -- --n 5 --dump /tmp/cache-probe.json
26
- */
27
- import { writeFileSync } from 'node:fs';
28
- import { blueprintKey } from '@ggui-ai/protocol/blueprint-key';
29
- import type { DataContract } from '@ggui-ai/protocol';
30
- import { synthesizeContract } from '../synthesize-contract.js';
31
- import {
32
- DEFAULT_MODEL,
33
- buildAnthropicLlmCaller,
34
- resolveAnthropicKey,
35
- } from './cli-llm.js';
36
-
37
- interface Args {
38
- intent: string;
39
- n: number;
40
- model: string;
41
- dump?: string;
42
- }
43
-
44
- function parseArgs(argv: readonly string[]): Args {
45
- let intent = 'my todo items';
46
- let n = 3;
47
- let model = DEFAULT_MODEL;
48
- let dump: string | undefined;
49
- for (let i = 0; i < argv.length; i++) {
50
- const a = argv[i];
51
- if (a === '--intent' && argv[i + 1]) intent = argv[++i]!;
52
- else if (a === '--n' && argv[i + 1]) n = Number(argv[++i]);
53
- else if (a === '--model' && argv[i + 1]) model = argv[++i]!;
54
- else if (a === '--dump' && argv[i + 1]) dump = argv[++i]!;
55
- }
56
- return { intent, n, model, ...(dump !== undefined ? { dump } : {}) };
57
- }
58
-
59
- /** One-line canonical-surface summary — the bits blueprintKey is sensitive to. */
60
- function shapeSummary(c: DataContract): string {
61
- const parts: string[] = [];
62
- const props = Object.keys(c.propsSpec?.properties ?? {});
63
- const ctx = Object.keys(c.contextSpec ?? {});
64
- const act = Object.keys(c.actionSpec ?? {});
65
- const str = Object.keys(c.streamSpec ?? {});
66
- const tools = Object.keys(c.agentCapabilities?.tools ?? {});
67
- if (props.length) parts.push(`props{${props.join(',')}}`);
68
- if (ctx.length) parts.push(`context{${ctx.join(',')}}`);
69
- if (act.length) parts.push(`action{${act.join(',')}}`);
70
- if (str.length) parts.push(`stream{${str.join(',')}}`);
71
- if (tools.length) parts.push(`tools{${tools.join(',')}}`);
72
- return parts.join(' ') || '(empty)';
73
- }
74
-
75
- /**
76
- * Flatten a contract to `path → json-encoded leaf`, applying the same
77
- * domain rule the cache key does: drop string-valued `description` /
78
- * `usage` (informational — `canonicalizeContracts` strips them). Objects
79
- * recurse; arrays are encoded whole (order is key-affecting under JCS,
80
- * which sorts object keys but NOT array elements).
81
- */
82
- function flattenKeyAffecting(
83
- value: unknown,
84
- prefix: string,
85
- out: Map<string, string>,
86
- ): void {
87
- if (Array.isArray(value)) {
88
- out.set(prefix, JSON.stringify(value));
89
- return;
90
- }
91
- if (value !== null && typeof value === 'object') {
92
- for (const [k, v] of Object.entries(value)) {
93
- if ((k === 'description' || k === 'usage') && typeof v === 'string') {
94
- continue; // stripped by the cache key
95
- }
96
- flattenKeyAffecting(v, prefix ? `${prefix}.${k}` : k, out);
97
- }
98
- return;
99
- }
100
- out.set(prefix, JSON.stringify(value));
101
- }
102
-
103
- /** Every leaf path where the N contracts do not all agree, with the
104
- * distinct per-run values (or `∅` when a run omits the path). */
105
- function crossRunDiff(
106
- contracts: readonly DataContract[],
107
- ): Array<{ path: string; values: string[] }> {
108
- const flat = contracts.map((c) => {
109
- const m = new Map<string, string>();
110
- flattenKeyAffecting(c, '', m);
111
- return m;
112
- });
113
- const allPaths = new Set<string>();
114
- for (const m of flat) for (const p of m.keys()) allPaths.add(p);
115
-
116
- const divergent: Array<{ path: string; values: string[] }> = [];
117
- for (const path of [...allPaths].sort()) {
118
- const values = flat.map((m) => m.get(path) ?? '∅');
119
- if (new Set(values).size > 1) divergent.push({ path, values });
120
- }
121
- return divergent;
122
- }
123
-
124
- async function main(): Promise<void> {
125
- const args = parseArgs(process.argv.slice(2));
126
- const llm = buildAnthropicLlmCaller(resolveAnthropicKey('cache-probe'), args.model);
127
- process.stdout.write(
128
- `cache-probe: intent=${JSON.stringify(args.intent)} n=${args.n} model=${args.model}\n\n`,
129
- );
130
-
131
- const contracts: DataContract[] = [];
132
- const keys: string[] = [];
133
- for (let i = 0; i < args.n; i++) {
134
- const synth = await synthesizeContract({ llm }, args.intent);
135
- if (synth.contract === null) {
136
- process.stdout.write(` run ${i + 1}: synth declined — ${synth.reason}\n`);
137
- continue;
138
- }
139
- const key = blueprintKey(synth.contract);
140
- contracts.push(synth.contract);
141
- keys.push(key);
142
- process.stdout.write(
143
- ` run ${i + 1}: ${key} ${shapeSummary(synth.contract)}\n`,
144
- );
145
- }
146
-
147
- const distinct = new Set(keys);
148
- process.stdout.write(
149
- `\nDistinct blueprintKeys: ${distinct.size}/${keys.length}\n`,
150
- );
151
- if (distinct.size <= 1 && keys.length > 1) {
152
- process.stdout.write(
153
- 'STABLE → the exact-key cache WOULD hit on a re-run of this intent.\n',
154
- );
155
- } else if (keys.length > 1) {
156
- process.stdout.write(
157
- 'UNSTABLE → same intent yields different canonical contracts → the exact-key\n' +
158
- 'cache cannot hit; each request cold-gens a new blueprint.\n',
159
- );
160
- }
161
-
162
- // Key-affecting cross-run diff — the "what varies" the user asked for.
163
- if (contracts.length > 1) {
164
- const diff = crossRunDiff(contracts);
165
- process.stdout.write(
166
- `\n=== KEY-AFFECTING DIFF (${diff.length} divergent paths; descriptions stripped) ===\n`,
167
- );
168
- for (const { path, values } of diff) {
169
- process.stdout.write(` ${path}\n`);
170
- values.forEach((v, i) => {
171
- const capped = v.length > 100 ? `${v.slice(0, 100)}…` : v;
172
- process.stdout.write(` run${i + 1}: ${capped}\n`);
173
- });
174
- }
175
- // Heuristic taxonomy hint.
176
- const isArrayOrderOnly = diff.every(({ values }) => {
177
- const parsed = values
178
- .filter((v) => v !== '∅')
179
- .map((v) => {
180
- try {
181
- return JSON.stringify([...(JSON.parse(v) as unknown[])].sort());
182
- } catch {
183
- return v;
184
- }
185
- });
186
- return new Set(parsed).size === 1;
187
- });
188
- process.stdout.write(
189
- `\nHint: ${
190
- diff.length === 0
191
- ? 'no key-affecting differences — variance must be elsewhere'
192
- : isArrayOrderOnly
193
- ? 'ALL divergent paths are arrays differing only in ORDER → a canonical sort in canonicalizeContracts would collapse these keys (cheap fix, no semantic match needed).'
194
- : 'divergent paths include non-array-order differences (names / types / structure / presence) → a semantic intent-match is needed to bridge them (canonicalization alone cannot).'
195
- }\n`,
196
- );
197
- }
198
-
199
- if (args.dump !== undefined) {
200
- writeFileSync(
201
- args.dump,
202
- JSON.stringify({ intent: args.intent, model: args.model, keys, contracts }, null, 2),
203
- );
204
- process.stdout.write(`\nDumped ${contracts.length} contracts → ${args.dump}\n`);
205
- }
206
- }
207
-
208
- main().catch((err) => {
209
- process.stderr.write(
210
- `cache-probe failed: ${err instanceof Error ? err.message : String(err)}\n`,
211
- );
212
- process.exit(1);
213
- });
@@ -1,168 +0,0 @@
1
- /**
2
- * Shared CLI LLM plumbing for the synth-bench family of live probes
3
- * (bench-synth, bench-repair). Builds an Anthropic-backed
4
- * {@link LLMCaller} with forced tool-use, resolves the API key from env
5
- * or `~/.ggui/credentials.json`, and accumulates token usage for the
6
- * per-run cost line. Bench-only — not exported from the package index.
7
- */
8
- import { readFileSync } from 'node:fs';
9
- import { homedir } from 'node:os';
10
- import { resolve as pathResolve } from 'node:path';
11
- import type { LLMCaller, ToolSchema } from '../llm-caller.js';
12
- import { anthropicRejectsForcedToolChoice } from '@ggui-ai/protocol';
13
-
14
- const ANTHROPIC_API = 'https://api.anthropic.com/v1/messages';
15
-
16
- /** Default bench model — the model the `ui-gen-default` seed generator declares. */
17
- export const DEFAULT_MODEL = 'claude-haiku-5-5';
18
-
19
- /** Haiku 4.5 token pricing (USD per token) for the cost report. */
20
- export const HAIKU_4_5_PRICE_INPUT_PER_TOKEN = 1.0 / 1_000_000;
21
- export const HAIKU_4_5_PRICE_OUTPUT_PER_TOKEN = 5.0 / 1_000_000;
22
-
23
- interface CredsFile {
24
- apps?: { global?: { anthropic?: string } };
25
- }
26
-
27
- /**
28
- * Resolve the Anthropic API key: `ANTHROPIC_API_KEY` env var first, then
29
- * `~/.ggui/credentials.json` at `apps.global.anthropic`. `scriptName`
30
- * prefixes the error hints so a failure names the bench that needs it.
31
- */
32
- export function resolveAnthropicKey(scriptName: string): string {
33
- const envKey = process.env['ANTHROPIC_API_KEY'];
34
- if (envKey && envKey.length > 0) return envKey;
35
- const credsPath = pathResolve(homedir(), '.ggui', 'credentials.json');
36
- let parsed: CredsFile;
37
- try {
38
- parsed = JSON.parse(readFileSync(credsPath, 'utf8')) as CredsFile;
39
- } catch (err) {
40
- throw new Error(
41
- `${scriptName}: could not read ${credsPath} (${err instanceof Error ? err.message : String(err)}). Set ANTHROPIC_API_KEY env var or run \`ggui auth set anthropic\`.`,
42
- );
43
- }
44
- const key = parsed.apps?.global?.anthropic;
45
- if (typeof key !== 'string' || key.length === 0) {
46
- throw new Error(
47
- `${scriptName}: no anthropic key found at apps.global.anthropic in ${credsPath}.`,
48
- );
49
- }
50
- return key;
51
- }
52
-
53
- interface AnthropicContentBlock {
54
- type: string;
55
- name?: string;
56
- input?: unknown;
57
- text?: string;
58
- }
59
-
60
- interface AnthropicResponse {
61
- content?: AnthropicContentBlock[];
62
- usage?: { input_tokens?: number; output_tokens?: number };
63
- stop_reason?: string;
64
- error?: { type?: string; message?: string };
65
- }
66
-
67
- let totalInputTokens = 0;
68
- let totalOutputTokens = 0;
69
-
70
- /** Running token usage accumulated across `callStructured` calls in this
71
- * process — read after a run for the cost line. */
72
- export function getTokenUsage(): {
73
- readonly input: number;
74
- readonly output: number;
75
- } {
76
- return { input: totalInputTokens, output: totalOutputTokens };
77
- }
78
-
79
- /**
80
- * Thinking budget added to the caller's answer budget for a model that
81
- * refuses a forced tool (ggui#1264). Same value and reason as the
82
- * negotiator's production caller in `@ggui-ai/mcp-server`; the three
83
- * copies are consolidated under ggui#1271.
84
- */
85
- const ALWAYS_THINKING_HEADROOM_TOKENS = 16_000;
86
-
87
- /**
88
- * The dev caller the synth benches and the rerank probe CLI share
89
- * (Haiku-pinned by default; every CLI takes `--model`). Text mode is not
90
- * exercised — every consumer uses `callStructured`.
91
- */
92
- export function buildAnthropicLlmCaller(
93
- apiKey: string,
94
- model: string,
95
- ): LLMCaller {
96
- return {
97
- async call(): Promise<string> {
98
- throw new Error(
99
- 'negotiator dev caller: text-mode not exercised — use callStructured',
100
- );
101
- },
102
- async callStructured(
103
- systemPrompt: string,
104
- userMessage: string,
105
- tool: ToolSchema,
106
- maxTokens?: number,
107
- ): Promise<unknown> {
108
- // `temperature` deprecated on Haiku 4.5+ — Anthropic rejects with
109
- // HTTP 400. Residual stochasticity stays bounded via canonical-key
110
- // normalization downstream.
111
- //
112
- // ggui#1264 — a forced `tool_choice` only where the model accepts
113
- // one. The always-thinking models (`@ggui-ai/protocol`
114
- // `anthropicRejectsForcedToolChoice`) 400 on it, so they get
115
- // `auto` + at most one call + the tool named in the system prompt,
116
- // and thinking headroom on top of the answer budget (their thinking
117
- // counts against `max_tokens` and comes first).
118
- const refusesForcedTool = anthropicRejectsForcedToolChoice(model);
119
- const answerBudget = maxTokens ?? 1024;
120
- const body = {
121
- model,
122
- max_tokens: refusesForcedTool
123
- ? answerBudget + ALWAYS_THINKING_HEADROOM_TOKENS
124
- : answerBudget,
125
- system: refusesForcedTool
126
- ? `${systemPrompt}\n\nAnswer by calling the \`${tool.name}\` tool exactly once. Do not answer in text.`
127
- : systemPrompt,
128
- messages: [{ role: 'user', content: userMessage }],
129
- tools: [
130
- {
131
- name: tool.name,
132
- description: tool.description,
133
- input_schema: tool.input_schema,
134
- },
135
- ],
136
- tool_choice: refusesForcedTool
137
- ? { type: 'auto', disable_parallel_tool_use: true }
138
- : { type: 'tool', name: tool.name },
139
- };
140
- const res = await fetch(ANTHROPIC_API, {
141
- method: 'POST',
142
- headers: {
143
- 'content-type': 'application/json',
144
- 'x-api-key': apiKey,
145
- 'anthropic-version': '2023-06-01',
146
- },
147
- body: JSON.stringify(body),
148
- });
149
- const json = (await res.json()) as AnthropicResponse;
150
- if (!res.ok) {
151
- const errType = json.error?.type ?? 'unknown';
152
- const errMsg = json.error?.message ?? `HTTP ${res.status}`;
153
- throw new Error(`anthropic ${errType}: ${errMsg}`);
154
- }
155
- if (json.usage) {
156
- totalInputTokens += json.usage.input_tokens ?? 0;
157
- totalOutputTokens += json.usage.output_tokens ?? 0;
158
- }
159
- const toolBlock = json.content?.find((b) => b.type === 'tool_use');
160
- if (!toolBlock || toolBlock.input === undefined) {
161
- throw new Error(
162
- `anthropic: no tool_use block in response (stop_reason=${json.stop_reason ?? 'unknown'})`,
163
- );
164
- }
165
- return toolBlock.input;
166
- },
167
- };
168
- }