@rudra-js/anthropic 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Clive Dsouza
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @rudra-js/anthropic
2
+
3
+ An Anthropic adapter for
4
+ [`@rudra-js/core`](https://github.com/clivedsouza1010/rudra-js/tree/main/packages/core)'s
5
+ `ComponentProvider`.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install @rudra-js/anthropic @rudra-js/core zod
11
+ ```
12
+
13
+ ```ts
14
+ import { createAnthropicProvider } from '@rudra-js/anthropic';
15
+ import { createComponentGenerator } from '@rudra-js/core';
16
+
17
+ const provider = createAnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! });
18
+ const generator = createComponentGenerator({ provider });
19
+ ```
20
+
21
+ `model` defaults to the current Claude model this package was written against;
22
+ pass it to pin a different one. `maxTokens` and `baseUrl` are also optional.
23
+
24
+ ### An identity-linked key needs a workspace
25
+
26
+ A key created against your identity rather than inside a workspace belongs to
27
+ you across several of them, so the API cannot infer which one a request acts in
28
+ and answers:
29
+
30
+ ```
31
+ 400 invalid_request_error — anthropic-workspace-id is required when
32
+ authenticating with an identity-linked API key
33
+ ```
34
+
35
+ Pass the workspace, or create the key from inside a workspace instead:
36
+
37
+ ```ts
38
+ createAnthropicProvider({
39
+ apiKey: process.env.ANTHROPIC_API_KEY!,
40
+ workspaceId: process.env.ANTHROPIC_WORKSPACE_ID!,
41
+ });
42
+ ```
43
+
44
+ `fetch` is injectable, which is what the test suite uses in place of a network
45
+ call.
46
+
47
+ The tool schema sent to the model is derived from the `schema` on the
48
+ `ProviderRequest` — the same schema `@rudra-js/core` defines — rather than a
49
+ copy written out here. A second copy would be a second vocabulary: the
50
+ reconciler would enforce one thing and the model would be told another.
51
+
52
+ ## Licence
53
+
54
+ [MIT](./LICENSE)
@@ -0,0 +1,25 @@
1
+ import type { ComponentProvider } from '@rudra-js/core';
2
+ export interface AnthropicProviderOptions {
3
+ apiKey: string;
4
+ /** Defaults to the current Claude model this package was written against. */
5
+ model?: string;
6
+ maxTokens?: number;
7
+ baseUrl?: string;
8
+ /**
9
+ * Required when the key is identity-linked rather than workspace-scoped —
10
+ * such a key belongs to a person across several workspaces, so the API cannot
11
+ * infer which one a request acts in and rejects it with a 400.
12
+ */
13
+ workspaceId?: string;
14
+ /** Injected so the adapter is testable without a network or an SDK. */
15
+ fetch?: typeof globalThis.fetch;
16
+ }
17
+ /**
18
+ * Adapts the Anthropic Messages API to `ComponentProvider`.
19
+ *
20
+ * The tool schema is derived from the schema core exports rather than restated
21
+ * here: a second copy is a second vocabulary, and the drift shows up as
22
+ * unexplained `invalid-generation` events.
23
+ */
24
+ export declare function createAnthropicProvider(options: AnthropicProviderOptions): ComponentProvider;
25
+ //# sourceMappingURL=anthropic-provider.d.ts.map
@@ -0,0 +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;IACf,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AA0CD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,wBAAwB,GAAG,iBAAiB,CA6H5F"}
@@ -0,0 +1,198 @@
1
+ import { z } from 'zod';
2
+ const TOOL_NAME = 'emit_component_spec';
3
+ /**
4
+ * This model runs adaptive thinking by default, and thinking draws on the
5
+ * same output budget as the tool call. A cap too close to what reasoning
6
+ * alone can spend leaves no room for the tool block, so the default is well
7
+ * above a typical spec's size rather than tuned to it.
8
+ */
9
+ const DEFAULT_MAX_TOKENS = 8192;
10
+ function describeShape(input) {
11
+ if (input === null)
12
+ return 'null';
13
+ if (Array.isArray(input))
14
+ return `an array of ${input.length}`;
15
+ if (typeof input !== 'object')
16
+ return String(typeof input);
17
+ const keys = Object.keys(input);
18
+ return keys.length === 0 ? 'an empty object' : `an object with keys ${keys.join(', ')}`;
19
+ }
20
+ function onlyValue(input) {
21
+ if (typeof input !== 'object' || input === null || Array.isArray(input))
22
+ return undefined;
23
+ const values = Object.values(input);
24
+ return values.length === 1 ? values[0] : undefined;
25
+ }
26
+ function isToolUseBlock(candidate) {
27
+ return (typeof candidate === 'object' &&
28
+ candidate !== null &&
29
+ candidate['type'] === 'tool_use' &&
30
+ candidate['name'] === TOOL_NAME);
31
+ }
32
+ /**
33
+ * Adapts the Anthropic Messages API to `ComponentProvider`.
34
+ *
35
+ * The tool schema is derived from the schema core exports rather than restated
36
+ * here: a second copy is a second vocabulary, and the drift shows up as
37
+ * unexplained `invalid-generation` events.
38
+ */
39
+ export function createAnthropicProvider(options) {
40
+ const model = options.model ?? 'claude-opus-5';
41
+ const call = options.fetch ?? globalThis.fetch;
42
+ // Trimmed so a caller-supplied `baseUrl` ending in `/` cannot turn into
43
+ // `//v1/messages`. Done with a loop rather than `/\/+$/`: that pattern
44
+ // backtracks on a string of many trailing slashes, which is a denial of
45
+ // service in a published package even though the value comes from the caller
46
+ // rather than from a request.
47
+ let baseUrl = options.baseUrl ?? 'https://api.anthropic.com';
48
+ while (baseUrl.endsWith('/'))
49
+ baseUrl = baseUrl.slice(0, -1);
50
+ const maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;
51
+ return {
52
+ name: 'anthropic',
53
+ model,
54
+ async generate(request) {
55
+ // The half of obligation three that `fetch` does not cover. The real
56
+ // `fetch` rejects an already-aborted signal on its own, but `fetch` is an
57
+ // injected seam here, and a caller's own transport has no such duty — so
58
+ // without this, a call the caller has already given up on goes out.
59
+ request.signal.throwIfAborted();
60
+ const response = await send(call, `${baseUrl}/v1/messages`, {
61
+ method: 'POST',
62
+ headers: {
63
+ 'content-type': 'application/json',
64
+ 'x-api-key': options.apiKey,
65
+ 'anthropic-version': '2023-06-01',
66
+ ...(options.workspaceId ? { 'anthropic-workspace-id': options.workspaceId } : {}),
67
+ },
68
+ // The caller's deadline, handed straight to the transport: the contract
69
+ // asks an adapter to stop, not merely to stop caring about the answer.
70
+ signal: request.signal,
71
+ body: JSON.stringify({
72
+ model,
73
+ max_tokens: maxTokens,
74
+ // Marked as the cached prefix. Anything per-shopper interpolated here
75
+ // would destroy the prompt cache hit rate.
76
+ system: [{ type: 'text', text: request.system, cache_control: { type: 'ephemeral' } }],
77
+ messages: [{ role: 'user', content: request.user }],
78
+ tools: [
79
+ {
80
+ name: TOOL_NAME,
81
+ description: 'Return the component specification.',
82
+ // "input" — a tool's input_schema describes what the model must
83
+ // produce as the tool call's argument, not core's own output. The
84
+ // two are not the same: "output" carries additionalProperties:
85
+ // false on every block and "input" does not. Pinned in
86
+ // tests/tool-schema.test.ts.
87
+ input_schema: z.toJSONSchema(request.schema, { io: 'input' }),
88
+ },
89
+ ],
90
+ tool_choice: { type: 'tool', name: TOOL_NAME },
91
+ }),
92
+ });
93
+ if (!response.ok) {
94
+ // Status and the vendor's error category only. Its message quotes the
95
+ // request back, and for this framework that can be a shopper's own search
96
+ // terms — which an adopter's `console.error(err)` would then capture.
97
+ const category = await errorCategory(response);
98
+ throw new Error(`anthropic responded ${response.status}${category}`);
99
+ }
100
+ const parsed = await response.json();
101
+ // `response.json()` yields whatever the body held, and `null` is valid
102
+ // JSON — reading `stop_reason` off it would throw a TypeError naming this
103
+ // adapter rather than the vendor that sent it.
104
+ if (typeof parsed !== 'object' || parsed === null) {
105
+ throw new Error(`anthropic returned ${parsed === null ? 'null' : typeof parsed}, not an object`);
106
+ }
107
+ const body = parsed;
108
+ // Before the tool-block lookup: both are ordinary 200s with no tool_use,
109
+ // and reporting them as "no tool use" blames the model for a budget or a
110
+ // policy this adapter controls.
111
+ if (body.stop_reason === 'max_tokens') {
112
+ throw new Error(`anthropic stopped at the max_tokens budget (${maxTokens}) before returning a tool use`);
113
+ }
114
+ if (body.stop_reason === 'refusal') {
115
+ throw new Error('anthropic refused to answer (stop_reason: refusal)');
116
+ }
117
+ // `content` and each of its entries are untrusted shapes from here on:
118
+ // a malformed response should name the vendor, not crash on the
119
+ // adapter's own `.find`/`.type` access.
120
+ const blocks = Array.isArray(body.content) ? body.content : [];
121
+ const block = blocks.find(isToolUseBlock);
122
+ if (!block) {
123
+ throw new Error(`anthropic returned no ${TOOL_NAME} tool use`);
124
+ }
125
+ // Parsed against the caller's own schema. generatedSpecSchema has no
126
+ // refinements, so this catches type and enum violations — a block kind
127
+ // outside the closed set, a non-string headline — not refinement logic.
128
+ const asSent = request.schema.safeParse(block.input);
129
+ const usable = asSent.success ? asSent : request.schema.safeParse(onlyValue(block.input));
130
+ if (!usable.success) {
131
+ throw new Error(`anthropic returned a ${TOOL_NAME} tool use that does not fit the schema. ` +
132
+ `It sent ${describeShape(block.input)}.`, { cause: asSent.error });
133
+ }
134
+ const spec = usable.data;
135
+ const usage = toUsage(body.usage);
136
+ return { spec, ...(usage ? { usage } : {}) };
137
+ },
138
+ };
139
+ }
140
+ /**
141
+ * Calls the transport, and says what went wrong when it never answered.
142
+ *
143
+ * `fetch` reports every transport fault as the same `TypeError: fetch failed`
144
+ * and hides the reason in `cause` — so a refused connection, a DNS failure and
145
+ * a socket reset are indistinguishable in a log. An operator needs to tell
146
+ * those apart, and none of them carries request content.
147
+ */
148
+ async function send(call, url, init) {
149
+ try {
150
+ return await call(url, init);
151
+ }
152
+ catch (error) {
153
+ // The caller's own deadline. It means something specific upstream, so it
154
+ // travels unchanged.
155
+ if (init.signal?.aborted)
156
+ throw error;
157
+ const cause = error instanceof Error ? (error.cause ?? error) : error;
158
+ const detail = cause && typeof cause === 'object' && 'code' in cause
159
+ ? String(cause.code)
160
+ : String(cause instanceof Error ? cause.message : cause);
161
+ throw new Error(`anthropic did not answer: ${detail}`, { cause: error });
162
+ }
163
+ }
164
+ /** The vendor's error category, never its message — the message quotes the request. */
165
+ async function errorCategory(response) {
166
+ try {
167
+ const body = JSON.parse(await response.text());
168
+ const type = body?.error?.type;
169
+ return typeof type === 'string' ? ` (${type})` : '';
170
+ }
171
+ catch {
172
+ // A body that is unreadable or not JSON tells us nothing extra. The status
173
+ // is still in the message.
174
+ return '';
175
+ }
176
+ }
177
+ function toUsage(usage) {
178
+ if (!usage)
179
+ return undefined;
180
+ // A JSON body is untrusted: `"input_tokens": "11"` must not become part of
181
+ // a cost figure that downstream code adds instead of concatenates.
182
+ const numberAt = (key) => {
183
+ const value = usage[key];
184
+ return typeof value === 'number' ? value : undefined;
185
+ };
186
+ const inputTokens = numberAt('input_tokens');
187
+ const outputTokens = numberAt('output_tokens');
188
+ const cacheReadTokens = numberAt('cache_read_input_tokens');
189
+ const cacheWriteTokens = numberAt('cache_creation_input_tokens');
190
+ const mapped = {
191
+ ...(inputTokens === undefined ? {} : { inputTokens }),
192
+ ...(outputTokens === undefined ? {} : { outputTokens }),
193
+ ...(cacheReadTokens === undefined ? {} : { cacheReadTokens }),
194
+ ...(cacheWriteTokens === undefined ? {} : { cacheWriteTokens }),
195
+ };
196
+ return Object.keys(mapped).length > 0 ? mapped : undefined;
197
+ }
198
+ //# sourceMappingURL=anthropic-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic-provider.js","sourceRoot":"","sources":["../src/anthropic-provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ /** @rudra-js/anthropic — an Anthropic adapter for `@rudra-js/core`. */
2
+ export { createAnthropicProvider, type AnthropicProviderOptions } from './anthropic-provider.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,OAAO,EAAE,uBAAuB,EAAE,KAAK,wBAAwB,EAAE,MAAM,yBAAyB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ /** @rudra-js/anthropic — an Anthropic adapter for `@rudra-js/core`. */
2
+ export { createAnthropicProvider } from './anthropic-provider.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,OAAO,EAAE,uBAAuB,EAAiC,MAAM,yBAAyB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@rudra-js/anthropic",
3
+ "version": "0.1.0",
4
+ "description": "An Anthropic adapter for @rudra-js/core, with no vendor SDK and no dependencies",
5
+ "keywords": [
6
+ "anthropic",
7
+ "claude",
8
+ "llm",
9
+ "rudra",
10
+ "server-components"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Clive Dsouza",
14
+ "homepage": "https://github.com/clivedsouza1010/rudra-js/tree/main/packages/anthropic#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/clivedsouza1010/rudra-js/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/clivedsouza1010/rudra-js.git",
21
+ "directory": "packages/anthropic"
22
+ },
23
+ "engines": {
24
+ "node": "^20.19.0 || >=22.12.0"
25
+ },
26
+ "type": "module",
27
+ "main": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "default": "./dist/index.js"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "src/**/*.ts",
39
+ "!src/**/*.test.ts"
40
+ ],
41
+ "sideEffects": false,
42
+ "scripts": {
43
+ "build": "tsc -p tsconfig.json",
44
+ "prepack": "npm run build"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "peerDependencies": {
50
+ "@rudra-js/core": "^0.1.0",
51
+ "zod": "^4.5.4"
52
+ },
53
+ "devDependencies": {
54
+ "@rudra-js/core": "0.1.0",
55
+ "zod": "^4.5.4"
56
+ }
57
+ }
@@ -0,0 +1,261 @@
1
+ import { z } from 'zod';
2
+ import type {
3
+ ComponentProvider,
4
+ ProviderRequest,
5
+ ProviderResult,
6
+ TokenUsage,
7
+ } from '@rudra-js/core';
8
+
9
+ export interface AnthropicProviderOptions {
10
+ apiKey: string;
11
+ /** Defaults to the current Claude model this package was written against. */
12
+ model?: string;
13
+ maxTokens?: number;
14
+ baseUrl?: string;
15
+ /**
16
+ * Required when the key is identity-linked rather than workspace-scoped —
17
+ * such a key belongs to a person across several workspaces, so the API cannot
18
+ * infer which one a request acts in and rejects it with a 400.
19
+ */
20
+ workspaceId?: string;
21
+ /** Injected so the adapter is testable without a network or an SDK. */
22
+ fetch?: typeof globalThis.fetch;
23
+ }
24
+
25
+ const TOOL_NAME = 'emit_component_spec';
26
+
27
+ /**
28
+ * This model runs adaptive thinking by default, and thinking draws on the
29
+ * same output budget as the tool call. A cap too close to what reasoning
30
+ * alone can spend leaves no room for the tool block, so the default is well
31
+ * above a typical spec's size rather than tuned to it.
32
+ */
33
+ const DEFAULT_MAX_TOKENS = 8192;
34
+
35
+ interface ToolUseBlock {
36
+ type: 'tool_use';
37
+ name: string;
38
+ input: unknown;
39
+ }
40
+
41
+ function describeShape(input: unknown): string {
42
+ if (input === null) return 'null';
43
+ if (Array.isArray(input)) return `an array of ${input.length}`;
44
+ if (typeof input !== 'object') return String(typeof input);
45
+
46
+ const keys = Object.keys(input);
47
+ return keys.length === 0 ? 'an empty object' : `an object with keys ${keys.join(', ')}`;
48
+ }
49
+
50
+ function onlyValue(input: unknown): unknown {
51
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) return undefined;
52
+ const values = Object.values(input);
53
+ return values.length === 1 ? values[0] : undefined;
54
+ }
55
+
56
+ function isToolUseBlock(candidate: unknown): candidate is ToolUseBlock {
57
+ return (
58
+ typeof candidate === 'object' &&
59
+ candidate !== null &&
60
+ (candidate as Record<string, unknown>)['type'] === 'tool_use' &&
61
+ (candidate as Record<string, unknown>)['name'] === TOOL_NAME
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Adapts the Anthropic Messages API to `ComponentProvider`.
67
+ *
68
+ * The tool schema is derived from the schema core exports rather than restated
69
+ * here: a second copy is a second vocabulary, and the drift shows up as
70
+ * unexplained `invalid-generation` events.
71
+ */
72
+ export function createAnthropicProvider(options: AnthropicProviderOptions): ComponentProvider {
73
+ const model = options.model ?? 'claude-opus-5';
74
+ const call = options.fetch ?? globalThis.fetch;
75
+ // Trimmed so a caller-supplied `baseUrl` ending in `/` cannot turn into
76
+ // `//v1/messages`. Done with a loop rather than `/\/+$/`: that pattern
77
+ // backtracks on a string of many trailing slashes, which is a denial of
78
+ // service in a published package even though the value comes from the caller
79
+ // rather than from a request.
80
+ let baseUrl = options.baseUrl ?? 'https://api.anthropic.com';
81
+ while (baseUrl.endsWith('/')) baseUrl = baseUrl.slice(0, -1);
82
+ const maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;
83
+
84
+ return {
85
+ name: 'anthropic',
86
+ model,
87
+
88
+ async generate(request: ProviderRequest): Promise<ProviderResult> {
89
+ // The half of obligation three that `fetch` does not cover. The real
90
+ // `fetch` rejects an already-aborted signal on its own, but `fetch` is an
91
+ // injected seam here, and a caller's own transport has no such duty — so
92
+ // without this, a call the caller has already given up on goes out.
93
+ request.signal.throwIfAborted();
94
+
95
+ const response = await send(call, `${baseUrl}/v1/messages`, {
96
+ method: 'POST',
97
+ headers: {
98
+ 'content-type': 'application/json',
99
+ 'x-api-key': options.apiKey,
100
+ 'anthropic-version': '2023-06-01',
101
+ ...(options.workspaceId ? { 'anthropic-workspace-id': options.workspaceId } : {}),
102
+ },
103
+ // The caller's deadline, handed straight to the transport: the contract
104
+ // asks an adapter to stop, not merely to stop caring about the answer.
105
+ signal: request.signal,
106
+ body: JSON.stringify({
107
+ model,
108
+ max_tokens: maxTokens,
109
+ // Marked as the cached prefix. Anything per-shopper interpolated here
110
+ // would destroy the prompt cache hit rate.
111
+ system: [{ type: 'text', text: request.system, cache_control: { type: 'ephemeral' } }],
112
+ messages: [{ role: 'user', content: request.user }],
113
+ tools: [
114
+ {
115
+ name: TOOL_NAME,
116
+ description: 'Return the component specification.',
117
+ // "input" — a tool's input_schema describes what the model must
118
+ // produce as the tool call's argument, not core's own output. The
119
+ // two are not the same: "output" carries additionalProperties:
120
+ // false on every block and "input" does not. Pinned in
121
+ // tests/tool-schema.test.ts.
122
+ input_schema: z.toJSONSchema(request.schema, { io: 'input' }),
123
+ },
124
+ ],
125
+ tool_choice: { type: 'tool', name: TOOL_NAME },
126
+ }),
127
+ });
128
+
129
+ if (!response.ok) {
130
+ // Status and the vendor's error category only. Its message quotes the
131
+ // request back, and for this framework that can be a shopper's own search
132
+ // terms — which an adopter's `console.error(err)` would then capture.
133
+ const category = await errorCategory(response);
134
+
135
+ throw new Error(`anthropic responded ${response.status}${category}`);
136
+ }
137
+
138
+ const parsed: unknown = await response.json();
139
+
140
+ // `response.json()` yields whatever the body held, and `null` is valid
141
+ // JSON — reading `stop_reason` off it would throw a TypeError naming this
142
+ // adapter rather than the vendor that sent it.
143
+ if (typeof parsed !== 'object' || parsed === null) {
144
+ throw new Error(
145
+ `anthropic returned ${parsed === null ? 'null' : typeof parsed}, not an object`,
146
+ );
147
+ }
148
+
149
+ const body = parsed as {
150
+ content?: unknown;
151
+ usage?: Record<string, unknown>;
152
+ stop_reason?: string;
153
+ };
154
+
155
+ // Before the tool-block lookup: both are ordinary 200s with no tool_use,
156
+ // and reporting them as "no tool use" blames the model for a budget or a
157
+ // policy this adapter controls.
158
+ if (body.stop_reason === 'max_tokens') {
159
+ throw new Error(
160
+ `anthropic stopped at the max_tokens budget (${maxTokens}) before returning a tool use`,
161
+ );
162
+ }
163
+ if (body.stop_reason === 'refusal') {
164
+ throw new Error('anthropic refused to answer (stop_reason: refusal)');
165
+ }
166
+
167
+ // `content` and each of its entries are untrusted shapes from here on:
168
+ // a malformed response should name the vendor, not crash on the
169
+ // adapter's own `.find`/`.type` access.
170
+ const blocks = Array.isArray(body.content) ? body.content : [];
171
+ const block = blocks.find(isToolUseBlock);
172
+
173
+ if (!block) {
174
+ throw new Error(`anthropic returned no ${TOOL_NAME} tool use`);
175
+ }
176
+
177
+ // Parsed against the caller's own schema. generatedSpecSchema has no
178
+ // refinements, so this catches type and enum violations — a block kind
179
+ // outside the closed set, a non-string headline — not refinement logic.
180
+ const asSent = request.schema.safeParse(block.input);
181
+ const usable = asSent.success ? asSent : request.schema.safeParse(onlyValue(block.input));
182
+
183
+ if (!usable.success) {
184
+ throw new Error(
185
+ `anthropic returned a ${TOOL_NAME} tool use that does not fit the schema. ` +
186
+ `It sent ${describeShape(block.input)}.`,
187
+ { cause: asSent.error },
188
+ );
189
+ }
190
+
191
+ const spec = usable.data;
192
+ const usage = toUsage(body.usage);
193
+
194
+ return { spec, ...(usage ? { usage } : {}) };
195
+ },
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Calls the transport, and says what went wrong when it never answered.
201
+ *
202
+ * `fetch` reports every transport fault as the same `TypeError: fetch failed`
203
+ * and hides the reason in `cause` — so a refused connection, a DNS failure and
204
+ * a socket reset are indistinguishable in a log. An operator needs to tell
205
+ * those apart, and none of them carries request content.
206
+ */
207
+ async function send(call: typeof globalThis.fetch, url: string, init: RequestInit) {
208
+ try {
209
+ return await call(url, init);
210
+ } catch (error) {
211
+ // The caller's own deadline. It means something specific upstream, so it
212
+ // travels unchanged.
213
+ if (init.signal?.aborted) throw error;
214
+
215
+ const cause = error instanceof Error ? (error.cause ?? error) : error;
216
+ const detail =
217
+ cause && typeof cause === 'object' && 'code' in cause
218
+ ? String((cause as { code: unknown }).code)
219
+ : String(cause instanceof Error ? cause.message : cause);
220
+
221
+ throw new Error(`anthropic did not answer: ${detail}`, { cause: error });
222
+ }
223
+ }
224
+
225
+ /** The vendor's error category, never its message — the message quotes the request. */
226
+ async function errorCategory(response: Response): Promise<string> {
227
+ try {
228
+ const body: unknown = JSON.parse(await response.text());
229
+ const type = (body as { error?: { type?: unknown } })?.error?.type;
230
+ return typeof type === 'string' ? ` (${type})` : '';
231
+ } catch {
232
+ // A body that is unreadable or not JSON tells us nothing extra. The status
233
+ // is still in the message.
234
+ return '';
235
+ }
236
+ }
237
+
238
+ function toUsage(usage: Record<string, unknown> | undefined): TokenUsage | undefined {
239
+ if (!usage) return undefined;
240
+
241
+ // A JSON body is untrusted: `"input_tokens": "11"` must not become part of
242
+ // a cost figure that downstream code adds instead of concatenates.
243
+ const numberAt = (key: string): number | undefined => {
244
+ const value = usage[key];
245
+ return typeof value === 'number' ? value : undefined;
246
+ };
247
+
248
+ const inputTokens = numberAt('input_tokens');
249
+ const outputTokens = numberAt('output_tokens');
250
+ const cacheReadTokens = numberAt('cache_read_input_tokens');
251
+ const cacheWriteTokens = numberAt('cache_creation_input_tokens');
252
+
253
+ const mapped = {
254
+ ...(inputTokens === undefined ? {} : { inputTokens }),
255
+ ...(outputTokens === undefined ? {} : { outputTokens }),
256
+ ...(cacheReadTokens === undefined ? {} : { cacheReadTokens }),
257
+ ...(cacheWriteTokens === undefined ? {} : { cacheWriteTokens }),
258
+ };
259
+
260
+ return Object.keys(mapped).length > 0 ? mapped : undefined;
261
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ /** @rudra-js/anthropic — an Anthropic adapter for `@rudra-js/core`. */
2
+
3
+ export { createAnthropicProvider, type AnthropicProviderOptions } from './anthropic-provider.js';