@purista/harness-anthropic 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +4 -0
- package/README.md +25 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +263 -0
- package/package.json +56 -0
package/LICENSE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @purista/harness-anthropic
|
|
2
|
+
|
|
3
|
+
Anthropic model provider adapter for `@purista/harness`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @purista/harness @purista/harness-anthropic
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Configure the provider with an Anthropic API key in your application
|
|
12
|
+
environment. The adapter is designed for use through the typed
|
|
13
|
+
`@purista/harness` model provider port.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { anthropic } from '@purista/harness-anthropic'
|
|
17
|
+
|
|
18
|
+
anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Package Format
|
|
22
|
+
|
|
23
|
+
This package is ESM-only and ships compiled JavaScript plus TypeScript
|
|
24
|
+
declarations from `dist/`. Source files, tests, source maps, and local configs
|
|
25
|
+
are not included in the published package.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { BaseModelProviderOptions, ModelProvider } from '@purista/harness';
|
|
2
|
+
import { type ClientOptions } from '@anthropic-ai/sdk';
|
|
3
|
+
export interface AnthropicFactoryOptions extends ClientOptions {
|
|
4
|
+
/** Optional injected client for tests or custom transport behavior. */
|
|
5
|
+
client?: AnthropicClient;
|
|
6
|
+
/** Optional adapter-level logger override. Defaults to the harness logger when registered. */
|
|
7
|
+
harnessLogger?: BaseModelProviderOptions['logger'];
|
|
8
|
+
/** Optional adapter-level telemetry override. Defaults to the harness telemetry shim when registered. */
|
|
9
|
+
telemetry?: BaseModelProviderOptions['telemetry'];
|
|
10
|
+
/** Optional adapter-level timeout override. Defaults to the harness model timeout when registered. */
|
|
11
|
+
harnessTimeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Creates an Anthropic-backed harness `ModelProvider`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { anthropic } from '@purista/harness-anthropic'
|
|
19
|
+
*
|
|
20
|
+
* const provider = anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function anthropic(options?: AnthropicFactoryOptions): ModelProvider;
|
|
24
|
+
export type AnthropicClient = {
|
|
25
|
+
messages: {
|
|
26
|
+
create(payload: unknown, options?: {
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}): Promise<any>;
|
|
29
|
+
};
|
|
30
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { BaseModelProvider, ModelError } from '@purista/harness';
|
|
2
|
+
import Anthropic, {} from '@anthropic-ai/sdk';
|
|
3
|
+
/**
|
|
4
|
+
* Creates an Anthropic-backed harness `ModelProvider`.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { anthropic } from '@purista/harness-anthropic'
|
|
9
|
+
*
|
|
10
|
+
* const provider = anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export function anthropic(options = {}) {
|
|
14
|
+
return new AnthropicModelProvider(options);
|
|
15
|
+
}
|
|
16
|
+
class AnthropicModelProvider extends BaseModelProvider {
|
|
17
|
+
options;
|
|
18
|
+
client;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
super({
|
|
21
|
+
id: 'anthropic',
|
|
22
|
+
genAiSystem: 'anthropic',
|
|
23
|
+
...(options.harnessLogger ? { logger: options.harnessLogger } : {}),
|
|
24
|
+
...(options.telemetry ? { telemetry: options.telemetry } : {}),
|
|
25
|
+
...(options.harnessTimeoutMs !== undefined ? { timeoutMs: options.harnessTimeoutMs } : options.timeout !== undefined ? { timeoutMs: options.timeout } : {})
|
|
26
|
+
});
|
|
27
|
+
this.options = options;
|
|
28
|
+
this.client = options.client ?? new Anthropic(toClientOptions(options));
|
|
29
|
+
}
|
|
30
|
+
async doText(req) {
|
|
31
|
+
req.signal.throwIfAborted();
|
|
32
|
+
const response = await createMessage(this.client, req, false);
|
|
33
|
+
const toolCalls = extractToolCalls(response, req, 'text');
|
|
34
|
+
return {
|
|
35
|
+
content: response.content?.filter((block) => block.type === 'text').map((block) => block.text).join('') ?? '',
|
|
36
|
+
...(toolCalls ? { toolCalls } : {}),
|
|
37
|
+
usage: toUsage(response.usage?.input_tokens, response.usage?.output_tokens),
|
|
38
|
+
finishReason: toFinishReason(response.stop_reason),
|
|
39
|
+
raw: response
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async *doTextStream(req) {
|
|
43
|
+
req.signal.throwIfAborted();
|
|
44
|
+
const stream = await createMessage(this.client, req, true);
|
|
45
|
+
const toolState = new Map();
|
|
46
|
+
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
47
|
+
let finishReason = 'stop';
|
|
48
|
+
for await (const event of stream) {
|
|
49
|
+
req.signal.throwIfAborted();
|
|
50
|
+
if (event.type === 'message_start') {
|
|
51
|
+
usage = toUsage(event.message?.usage?.input_tokens, event.message?.usage?.output_tokens);
|
|
52
|
+
}
|
|
53
|
+
else if (event.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
|
|
54
|
+
toolState.set(event.index, {
|
|
55
|
+
id: String(event.content_block.id),
|
|
56
|
+
name: String(event.content_block.name),
|
|
57
|
+
input: JSON.stringify(event.content_block.input ?? {})
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else if (event.type === 'content_block_delta') {
|
|
61
|
+
if (event.delta?.type === 'text_delta') {
|
|
62
|
+
yield { kind: 'delta', text: event.delta.text };
|
|
63
|
+
}
|
|
64
|
+
else if (event.delta?.type === 'input_json_delta') {
|
|
65
|
+
const state = toolState.get(event.index);
|
|
66
|
+
if (state)
|
|
67
|
+
state.input += event.delta.partial_json;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (event.type === 'content_block_stop') {
|
|
71
|
+
const state = toolState.get(event.index);
|
|
72
|
+
if (state) {
|
|
73
|
+
yield { kind: 'tool_call', call: { id: state.id, name: state.name, arguments: parseJson(state.input, req, 'textStream') } };
|
|
74
|
+
toolState.delete(event.index);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (event.type === 'message_delta') {
|
|
78
|
+
finishReason = toFinishReason(event.delta?.stop_reason);
|
|
79
|
+
usage = toUsage(usage.inputTokens, event.usage?.output_tokens ?? usage.outputTokens);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
yield { kind: 'finish', usage, finishReason };
|
|
83
|
+
}
|
|
84
|
+
async doObject(req) {
|
|
85
|
+
req.signal.throwIfAborted();
|
|
86
|
+
const response = await createMessage(this.client, req, false, true);
|
|
87
|
+
const toolUse = response.content?.find((block) => block.type === 'tool_use' && block.name === 'harness_response');
|
|
88
|
+
const object = (toolUse?.input ?? parseJson(response.content?.filter((block) => block.type === 'text').map((block) => block.text).join('') || '{}', req, 'object'));
|
|
89
|
+
return {
|
|
90
|
+
object,
|
|
91
|
+
usage: toUsage(response.usage?.input_tokens, response.usage?.output_tokens),
|
|
92
|
+
finishReason: toFinishReason(response.stop_reason),
|
|
93
|
+
raw: response
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async *doObjectStream(req) {
|
|
97
|
+
req.signal.throwIfAborted();
|
|
98
|
+
const stream = await createMessage(this.client, req, true, true);
|
|
99
|
+
let text = '';
|
|
100
|
+
let objectInput = '';
|
|
101
|
+
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
102
|
+
let finishReason = 'stop';
|
|
103
|
+
for await (const event of stream) {
|
|
104
|
+
req.signal.throwIfAborted();
|
|
105
|
+
if (event.type === 'message_start') {
|
|
106
|
+
usage = toUsage(event.message?.usage?.input_tokens, event.message?.usage?.output_tokens);
|
|
107
|
+
}
|
|
108
|
+
else if (event.type === 'content_block_start' && event.content_block?.type === 'tool_use' && event.content_block.name === 'harness_response') {
|
|
109
|
+
objectInput = JSON.stringify(event.content_block.input ?? {});
|
|
110
|
+
}
|
|
111
|
+
else if (event.type === 'content_block_delta') {
|
|
112
|
+
if (event.delta?.type === 'text_delta') {
|
|
113
|
+
text += event.delta.text;
|
|
114
|
+
yield { kind: 'partial', partial: safePartialJson(text) };
|
|
115
|
+
}
|
|
116
|
+
else if (event.delta?.type === 'input_json_delta') {
|
|
117
|
+
objectInput += event.delta.partial_json;
|
|
118
|
+
yield { kind: 'partial', partial: safePartialJson(objectInput) };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (event.type === 'message_delta') {
|
|
122
|
+
finishReason = toFinishReason(event.delta?.stop_reason);
|
|
123
|
+
usage = toUsage(usage.inputTokens, event.usage?.output_tokens ?? usage.outputTokens);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const object = parseJson(objectInput || text || '{}', req, 'objectStream');
|
|
127
|
+
yield { kind: 'finish', object, usage, finishReason };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function toClientOptions(options) {
|
|
131
|
+
const { client: _client, harnessLogger: _harnessLogger, telemetry: _telemetry, harnessTimeoutMs: _harnessTimeoutMs, ...clientOptions } = options;
|
|
132
|
+
return clientOptions;
|
|
133
|
+
}
|
|
134
|
+
async function createMessage(client, req, stream, forceObject = false) {
|
|
135
|
+
const providerOptions = {
|
|
136
|
+
...(req.defaults?.providerOptions ?? {}),
|
|
137
|
+
...(req.call?.providerOptions ?? {})
|
|
138
|
+
};
|
|
139
|
+
const { requestOptions, ...bodyOptions } = providerOptions;
|
|
140
|
+
const { system, messages } = toAnthropicMessages(req.messages);
|
|
141
|
+
const tools = forceObject ? [toObjectTool(req)] : toTools(req.tools);
|
|
142
|
+
return client.messages.create({
|
|
143
|
+
model: req.model,
|
|
144
|
+
messages,
|
|
145
|
+
stream,
|
|
146
|
+
max_tokens: req.call?.maxTokens ?? req.defaults?.maxTokens ?? 1024,
|
|
147
|
+
...(system ? { system } : {}),
|
|
148
|
+
...(tools ? { tools } : {}),
|
|
149
|
+
...(forceObject ? { tool_choice: { type: 'tool', name: 'harness_response' } } : {}),
|
|
150
|
+
...(req.call?.temperature ?? req.defaults?.temperature !== undefined ? { temperature: req.call?.temperature ?? req.defaults?.temperature } : {}),
|
|
151
|
+
...(req.call?.topP ?? req.defaults?.topP !== undefined ? { top_p: req.call?.topP ?? req.defaults?.topP } : {}),
|
|
152
|
+
...(req.call?.stopSequences ?? req.defaults?.stopSequences ? { stop_sequences: req.call?.stopSequences ?? req.defaults?.stopSequences } : {}),
|
|
153
|
+
...bodyOptions
|
|
154
|
+
}, { ...requestOptions, signal: req.signal });
|
|
155
|
+
}
|
|
156
|
+
function toAnthropicMessages(messages) {
|
|
157
|
+
const system = messages.filter((message) => message.role === 'system').map((message) => message.content).join('\n\n');
|
|
158
|
+
const converted = messages.filter((message) => message.role !== 'system').map((message) => {
|
|
159
|
+
if (message.role === 'tool') {
|
|
160
|
+
return {
|
|
161
|
+
role: 'user',
|
|
162
|
+
content: [{ type: 'tool_result', tool_use_id: message.toolCallId, content: message.content }]
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (message.role === 'assistant' && message.toolCalls && message.toolCalls.length > 0) {
|
|
166
|
+
return {
|
|
167
|
+
role: 'assistant',
|
|
168
|
+
content: [
|
|
169
|
+
...(typeof message.content === 'string' && message.content ? [{ type: 'text', text: message.content }] : []),
|
|
170
|
+
...message.toolCalls.map((call) => ({ type: 'tool_use', id: call.id, name: call.name, input: call.arguments }))
|
|
171
|
+
]
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
role: message.role,
|
|
176
|
+
content: typeof message.content === 'string' ? message.content : message.content.map(toContentBlock)
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
return {
|
|
180
|
+
...(system ? { system } : {}),
|
|
181
|
+
messages: converted
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function toContentBlock(part) {
|
|
185
|
+
if (part.kind === 'text')
|
|
186
|
+
return { type: 'text', text: part.text };
|
|
187
|
+
if (part.kind === 'image') {
|
|
188
|
+
return { type: 'image', source: { type: 'base64', media_type: part.mimeType, data: part.dataBase64 } };
|
|
189
|
+
}
|
|
190
|
+
if (part.kind === 'image_url') {
|
|
191
|
+
return { type: 'image', source: { type: 'url', url: part.url } };
|
|
192
|
+
}
|
|
193
|
+
return { type: 'text', text: `[unsupported ${part.kind} content omitted]` };
|
|
194
|
+
}
|
|
195
|
+
function toTools(tools) {
|
|
196
|
+
if (!tools || tools.length === 0)
|
|
197
|
+
return undefined;
|
|
198
|
+
return tools.map((tool) => ({
|
|
199
|
+
name: tool.name,
|
|
200
|
+
description: tool.description,
|
|
201
|
+
input_schema: tool.parameters
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
function toObjectTool(req) {
|
|
205
|
+
return {
|
|
206
|
+
name: 'harness_response',
|
|
207
|
+
description: 'Return the structured response object.',
|
|
208
|
+
input_schema: req.schema
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function extractToolCalls(response, req, method) {
|
|
212
|
+
const calls = response.content?.filter((block) => block.type === 'tool_use' && block.name && block.id);
|
|
213
|
+
if (!calls || calls.length === 0)
|
|
214
|
+
return undefined;
|
|
215
|
+
return calls.map((call) => ({
|
|
216
|
+
id: String(call.id),
|
|
217
|
+
name: String(call.name),
|
|
218
|
+
arguments: typeof call.input === 'string' ? parseJson(call.input, req, method) : call.input ?? {}
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
function parseJson(content, req, method) {
|
|
222
|
+
try {
|
|
223
|
+
return JSON.parse(content);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
throw malformedResponseError(req, method, 'Anthropic returned malformed structured JSON.', content, error);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function malformedResponseError(req, method, message, body, cause) {
|
|
230
|
+
return new ModelError(message, {
|
|
231
|
+
provider: 'anthropic',
|
|
232
|
+
model: req.model,
|
|
233
|
+
method,
|
|
234
|
+
reason: 'malformed_response',
|
|
235
|
+
providerBody: body
|
|
236
|
+
}, cause);
|
|
237
|
+
}
|
|
238
|
+
function safePartialJson(content) {
|
|
239
|
+
try {
|
|
240
|
+
return JSON.parse(content);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return { _partial: content };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function toUsage(inputTokens, outputTokens) {
|
|
247
|
+
const input = inputTokens ?? 0;
|
|
248
|
+
const output = outputTokens ?? 0;
|
|
249
|
+
return { inputTokens: input, outputTokens: output, totalTokens: input + output };
|
|
250
|
+
}
|
|
251
|
+
function toFinishReason(value) {
|
|
252
|
+
switch (value) {
|
|
253
|
+
case 'end_turn':
|
|
254
|
+
case 'stop_sequence':
|
|
255
|
+
return 'stop';
|
|
256
|
+
case 'max_tokens':
|
|
257
|
+
return 'length';
|
|
258
|
+
case 'tool_use':
|
|
259
|
+
return 'tool_calls';
|
|
260
|
+
default:
|
|
261
|
+
return 'error';
|
|
262
|
+
}
|
|
263
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@purista/harness-anthropic",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Anthropic model provider adapter for @purista/harness.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/**/*.js",
|
|
16
|
+
"dist/**/*.d.ts",
|
|
17
|
+
"package.json",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/puristajs/harness",
|
|
26
|
+
"directory": "packages/harness-anthropic"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/puristajs/harness/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/puristajs/harness#readme",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"clean": "rm -rf dist",
|
|
37
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
38
|
+
"typecheck": "tsc -p tsconfig.typecheck.json",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"lint": "echo 'no lint configured for @purista/harness-anthropic'"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@anthropic-ai/sdk": "^0.95.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
47
|
+
"typescript": "^6.0.3",
|
|
48
|
+
"vitest": "^4.1.5"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@purista/harness": "*"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=24.15.0"
|
|
55
|
+
}
|
|
56
|
+
}
|