@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.
Files changed (2) hide show
  1. package/dist/index.js +97 -50
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BaseModelProvider, ModelError } from '@purista/harness';
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: toUsage(response.usage?.input_tokens, response.usage?.output_tokens),
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 = toUsage(event.message?.usage?.input_tokens, event.message?.usage?.output_tokens);
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: JSON.stringify(event.content_block.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.input, req, 'textStream') } };
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
- finishReason = toFinishReason(event.delta?.stop_reason);
79
- usage = toUsage(usage.inputTokens, event.usage?.output_tokens ?? usage.outputTokens);
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: toUsage(response.usage?.input_tokens, response.usage?.output_tokens),
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 = toUsage(event.message?.usage?.input_tokens, event.message?.usage?.output_tokens);
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' && event.content_block.name === 'harness_response') {
111
- objectInput = JSON.stringify(event.content_block.input ?? {});
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
- objectInput += event.delta.partial_json;
120
- yield { kind: 'partial', partial: safePartialJson(objectInput) };
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
- finishReason = toFinishReason(event.delta?.stop_reason);
125
- usage = toUsage(usage.inputTokens, event.usage?.output_tokens ?? usage.outputTokens);
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 object = parseJson(objectInput || text || '{}', req, 'objectStream');
129
- yield { kind: 'finish', object, usage, finishReason };
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
- function withoutObjectTool(calls) {
236
- const filtered = calls?.filter((call) => call.name !== 'harness_response');
237
- return filtered && filtered.length > 0 ? filtered : undefined;
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
- try {
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.2.5",
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"