@purista/harness-anthropic 1.2.5 → 1.5.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/dist/index.js +97 -50
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseModelProvider,
|
|
1
|
+
import { BaseModelProvider, parseProviderJson, safePartialJson, toTokenUsage, withoutObjectTool } from '@purista/harness';
|
|
2
2
|
import Anthropic, {} from '@anthropic-ai/sdk';
|
|
3
3
|
/**
|
|
4
4
|
* Creates an Anthropic-backed harness `ModelProvider`.
|
|
@@ -34,8 +34,9 @@ class AnthropicModelProvider extends BaseModelProvider {
|
|
|
34
34
|
return {
|
|
35
35
|
content: response.content?.filter((block) => block.type === 'text').map((block) => block.text).join('') ?? '',
|
|
36
36
|
...(toolCalls ? { toolCalls } : {}),
|
|
37
|
-
usage:
|
|
37
|
+
usage: toTokenUsage(response.usage?.input_tokens, response.usage?.output_tokens),
|
|
38
38
|
finishReason: toFinishReason(response.stop_reason),
|
|
39
|
+
outcome: toOutcome(response.stop_reason),
|
|
39
40
|
raw: response
|
|
40
41
|
};
|
|
41
42
|
}
|
|
@@ -45,16 +46,18 @@ class AnthropicModelProvider extends BaseModelProvider {
|
|
|
45
46
|
const toolState = new Map();
|
|
46
47
|
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
47
48
|
let finishReason = 'stop';
|
|
49
|
+
let providerFinishReason;
|
|
48
50
|
for await (const event of stream) {
|
|
49
51
|
req.signal.throwIfAborted();
|
|
50
52
|
if (event.type === 'message_start') {
|
|
51
|
-
usage =
|
|
53
|
+
usage = toTokenUsage(event.message?.usage?.input_tokens, event.message?.usage?.output_tokens);
|
|
52
54
|
}
|
|
53
55
|
else if (event.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
|
|
54
56
|
toolState.set(event.index, {
|
|
55
57
|
id: String(event.content_block.id),
|
|
56
58
|
name: String(event.content_block.name),
|
|
57
|
-
input:
|
|
59
|
+
input: '',
|
|
60
|
+
startInput: event.content_block.input
|
|
58
61
|
});
|
|
59
62
|
}
|
|
60
63
|
else if (event.type === 'content_block_delta') {
|
|
@@ -70,16 +73,19 @@ class AnthropicModelProvider extends BaseModelProvider {
|
|
|
70
73
|
else if (event.type === 'content_block_stop') {
|
|
71
74
|
const state = toolState.get(event.index);
|
|
72
75
|
if (state) {
|
|
73
|
-
yield { kind: 'tool_call', call: { id: state.id, name: state.name, arguments: parseJson(state
|
|
76
|
+
yield { kind: 'tool_call', call: { id: state.id, name: state.name, arguments: parseJson(toolBlockInputJson(state), req, 'textStream') } };
|
|
74
77
|
toolState.delete(event.index);
|
|
75
78
|
}
|
|
76
79
|
}
|
|
77
80
|
else if (event.type === 'message_delta') {
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
if (event.delta?.stop_reason) {
|
|
82
|
+
providerFinishReason = event.delta.stop_reason;
|
|
83
|
+
finishReason = toFinishReason(providerFinishReason);
|
|
84
|
+
}
|
|
85
|
+
usage = toTokenUsage(usage.inputTokens, event.usage?.output_tokens ?? usage.outputTokens);
|
|
80
86
|
}
|
|
81
87
|
}
|
|
82
|
-
yield { kind: 'finish', usage, finishReason };
|
|
88
|
+
yield { kind: 'finish', usage, finishReason, outcome: streamOutcome(finishReason, providerFinishReason) };
|
|
83
89
|
}
|
|
84
90
|
async doObject(req) {
|
|
85
91
|
req.signal.throwIfAborted();
|
|
@@ -90,8 +96,9 @@ class AnthropicModelProvider extends BaseModelProvider {
|
|
|
90
96
|
return {
|
|
91
97
|
object,
|
|
92
98
|
...(toolCalls ? { toolCalls } : {}),
|
|
93
|
-
usage:
|
|
99
|
+
usage: toTokenUsage(response.usage?.input_tokens, response.usage?.output_tokens),
|
|
94
100
|
finishReason: toFinishReason(response.stop_reason),
|
|
101
|
+
outcome: toOutcome(response.stop_reason),
|
|
95
102
|
raw: response
|
|
96
103
|
};
|
|
97
104
|
}
|
|
@@ -100,15 +107,33 @@ class AnthropicModelProvider extends BaseModelProvider {
|
|
|
100
107
|
const stream = await createMessage(this.client, req, true, true);
|
|
101
108
|
let text = '';
|
|
102
109
|
let objectInput = '';
|
|
110
|
+
let objectBlockIndex;
|
|
111
|
+
let objectStartInput;
|
|
112
|
+
const toolState = new Map();
|
|
103
113
|
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
104
114
|
let finishReason = 'stop';
|
|
115
|
+
let providerFinishReason;
|
|
105
116
|
for await (const event of stream) {
|
|
106
117
|
req.signal.throwIfAborted();
|
|
107
118
|
if (event.type === 'message_start') {
|
|
108
|
-
usage =
|
|
119
|
+
usage = toTokenUsage(event.message?.usage?.input_tokens, event.message?.usage?.output_tokens);
|
|
109
120
|
}
|
|
110
|
-
else if (event.type === 'content_block_start' && event.content_block?.type === 'tool_use'
|
|
111
|
-
|
|
121
|
+
else if (event.type === 'content_block_start' && event.content_block?.type === 'tool_use') {
|
|
122
|
+
// Only the synthetic `harness_response` block carries the structured
|
|
123
|
+
// object; other tool blocks are real tool calls and must not bleed
|
|
124
|
+
// into the object JSON (parity with the OpenAI/Azure adapters).
|
|
125
|
+
if (event.content_block.name === 'harness_response' && objectBlockIndex === undefined) {
|
|
126
|
+
objectBlockIndex = event.index;
|
|
127
|
+
objectStartInput = event.content_block.input;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
toolState.set(event.index, {
|
|
131
|
+
id: String(event.content_block.id),
|
|
132
|
+
name: String(event.content_block.name),
|
|
133
|
+
input: '',
|
|
134
|
+
startInput: event.content_block.input
|
|
135
|
+
});
|
|
136
|
+
}
|
|
112
137
|
}
|
|
113
138
|
else if (event.type === 'content_block_delta') {
|
|
114
139
|
if (event.delta?.type === 'text_delta') {
|
|
@@ -116,22 +141,48 @@ class AnthropicModelProvider extends BaseModelProvider {
|
|
|
116
141
|
yield { kind: 'partial', partial: safePartialJson(text) };
|
|
117
142
|
}
|
|
118
143
|
else if (event.delta?.type === 'input_json_delta') {
|
|
119
|
-
|
|
120
|
-
|
|
144
|
+
if (event.index === objectBlockIndex) {
|
|
145
|
+
objectInput += event.delta.partial_json;
|
|
146
|
+
yield { kind: 'partial', partial: safePartialJson(objectInput) };
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
const state = toolState.get(event.index);
|
|
150
|
+
if (state)
|
|
151
|
+
state.input += event.delta.partial_json;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else if (event.type === 'content_block_stop') {
|
|
156
|
+
const state = toolState.get(event.index);
|
|
157
|
+
if (state) {
|
|
158
|
+
yield { kind: 'tool_call', call: { id: state.id, name: state.name, arguments: parseJson(toolBlockInputJson(state), req, 'objectStream') } };
|
|
159
|
+
toolState.delete(event.index);
|
|
121
160
|
}
|
|
122
161
|
}
|
|
123
162
|
else if (event.type === 'message_delta') {
|
|
124
|
-
|
|
125
|
-
|
|
163
|
+
if (event.delta?.stop_reason) {
|
|
164
|
+
providerFinishReason = event.delta.stop_reason;
|
|
165
|
+
finishReason = toFinishReason(providerFinishReason);
|
|
166
|
+
}
|
|
167
|
+
usage = toTokenUsage(usage.inputTokens, event.usage?.output_tokens ?? usage.outputTokens);
|
|
126
168
|
}
|
|
127
169
|
}
|
|
128
|
-
const
|
|
129
|
-
|
|
170
|
+
const objectSource = objectInput
|
|
171
|
+
|| (objectStartInput !== undefined ? JSON.stringify(objectStartInput) : '')
|
|
172
|
+
|| text
|
|
173
|
+
|| '{}';
|
|
174
|
+
const object = parseJson(objectSource, req, 'objectStream');
|
|
175
|
+
yield { kind: 'finish', object, usage, finishReason, outcome: streamOutcome(finishReason, providerFinishReason) };
|
|
130
176
|
}
|
|
131
177
|
}
|
|
178
|
+
function toolBlockInputJson(state) {
|
|
179
|
+
if (state.input)
|
|
180
|
+
return state.input;
|
|
181
|
+
return JSON.stringify(state.startInput ?? {});
|
|
182
|
+
}
|
|
132
183
|
function toClientOptions(options) {
|
|
133
184
|
const { client: _client, harnessLogger: _harnessLogger, telemetry: _telemetry, harnessTimeoutMs: _harnessTimeoutMs, ...clientOptions } = options;
|
|
134
|
-
return clientOptions;
|
|
185
|
+
return { maxRetries: 0, ...clientOptions };
|
|
135
186
|
}
|
|
136
187
|
async function createMessage(client, req, stream, forceObject = false) {
|
|
137
188
|
const providerOptions = {
|
|
@@ -232,39 +283,12 @@ function extractToolCalls(response, req, method) {
|
|
|
232
283
|
arguments: typeof call.input === 'string' ? parseJson(call.input, req, method) : call.input ?? {}
|
|
233
284
|
}));
|
|
234
285
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return
|
|
286
|
+
const MALFORMED_JSON_MESSAGE = 'Anthropic returned malformed structured JSON.';
|
|
287
|
+
function callContext(req, method) {
|
|
288
|
+
return { provider: 'anthropic', model: req.model, method };
|
|
238
289
|
}
|
|
239
290
|
function parseJson(content, req, method) {
|
|
240
|
-
|
|
241
|
-
return JSON.parse(content);
|
|
242
|
-
}
|
|
243
|
-
catch (error) {
|
|
244
|
-
throw malformedResponseError(req, method, 'Anthropic returned malformed structured JSON.', content, error);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
function malformedResponseError(req, method, message, body, cause) {
|
|
248
|
-
return new ModelError(message, {
|
|
249
|
-
provider: 'anthropic',
|
|
250
|
-
model: req.model,
|
|
251
|
-
method,
|
|
252
|
-
reason: 'malformed_response',
|
|
253
|
-
providerBody: body
|
|
254
|
-
}, cause);
|
|
255
|
-
}
|
|
256
|
-
function safePartialJson(content) {
|
|
257
|
-
try {
|
|
258
|
-
return JSON.parse(content);
|
|
259
|
-
}
|
|
260
|
-
catch {
|
|
261
|
-
return { _partial: content };
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
function toUsage(inputTokens, outputTokens) {
|
|
265
|
-
const input = inputTokens ?? 0;
|
|
266
|
-
const output = outputTokens ?? 0;
|
|
267
|
-
return { inputTokens: input, outputTokens: output, totalTokens: input + output };
|
|
291
|
+
return parseProviderJson(content, callContext(req, method), MALFORMED_JSON_MESSAGE);
|
|
268
292
|
}
|
|
269
293
|
function toFinishReason(value) {
|
|
270
294
|
switch (value) {
|
|
@@ -275,7 +299,30 @@ function toFinishReason(value) {
|
|
|
275
299
|
return 'length';
|
|
276
300
|
case 'tool_use':
|
|
277
301
|
return 'tool_calls';
|
|
302
|
+
case 'pause_turn':
|
|
303
|
+
return 'pause';
|
|
304
|
+
case 'refusal':
|
|
305
|
+
return 'refusal';
|
|
306
|
+
case 'model_context_window_exceeded':
|
|
307
|
+
return 'context_limit';
|
|
278
308
|
default:
|
|
279
309
|
return 'error';
|
|
280
310
|
}
|
|
281
311
|
}
|
|
312
|
+
function toOutcome(value) {
|
|
313
|
+
const finishReason = toFinishReason(value);
|
|
314
|
+
return {
|
|
315
|
+
finishReason,
|
|
316
|
+
...(typeof value === 'string' ? { providerFinishReason: value } : {})
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Stream outcome built from the tracked finish reason; `providerFinishReason`
|
|
321
|
+
* is omitted entirely when the provider never sent a stop reason.
|
|
322
|
+
*/
|
|
323
|
+
function streamOutcome(finishReason, providerFinishReason) {
|
|
324
|
+
return {
|
|
325
|
+
finishReason,
|
|
326
|
+
...(typeof providerFinishReason === 'string' ? { providerFinishReason } : {})
|
|
327
|
+
};
|
|
328
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@purista/harness-anthropic",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Anthropic model provider adapter for @purista/harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"vitest": "^4.1.8"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@purista/harness": "
|
|
51
|
+
"@purista/harness": "^1.5.0"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=24.15.0"
|