@rudra-js/anthropic 0.7.4 → 0.7.6
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/README.md +7 -1
- package/dist/anthropic-provider.d.ts.map +1 -1
- package/dist/anthropic-provider.js +87 -84
- package/dist/anthropic-provider.js.map +1 -1
- package/package.json +8 -6
- package/src/anthropic-provider.ts +103 -119
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @rudra-js/anthropic
|
|
2
2
|
|
|
3
|
+
This package is part of [rudra-js](https://rudrajs.com).
|
|
4
|
+
|
|
5
|
+
rudra-js is an open-source TypeScript library (MIT) you install from npm. A language model designs one product-recommendation block for a page in your shop, rudra-js checks it against your catalog, and React renders it.
|
|
6
|
+
|
|
7
|
+
It is not a hosted platform, a visual or low-code app builder, or a workspace you sign up for.
|
|
8
|
+
|
|
3
9
|
An Anthropic adapter for
|
|
4
10
|
[`@rudra-js/core`](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/core)'s
|
|
5
11
|
`ComponentProvider`.
|
|
@@ -118,7 +124,7 @@ anything.
|
|
|
118
124
|
When the API answers with an error, the `Error` we throw carries the status code
|
|
119
125
|
and the vendor's error category, like `anthropic responded 400
|
|
120
126
|
(invalid_request_error)`. We leave the vendor's own message out. It quotes the
|
|
121
|
-
request back, and
|
|
127
|
+
request back, and in rudra-js the request can hold a shopper's search
|
|
122
128
|
terms, which an adopter's `console.error(error)` would then write to a log. A
|
|
123
129
|
test in `anthropic-provider.test.ts` puts a search term in that message and
|
|
124
130
|
asserts it does not reach the thrown error.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anthropic-provider.d.ts","sourceRoot":"","sources":["../src/anthropic-provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,iBAAiB,EAIlB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"anthropic-provider.d.ts","sourceRoot":"","sources":["../src/anthropic-provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,iBAAiB,EAIlB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,UAAU,GAAG,UAAU,CAAA;KAAE,GAAG,IAAI,CAAC;IACpD,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AAqCD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,GAAG,iBAAiB,CA2D5F"}
|
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
const TOOL_NAME = 'emit_component_spec';
|
|
3
|
-
const DEFAULT_MAX_TOKENS = 8192;
|
|
4
3
|
function describeShape(input) {
|
|
5
4
|
if (input === null)
|
|
6
5
|
return 'null';
|
|
7
6
|
if (Array.isArray(input))
|
|
8
7
|
return `an array of ${input.length}`;
|
|
9
8
|
if (typeof input !== 'object')
|
|
10
|
-
return
|
|
9
|
+
return typeof input;
|
|
11
10
|
const keys = Object.keys(input);
|
|
12
11
|
return keys.length === 0 ? 'an empty object' : `an object with keys ${keys.join(', ')}`;
|
|
13
12
|
}
|
|
14
|
-
function
|
|
13
|
+
function unwrapSingleKey(input) {
|
|
15
14
|
if (typeof input !== 'object' || input === null || Array.isArray(input))
|
|
16
15
|
return undefined;
|
|
17
16
|
const values = Object.values(input);
|
|
18
17
|
return values.length === 1 ? values[0] : undefined;
|
|
19
18
|
}
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
function isSpecToolUse(block) {
|
|
20
|
+
if (typeof block !== 'object' || block === null)
|
|
21
|
+
return false;
|
|
22
|
+
const fields = block;
|
|
23
|
+
return fields['type'] === 'tool_use' && fields['name'] === TOOL_NAME;
|
|
25
24
|
}
|
|
26
25
|
export function createAnthropicProvider(options) {
|
|
27
26
|
const model = options.model ?? 'claude-sonnet-5';
|
|
@@ -30,76 +29,78 @@ export function createAnthropicProvider(options) {
|
|
|
30
29
|
let baseUrl = options.baseUrl ?? 'https://api.anthropic.com';
|
|
31
30
|
while (baseUrl.endsWith('/'))
|
|
32
31
|
baseUrl = baseUrl.slice(0, -1);
|
|
33
|
-
const maxTokens = options.maxTokens ??
|
|
32
|
+
const maxTokens = options.maxTokens ?? 8192;
|
|
34
33
|
return {
|
|
35
34
|
name: 'anthropic',
|
|
36
35
|
model,
|
|
37
36
|
async generate(request) {
|
|
38
37
|
request.signal.throwIfAborted();
|
|
38
|
+
const headers = {
|
|
39
|
+
'content-type': 'application/json',
|
|
40
|
+
'x-api-key': options.apiKey,
|
|
41
|
+
'anthropic-version': '2023-06-01',
|
|
42
|
+
...(options.workspaceId ? { 'anthropic-workspace-id': options.workspaceId } : {}),
|
|
43
|
+
};
|
|
44
|
+
const body = {
|
|
45
|
+
model,
|
|
46
|
+
max_tokens: maxTokens,
|
|
47
|
+
...(thinking ? { thinking } : {}),
|
|
48
|
+
system: [
|
|
49
|
+
{ type: 'text', text: request.system, cache_control: { type: 'ephemeral' } },
|
|
50
|
+
{
|
|
51
|
+
type: 'text',
|
|
52
|
+
text: `Return that JSON as the input to the ${TOOL_NAME} tool, not as text. You may say a brief sentence first.`,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
messages: [{ role: 'user', content: request.user }],
|
|
56
|
+
tools: [
|
|
57
|
+
{
|
|
58
|
+
name: TOOL_NAME,
|
|
59
|
+
description: 'Return the component specification.',
|
|
60
|
+
input_schema: z.toJSONSchema(request.schema, { io: 'input' }),
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
tool_choice: { type: 'auto', disable_parallel_tool_use: true },
|
|
64
|
+
};
|
|
39
65
|
const response = await send(call, `${baseUrl}/v1/messages`, {
|
|
40
66
|
method: 'POST',
|
|
41
|
-
headers
|
|
42
|
-
'content-type': 'application/json',
|
|
43
|
-
'x-api-key': options.apiKey,
|
|
44
|
-
'anthropic-version': '2023-06-01',
|
|
45
|
-
...(options.workspaceId ? { 'anthropic-workspace-id': options.workspaceId } : {}),
|
|
46
|
-
},
|
|
67
|
+
headers,
|
|
47
68
|
signal: request.signal,
|
|
48
|
-
body: JSON.stringify(
|
|
49
|
-
model,
|
|
50
|
-
max_tokens: maxTokens,
|
|
51
|
-
...(thinking ? { thinking } : {}),
|
|
52
|
-
system: [
|
|
53
|
-
{ type: 'text', text: request.system, cache_control: { type: 'ephemeral' } },
|
|
54
|
-
{
|
|
55
|
-
type: 'text',
|
|
56
|
-
text: `Return that JSON as the input to the ${TOOL_NAME} tool, not as text. You may say a brief sentence first.`,
|
|
57
|
-
},
|
|
58
|
-
],
|
|
59
|
-
messages: [{ role: 'user', content: request.user }],
|
|
60
|
-
tools: [
|
|
61
|
-
{
|
|
62
|
-
name: TOOL_NAME,
|
|
63
|
-
description: 'Return the component specification.',
|
|
64
|
-
input_schema: z.toJSONSchema(request.schema, { io: 'input' }),
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
// Opus 5.5, Sonnet 5.5, Fable 5.1 and Mythos 5.1 reject a forced tool call with a 400.
|
|
68
|
-
tool_choice: { type: 'auto', disable_parallel_tool_use: true },
|
|
69
|
-
}),
|
|
69
|
+
body: JSON.stringify(body),
|
|
70
70
|
});
|
|
71
71
|
if (!response.ok) {
|
|
72
|
-
const
|
|
73
|
-
throw new Error(`anthropic responded ${response.status}${
|
|
72
|
+
const suffix = await errorTypeSuffix(response);
|
|
73
|
+
throw new Error(`anthropic responded ${response.status}${suffix}`);
|
|
74
74
|
}
|
|
75
|
-
|
|
76
|
-
if (typeof parsed !== 'object' || parsed === null) {
|
|
77
|
-
throw new Error(`anthropic returned ${parsed === null ? 'null' : typeof parsed}, not an object`);
|
|
78
|
-
}
|
|
79
|
-
const body = parsed;
|
|
80
|
-
if (body.stop_reason === 'max_tokens') {
|
|
81
|
-
throw new Error(`anthropic stopped at the max_tokens budget (${maxTokens}) before returning a tool use`);
|
|
82
|
-
}
|
|
83
|
-
if (body.stop_reason === 'refusal') {
|
|
84
|
-
throw new Error('anthropic refused to answer (stop_reason: refusal)');
|
|
85
|
-
}
|
|
86
|
-
const blocks = Array.isArray(body.content) ? body.content : [];
|
|
87
|
-
const block = blocks.find(isToolUseBlock);
|
|
88
|
-
if (!block) {
|
|
89
|
-
throw new Error(`anthropic returned no ${TOOL_NAME} tool use`);
|
|
90
|
-
}
|
|
91
|
-
const asSent = request.schema.safeParse(block.input);
|
|
92
|
-
const usable = asSent.success ? asSent : request.schema.safeParse(onlyValue(block.input));
|
|
93
|
-
if (!usable.success) {
|
|
94
|
-
throw new Error(`anthropic returned a ${TOOL_NAME} tool use that does not fit the schema. ` +
|
|
95
|
-
`It sent ${describeShape(block.input)}.`, { cause: asSent.error });
|
|
96
|
-
}
|
|
97
|
-
const spec = usable.data;
|
|
98
|
-
const usage = toUsage(body.usage);
|
|
99
|
-
return { spec, ...(usage ? { usage } : {}) };
|
|
75
|
+
return readSpec(await response.json(), request.schema, maxTokens);
|
|
100
76
|
},
|
|
101
77
|
};
|
|
102
78
|
}
|
|
79
|
+
function readSpec(reply, schema, maxTokens) {
|
|
80
|
+
if (typeof reply !== 'object' || reply === null) {
|
|
81
|
+
throw new Error(`anthropic returned ${reply === null ? 'null' : typeof reply}, not an object`);
|
|
82
|
+
}
|
|
83
|
+
const { stop_reason: stopReason, content, usage } = reply;
|
|
84
|
+
if (stopReason === 'max_tokens') {
|
|
85
|
+
throw new Error(`anthropic stopped at the max_tokens budget (${maxTokens}) before returning a tool use`);
|
|
86
|
+
}
|
|
87
|
+
if (stopReason === 'refusal') {
|
|
88
|
+
throw new Error('anthropic refused to answer (stop_reason: refusal)');
|
|
89
|
+
}
|
|
90
|
+
const blocks = Array.isArray(content) ? content : [];
|
|
91
|
+
const block = blocks.find(isSpecToolUse);
|
|
92
|
+
if (!block) {
|
|
93
|
+
throw new Error(`anthropic returned no ${TOOL_NAME} tool use`);
|
|
94
|
+
}
|
|
95
|
+
const asSent = schema.safeParse(block.input);
|
|
96
|
+
const result = asSent.success ? asSent : schema.safeParse(unwrapSingleKey(block.input));
|
|
97
|
+
if (!result.success) {
|
|
98
|
+
throw new Error(`anthropic returned a ${TOOL_NAME} tool use that does not fit the schema. ` +
|
|
99
|
+
`It sent ${describeShape(block.input)}.`, { cause: asSent.error });
|
|
100
|
+
}
|
|
101
|
+
const tokenUsage = toUsage(usage);
|
|
102
|
+
return tokenUsage ? { spec: result.data, usage: tokenUsage } : { spec: result.data };
|
|
103
|
+
}
|
|
103
104
|
async function send(call, url, init) {
|
|
104
105
|
try {
|
|
105
106
|
return await call(url, init);
|
|
@@ -107,14 +108,18 @@ async function send(call, url, init) {
|
|
|
107
108
|
catch (error) {
|
|
108
109
|
if (init.signal?.aborted)
|
|
109
110
|
throw error;
|
|
110
|
-
|
|
111
|
-
const detail = cause && typeof cause === 'object' && 'code' in cause
|
|
112
|
-
? String(cause.code)
|
|
113
|
-
: String(cause instanceof Error ? cause.message : cause);
|
|
114
|
-
throw new Error(`anthropic did not answer: ${detail}`, { cause: error });
|
|
111
|
+
throw new Error(`anthropic did not answer: ${transportFault(error)}`, { cause: error });
|
|
115
112
|
}
|
|
116
113
|
}
|
|
117
|
-
|
|
114
|
+
function transportFault(error) {
|
|
115
|
+
const cause = error instanceof Error ? (error.cause ?? error) : error;
|
|
116
|
+
if (typeof cause === 'object' && cause !== null && 'code' in cause)
|
|
117
|
+
return String(cause.code);
|
|
118
|
+
if (cause instanceof Error)
|
|
119
|
+
return String(cause.message);
|
|
120
|
+
return String(cause);
|
|
121
|
+
}
|
|
122
|
+
async function errorTypeSuffix(response) {
|
|
118
123
|
try {
|
|
119
124
|
const body = JSON.parse(await response.text());
|
|
120
125
|
const type = body?.error?.type;
|
|
@@ -124,23 +129,21 @@ async function errorCategory(response) {
|
|
|
124
129
|
return '';
|
|
125
130
|
}
|
|
126
131
|
}
|
|
132
|
+
const USAGE_FIELDS = [
|
|
133
|
+
['input_tokens', 'inputTokens'],
|
|
134
|
+
['output_tokens', 'outputTokens'],
|
|
135
|
+
['cache_read_input_tokens', 'cacheReadTokens'],
|
|
136
|
+
['cache_creation_input_tokens', 'cacheWriteTokens'],
|
|
137
|
+
];
|
|
127
138
|
function toUsage(usage) {
|
|
128
139
|
if (!usage)
|
|
129
140
|
return undefined;
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const cacheReadTokens = numberAt('cache_read_input_tokens');
|
|
137
|
-
const cacheWriteTokens = numberAt('cache_creation_input_tokens');
|
|
138
|
-
const mapped = {
|
|
139
|
-
...(inputTokens === undefined ? {} : { inputTokens }),
|
|
140
|
-
...(outputTokens === undefined ? {} : { outputTokens }),
|
|
141
|
-
...(cacheReadTokens === undefined ? {} : { cacheReadTokens }),
|
|
142
|
-
...(cacheWriteTokens === undefined ? {} : { cacheWriteTokens }),
|
|
143
|
-
};
|
|
141
|
+
const mapped = {};
|
|
142
|
+
for (const [from, to] of USAGE_FIELDS) {
|
|
143
|
+
const value = usage[from];
|
|
144
|
+
if (typeof value === 'number')
|
|
145
|
+
mapped[to] = value;
|
|
146
|
+
}
|
|
144
147
|
return Object.keys(mapped).length > 0 ? mapped : undefined;
|
|
145
148
|
}
|
|
146
149
|
//# sourceMappingURL=anthropic-provider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anthropic-provider.js","sourceRoot":"","sources":["../src/anthropic-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"anthropic-provider.js","sourceRoot":"","sources":["../src/anthropic-provider.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rudra-js/anthropic",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.7.6",
|
|
4
|
+
"description": "Anthropic adapter for rudra-js (@rudra-js/core), with no vendor SDK",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"rudra-js",
|
|
7
|
+
"product-recommendations",
|
|
6
8
|
"anthropic",
|
|
7
9
|
"claude",
|
|
8
10
|
"llm",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
+
"ecommerce",
|
|
12
|
+
"typescript"
|
|
11
13
|
],
|
|
12
14
|
"license": "MIT",
|
|
13
15
|
"author": "Clive Dsouza",
|
|
@@ -50,10 +52,10 @@
|
|
|
50
52
|
"zod": "^4.0.0"
|
|
51
53
|
},
|
|
52
54
|
"peerDependencies": {
|
|
53
|
-
"@rudra-js/core": "^0.7.
|
|
55
|
+
"@rudra-js/core": "^0.7.6"
|
|
54
56
|
},
|
|
55
57
|
"devDependencies": {
|
|
56
|
-
"@rudra-js/core": "0.7.
|
|
58
|
+
"@rudra-js/core": "0.7.6",
|
|
57
59
|
"@types/node": "^26.4.1"
|
|
58
60
|
}
|
|
59
61
|
}
|
|
@@ -8,50 +8,47 @@ import type {
|
|
|
8
8
|
|
|
9
9
|
export interface AnthropicProviderOptions {
|
|
10
10
|
apiKey: string;
|
|
11
|
-
|
|
12
11
|
model?: string;
|
|
13
12
|
maxTokens?: number;
|
|
14
13
|
baseUrl?: string;
|
|
15
|
-
|
|
16
14
|
workspaceId?: string;
|
|
17
|
-
|
|
18
15
|
thinking?: { type: 'adaptive' | 'disabled' } | null;
|
|
19
|
-
|
|
20
16
|
fetch?: typeof globalThis.fetch;
|
|
21
17
|
}
|
|
22
18
|
|
|
23
19
|
const TOOL_NAME = 'emit_component_spec';
|
|
24
20
|
|
|
25
|
-
const DEFAULT_MAX_TOKENS = 8192;
|
|
26
|
-
|
|
27
21
|
interface ToolUseBlock {
|
|
28
22
|
type: 'tool_use';
|
|
29
23
|
name: string;
|
|
30
24
|
input: unknown;
|
|
31
25
|
}
|
|
32
26
|
|
|
27
|
+
interface MessagesReply {
|
|
28
|
+
stop_reason?: string;
|
|
29
|
+
content?: unknown;
|
|
30
|
+
usage?: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
33
|
function describeShape(input: unknown): string {
|
|
34
34
|
if (input === null) return 'null';
|
|
35
35
|
if (Array.isArray(input)) return `an array of ${input.length}`;
|
|
36
|
-
if (typeof input !== 'object') return
|
|
36
|
+
if (typeof input !== 'object') return typeof input;
|
|
37
37
|
|
|
38
38
|
const keys = Object.keys(input);
|
|
39
39
|
return keys.length === 0 ? 'an empty object' : `an object with keys ${keys.join(', ')}`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function
|
|
42
|
+
function unwrapSingleKey(input: unknown): unknown {
|
|
43
43
|
if (typeof input !== 'object' || input === null || Array.isArray(input)) return undefined;
|
|
44
44
|
const values = Object.values(input);
|
|
45
45
|
return values.length === 1 ? values[0] : undefined;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function
|
|
49
|
-
return
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
(candidate as Record<string, unknown>)['type'] === 'tool_use' &&
|
|
53
|
-
(candidate as Record<string, unknown>)['name'] === TOOL_NAME
|
|
54
|
-
);
|
|
48
|
+
function isSpecToolUse(block: unknown): block is ToolUseBlock {
|
|
49
|
+
if (typeof block !== 'object' || block === null) return false;
|
|
50
|
+
const fields = block as Record<string, unknown>;
|
|
51
|
+
return fields['type'] === 'tool_use' && fields['name'] === TOOL_NAME;
|
|
55
52
|
}
|
|
56
53
|
|
|
57
54
|
export function createAnthropicProvider(options: AnthropicProviderOptions): ComponentProvider {
|
|
@@ -59,10 +56,9 @@ export function createAnthropicProvider(options: AnthropicProviderOptions): Comp
|
|
|
59
56
|
const thinking =
|
|
60
57
|
options.thinking === undefined ? { type: 'disabled' as const } : options.thinking;
|
|
61
58
|
const call = options.fetch ?? globalThis.fetch;
|
|
62
|
-
|
|
63
59
|
let baseUrl = options.baseUrl ?? 'https://api.anthropic.com';
|
|
64
60
|
while (baseUrl.endsWith('/')) baseUrl = baseUrl.slice(0, -1);
|
|
65
|
-
const maxTokens = options.maxTokens ??
|
|
61
|
+
const maxTokens = options.maxTokens ?? 8192;
|
|
66
62
|
|
|
67
63
|
return {
|
|
68
64
|
name: 'anthropic',
|
|
@@ -71,95 +67,88 @@ export function createAnthropicProvider(options: AnthropicProviderOptions): Comp
|
|
|
71
67
|
async generate(request: ProviderRequest): Promise<ProviderResult> {
|
|
72
68
|
request.signal.throwIfAborted();
|
|
73
69
|
|
|
70
|
+
const headers = {
|
|
71
|
+
'content-type': 'application/json',
|
|
72
|
+
'x-api-key': options.apiKey,
|
|
73
|
+
'anthropic-version': '2023-06-01',
|
|
74
|
+
...(options.workspaceId ? { 'anthropic-workspace-id': options.workspaceId } : {}),
|
|
75
|
+
};
|
|
76
|
+
const body = {
|
|
77
|
+
model,
|
|
78
|
+
max_tokens: maxTokens,
|
|
79
|
+
...(thinking ? { thinking } : {}),
|
|
80
|
+
system: [
|
|
81
|
+
{ type: 'text', text: request.system, cache_control: { type: 'ephemeral' } },
|
|
82
|
+
{
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: `Return that JSON as the input to the ${TOOL_NAME} tool, not as text. You may say a brief sentence first.`,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
messages: [{ role: 'user', content: request.user }],
|
|
88
|
+
tools: [
|
|
89
|
+
{
|
|
90
|
+
name: TOOL_NAME,
|
|
91
|
+
description: 'Return the component specification.',
|
|
92
|
+
input_schema: z.toJSONSchema(request.schema, { io: 'input' }),
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
tool_choice: { type: 'auto', disable_parallel_tool_use: true },
|
|
96
|
+
};
|
|
97
|
+
|
|
74
98
|
const response = await send(call, `${baseUrl}/v1/messages`, {
|
|
75
99
|
method: 'POST',
|
|
76
|
-
headers
|
|
77
|
-
'content-type': 'application/json',
|
|
78
|
-
'x-api-key': options.apiKey,
|
|
79
|
-
'anthropic-version': '2023-06-01',
|
|
80
|
-
...(options.workspaceId ? { 'anthropic-workspace-id': options.workspaceId } : {}),
|
|
81
|
-
},
|
|
82
|
-
|
|
100
|
+
headers,
|
|
83
101
|
signal: request.signal,
|
|
84
|
-
body: JSON.stringify(
|
|
85
|
-
model,
|
|
86
|
-
max_tokens: maxTokens,
|
|
87
|
-
...(thinking ? { thinking } : {}),
|
|
88
|
-
|
|
89
|
-
system: [
|
|
90
|
-
{ type: 'text', text: request.system, cache_control: { type: 'ephemeral' } },
|
|
91
|
-
{
|
|
92
|
-
type: 'text',
|
|
93
|
-
text: `Return that JSON as the input to the ${TOOL_NAME} tool, not as text. You may say a brief sentence first.`,
|
|
94
|
-
},
|
|
95
|
-
],
|
|
96
|
-
messages: [{ role: 'user', content: request.user }],
|
|
97
|
-
tools: [
|
|
98
|
-
{
|
|
99
|
-
name: TOOL_NAME,
|
|
100
|
-
description: 'Return the component specification.',
|
|
101
|
-
|
|
102
|
-
input_schema: z.toJSONSchema(request.schema, { io: 'input' }),
|
|
103
|
-
},
|
|
104
|
-
],
|
|
105
|
-
// Opus 5.5, Sonnet 5.5, Fable 5.1 and Mythos 5.1 reject a forced tool call with a 400.
|
|
106
|
-
tool_choice: { type: 'auto', disable_parallel_tool_use: true },
|
|
107
|
-
}),
|
|
102
|
+
body: JSON.stringify(body),
|
|
108
103
|
});
|
|
109
104
|
|
|
110
105
|
if (!response.ok) {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
throw new Error(`anthropic responded ${response.status}${category}`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const parsed: unknown = await response.json();
|
|
117
|
-
|
|
118
|
-
if (typeof parsed !== 'object' || parsed === null) {
|
|
119
|
-
throw new Error(
|
|
120
|
-
`anthropic returned ${parsed === null ? 'null' : typeof parsed}, not an object`,
|
|
121
|
-
);
|
|
106
|
+
const suffix = await errorTypeSuffix(response);
|
|
107
|
+
throw new Error(`anthropic responded ${response.status}${suffix}`);
|
|
122
108
|
}
|
|
123
109
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
if (body.stop_reason === 'max_tokens') {
|
|
131
|
-
throw new Error(
|
|
132
|
-
`anthropic stopped at the max_tokens budget (${maxTokens}) before returning a tool use`,
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
if (body.stop_reason === 'refusal') {
|
|
136
|
-
throw new Error('anthropic refused to answer (stop_reason: refusal)');
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const blocks = Array.isArray(body.content) ? body.content : [];
|
|
140
|
-
const block = blocks.find(isToolUseBlock);
|
|
110
|
+
return readSpec(await response.json(), request.schema, maxTokens);
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
141
114
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
115
|
+
function readSpec(
|
|
116
|
+
reply: unknown,
|
|
117
|
+
schema: ProviderRequest['schema'],
|
|
118
|
+
maxTokens: number,
|
|
119
|
+
): ProviderResult {
|
|
120
|
+
if (typeof reply !== 'object' || reply === null) {
|
|
121
|
+
throw new Error(`anthropic returned ${reply === null ? 'null' : typeof reply}, not an object`);
|
|
122
|
+
}
|
|
145
123
|
|
|
146
|
-
|
|
147
|
-
|
|
124
|
+
const { stop_reason: stopReason, content, usage } = reply as MessagesReply;
|
|
125
|
+
if (stopReason === 'max_tokens') {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`anthropic stopped at the max_tokens budget (${maxTokens}) before returning a tool use`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (stopReason === 'refusal') {
|
|
131
|
+
throw new Error('anthropic refused to answer (stop_reason: refusal)');
|
|
132
|
+
}
|
|
148
133
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
);
|
|
155
|
-
}
|
|
134
|
+
const blocks = Array.isArray(content) ? content : [];
|
|
135
|
+
const block = blocks.find(isSpecToolUse);
|
|
136
|
+
if (!block) {
|
|
137
|
+
throw new Error(`anthropic returned no ${TOOL_NAME} tool use`);
|
|
138
|
+
}
|
|
156
139
|
|
|
157
|
-
|
|
158
|
-
|
|
140
|
+
const asSent = schema.safeParse(block.input);
|
|
141
|
+
const result = asSent.success ? asSent : schema.safeParse(unwrapSingleKey(block.input));
|
|
142
|
+
if (!result.success) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`anthropic returned a ${TOOL_NAME} tool use that does not fit the schema. ` +
|
|
145
|
+
`It sent ${describeShape(block.input)}.`,
|
|
146
|
+
{ cause: asSent.error },
|
|
147
|
+
);
|
|
148
|
+
}
|
|
159
149
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
};
|
|
150
|
+
const tokenUsage = toUsage(usage);
|
|
151
|
+
return tokenUsage ? { spec: result.data, usage: tokenUsage } : { spec: result.data };
|
|
163
152
|
}
|
|
164
153
|
|
|
165
154
|
async function send(call: typeof globalThis.fetch, url: string, init: RequestInit) {
|
|
@@ -167,18 +156,18 @@ async function send(call: typeof globalThis.fetch, url: string, init: RequestIni
|
|
|
167
156
|
return await call(url, init);
|
|
168
157
|
} catch (error) {
|
|
169
158
|
if (init.signal?.aborted) throw error;
|
|
170
|
-
|
|
171
|
-
const cause = error instanceof Error ? (error.cause ?? error) : error;
|
|
172
|
-
const detail =
|
|
173
|
-
cause && typeof cause === 'object' && 'code' in cause
|
|
174
|
-
? String((cause as { code: unknown }).code)
|
|
175
|
-
: String(cause instanceof Error ? cause.message : cause);
|
|
176
|
-
|
|
177
|
-
throw new Error(`anthropic did not answer: ${detail}`, { cause: error });
|
|
159
|
+
throw new Error(`anthropic did not answer: ${transportFault(error)}`, { cause: error });
|
|
178
160
|
}
|
|
179
161
|
}
|
|
180
162
|
|
|
181
|
-
|
|
163
|
+
function transportFault(error: unknown): string {
|
|
164
|
+
const cause = error instanceof Error ? (error.cause ?? error) : error;
|
|
165
|
+
if (typeof cause === 'object' && cause !== null && 'code' in cause) return String(cause.code);
|
|
166
|
+
if (cause instanceof Error) return String(cause.message);
|
|
167
|
+
return String(cause);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function errorTypeSuffix(response: Response): Promise<string> {
|
|
182
171
|
try {
|
|
183
172
|
const body: unknown = JSON.parse(await response.text());
|
|
184
173
|
const type = (body as { error?: { type?: unknown } })?.error?.type;
|
|
@@ -188,25 +177,20 @@ async function errorCategory(response: Response): Promise<string> {
|
|
|
188
177
|
}
|
|
189
178
|
}
|
|
190
179
|
|
|
180
|
+
const USAGE_FIELDS = [
|
|
181
|
+
['input_tokens', 'inputTokens'],
|
|
182
|
+
['output_tokens', 'outputTokens'],
|
|
183
|
+
['cache_read_input_tokens', 'cacheReadTokens'],
|
|
184
|
+
['cache_creation_input_tokens', 'cacheWriteTokens'],
|
|
185
|
+
] as const;
|
|
186
|
+
|
|
191
187
|
function toUsage(usage: Record<string, unknown> | undefined): TokenUsage | undefined {
|
|
192
188
|
if (!usage) return undefined;
|
|
193
189
|
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const inputTokens = numberAt('input_tokens');
|
|
200
|
-
const outputTokens = numberAt('output_tokens');
|
|
201
|
-
const cacheReadTokens = numberAt('cache_read_input_tokens');
|
|
202
|
-
const cacheWriteTokens = numberAt('cache_creation_input_tokens');
|
|
203
|
-
|
|
204
|
-
const mapped = {
|
|
205
|
-
...(inputTokens === undefined ? {} : { inputTokens }),
|
|
206
|
-
...(outputTokens === undefined ? {} : { outputTokens }),
|
|
207
|
-
...(cacheReadTokens === undefined ? {} : { cacheReadTokens }),
|
|
208
|
-
...(cacheWriteTokens === undefined ? {} : { cacheWriteTokens }),
|
|
209
|
-
};
|
|
210
|
-
|
|
190
|
+
const mapped: TokenUsage = {};
|
|
191
|
+
for (const [from, to] of USAGE_FIELDS) {
|
|
192
|
+
const value = usage[from];
|
|
193
|
+
if (typeof value === 'number') mapped[to] = value;
|
|
194
|
+
}
|
|
211
195
|
return Object.keys(mapped).length > 0 ? mapped : undefined;
|
|
212
196
|
}
|