@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 +21 -0
- package/README.md +54 -0
- package/dist/anthropic-provider.d.ts +25 -0
- package/dist/anthropic-provider.d.ts.map +1 -0
- package/dist/anthropic-provider.js +198 -0
- package/dist/anthropic-provider.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
- package/src/anthropic-provider.ts +261 -0
- package/src/index.ts +3 -0
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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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