@purista/harness-azure-foundry 1.2.6 → 1.5.1
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 +53 -71
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseModelProvider,
|
|
1
|
+
import { BaseModelProvider, accumulateStreamToolCallDeltas, createStreamToolCallState, finalizeStreamToolCalls, parseProviderJson, safePartialJson, toTokenUsage } from '@purista/harness';
|
|
2
2
|
import ModelClient, {} from '@azure-rest/ai-inference';
|
|
3
3
|
import { AzureKeyCredential } from '@azure/core-auth';
|
|
4
4
|
import { createSseStream } from '@azure/core-sse';
|
|
@@ -41,8 +41,9 @@ class AzureFoundryModelProvider extends BaseModelProvider {
|
|
|
41
41
|
return {
|
|
42
42
|
content: choice?.message?.content ?? '',
|
|
43
43
|
...(toolCalls ? { toolCalls } : {}),
|
|
44
|
-
usage:
|
|
44
|
+
usage: toTokenUsage(body.usage?.prompt_tokens, body.usage?.completion_tokens, body.usage?.total_tokens),
|
|
45
45
|
finishReason: toFinishReason(choice?.finish_reason),
|
|
46
|
+
outcome: toOutcome(choice?.finish_reason),
|
|
46
47
|
raw: response
|
|
47
48
|
};
|
|
48
49
|
}
|
|
@@ -50,8 +51,9 @@ class AzureFoundryModelProvider extends BaseModelProvider {
|
|
|
50
51
|
req.signal.throwIfAborted();
|
|
51
52
|
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
52
53
|
let finishReason = 'stop';
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
let providerFinishReason;
|
|
55
|
+
const toolState = createStreamToolCallState();
|
|
56
|
+
for await (const event of streamChat(this.client, req)) {
|
|
55
57
|
req.signal.throwIfAborted();
|
|
56
58
|
const data = parseStreamData(event, req, 'textStream');
|
|
57
59
|
if (!data)
|
|
@@ -61,20 +63,21 @@ class AzureFoundryModelProvider extends BaseModelProvider {
|
|
|
61
63
|
yield { kind: 'delta', text: choice.delta.content };
|
|
62
64
|
}
|
|
63
65
|
if (choice.delta?.tool_calls) {
|
|
64
|
-
|
|
66
|
+
accumulateStreamToolCallDeltas(toolState, choice.delta.tool_calls);
|
|
65
67
|
}
|
|
66
68
|
if (choice.finish_reason) {
|
|
67
|
-
|
|
69
|
+
providerFinishReason = choice.finish_reason;
|
|
70
|
+
finishReason = toFinishReason(providerFinishReason);
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
if (data.usage) {
|
|
71
|
-
usage =
|
|
74
|
+
usage = toTokenUsage(data.usage.prompt_tokens, data.usage.completion_tokens, data.usage.total_tokens);
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
|
-
for (const call of finalizeStreamToolCalls(toolState, req, 'textStream')) {
|
|
77
|
+
for (const call of finalizeStreamToolCalls(toolState, callContext(req, 'textStream'), MALFORMED_JSON_MESSAGE)) {
|
|
75
78
|
yield { kind: 'tool_call', call };
|
|
76
79
|
}
|
|
77
|
-
yield { kind: 'finish', usage, finishReason };
|
|
80
|
+
yield { kind: 'finish', usage, finishReason, outcome: streamOutcome(finishReason, providerFinishReason) };
|
|
78
81
|
}
|
|
79
82
|
async doObject(req) {
|
|
80
83
|
req.signal.throwIfAborted();
|
|
@@ -86,8 +89,9 @@ class AzureFoundryModelProvider extends BaseModelProvider {
|
|
|
86
89
|
return {
|
|
87
90
|
object: parseJson(text, req, 'object'),
|
|
88
91
|
...(toolCalls ? { toolCalls } : {}),
|
|
89
|
-
usage:
|
|
92
|
+
usage: toTokenUsage(body.usage?.prompt_tokens, body.usage?.completion_tokens, body.usage?.total_tokens),
|
|
90
93
|
finishReason: toFinishReason(choice?.finish_reason),
|
|
94
|
+
outcome: toOutcome(choice?.finish_reason),
|
|
91
95
|
raw: response
|
|
92
96
|
};
|
|
93
97
|
}
|
|
@@ -96,8 +100,9 @@ class AzureFoundryModelProvider extends BaseModelProvider {
|
|
|
96
100
|
let partial = '';
|
|
97
101
|
let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
98
102
|
let finishReason = 'stop';
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
let providerFinishReason;
|
|
104
|
+
const toolState = createStreamToolCallState();
|
|
105
|
+
for await (const event of streamChat(this.client, req)) {
|
|
101
106
|
req.signal.throwIfAborted();
|
|
102
107
|
const data = parseStreamData(event, req, 'objectStream');
|
|
103
108
|
if (!data)
|
|
@@ -108,21 +113,22 @@ class AzureFoundryModelProvider extends BaseModelProvider {
|
|
|
108
113
|
yield { kind: 'partial', partial: safePartialJson(partial) };
|
|
109
114
|
}
|
|
110
115
|
if (choice.delta?.tool_calls) {
|
|
111
|
-
|
|
116
|
+
accumulateStreamToolCallDeltas(toolState, choice.delta.tool_calls);
|
|
112
117
|
}
|
|
113
118
|
if (choice.finish_reason) {
|
|
114
|
-
|
|
119
|
+
providerFinishReason = choice.finish_reason;
|
|
120
|
+
finishReason = toFinishReason(providerFinishReason);
|
|
115
121
|
}
|
|
116
122
|
}
|
|
117
123
|
if (data.usage) {
|
|
118
|
-
usage =
|
|
124
|
+
usage = toTokenUsage(data.usage.prompt_tokens, data.usage.completion_tokens, data.usage.total_tokens);
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
|
-
for (const call of finalizeStreamToolCalls(toolState, req, 'objectStream')) {
|
|
127
|
+
for (const call of finalizeStreamToolCalls(toolState, callContext(req, 'objectStream'), MALFORMED_JSON_MESSAGE)) {
|
|
122
128
|
yield { kind: 'tool_call', call };
|
|
123
129
|
}
|
|
124
130
|
const object = parseJson(partial || '{}', req, 'objectStream');
|
|
125
|
-
yield { kind: 'finish', object, usage, finishReason };
|
|
131
|
+
yield { kind: 'finish', object, usage, finishReason, outcome: streamOutcome(finishReason, providerFinishReason) };
|
|
126
132
|
}
|
|
127
133
|
async doEmbed(req) {
|
|
128
134
|
req.signal.throwIfAborted();
|
|
@@ -146,7 +152,7 @@ class AzureFoundryModelProvider extends BaseModelProvider {
|
|
|
146
152
|
index: item.index,
|
|
147
153
|
vector: Array.isArray(item.embedding) ? item.embedding : []
|
|
148
154
|
})),
|
|
149
|
-
usage:
|
|
155
|
+
usage: toTokenUsage(body.usage?.prompt_tokens, 0, body.usage?.total_tokens),
|
|
150
156
|
raw: response
|
|
151
157
|
};
|
|
152
158
|
}
|
|
@@ -160,7 +166,10 @@ function createClient(options) {
|
|
|
160
166
|
if (!auth) {
|
|
161
167
|
throw new Error('Azure AI Foundry apiKey or credential is required when no client is injected.');
|
|
162
168
|
}
|
|
163
|
-
|
|
169
|
+
// The harness owns retry budgets (spec 23): disable the SDK pipeline's
|
|
170
|
+
// default retries while preserving explicit user retryOptions as an escape
|
|
171
|
+
// hatch via the spread below.
|
|
172
|
+
return ModelClient(endpoint, auth, { retryOptions: { maxRetries: 0 }, ...clientOptions });
|
|
164
173
|
}
|
|
165
174
|
async function postChat(client, req, stream) {
|
|
166
175
|
const providerOptions = {
|
|
@@ -188,7 +197,7 @@ async function postChat(client, req, stream) {
|
|
|
188
197
|
abortSignal: req.signal
|
|
189
198
|
});
|
|
190
199
|
}
|
|
191
|
-
async function* streamChat(client, req
|
|
200
|
+
async function* streamChat(client, req) {
|
|
192
201
|
const response = await postChat(client, req, true);
|
|
193
202
|
const nodeResponse = typeof response.asNodeStream === 'function' ? await response.asNodeStream() : response;
|
|
194
203
|
if (nodeResponse.status && nodeResponse.status !== '200' && nodeResponse.status !== 200) {
|
|
@@ -299,29 +308,6 @@ function extractToolCalls(toolCalls, req, method) {
|
|
|
299
308
|
arguments: parseJson(call.function.arguments ?? '{}', req, method)
|
|
300
309
|
}));
|
|
301
310
|
}
|
|
302
|
-
function accumulateToolCallDeltas(state, deltas) {
|
|
303
|
-
for (const delta of deltas) {
|
|
304
|
-
const index = typeof delta?.index === 'number' ? delta.index : 0;
|
|
305
|
-
const existing = state.get(index) ?? { args: '' };
|
|
306
|
-
if (delta?.id)
|
|
307
|
-
existing.id = String(delta.id);
|
|
308
|
-
if (delta?.function?.name)
|
|
309
|
-
existing.name = String(delta.function.name);
|
|
310
|
-
if (typeof delta?.function?.arguments === 'string')
|
|
311
|
-
existing.args += delta.function.arguments;
|
|
312
|
-
state.set(index, existing);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
function finalizeStreamToolCalls(state, req, method) {
|
|
316
|
-
return [...state.entries()]
|
|
317
|
-
.sort((a, b) => a[0] - b[0])
|
|
318
|
-
.filter(([, call]) => call.id && call.name)
|
|
319
|
-
.map(([, call]) => ({
|
|
320
|
-
id: call.id,
|
|
321
|
-
name: call.name,
|
|
322
|
-
arguments: parseJson(call.args || '{}', req, method)
|
|
323
|
-
}));
|
|
324
|
-
}
|
|
325
311
|
function parseStreamData(event, req, method) {
|
|
326
312
|
if (event === '[DONE]')
|
|
327
313
|
return undefined;
|
|
@@ -329,35 +315,12 @@ function parseStreamData(event, req, method) {
|
|
|
329
315
|
return parseJson(event, req, method);
|
|
330
316
|
return event;
|
|
331
317
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
catch (error) {
|
|
337
|
-
throw malformedResponseError(req, method, 'Azure AI Foundry returned malformed JSON.', content, error);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
function malformedResponseError(req, method, message, body, cause) {
|
|
341
|
-
return new ModelError(message, {
|
|
342
|
-
provider: 'azure-foundry',
|
|
343
|
-
model: req.model,
|
|
344
|
-
method,
|
|
345
|
-
reason: 'malformed_response',
|
|
346
|
-
providerBody: body
|
|
347
|
-
}, cause);
|
|
348
|
-
}
|
|
349
|
-
function safePartialJson(content) {
|
|
350
|
-
try {
|
|
351
|
-
return JSON.parse(content);
|
|
352
|
-
}
|
|
353
|
-
catch {
|
|
354
|
-
return { _partial: content };
|
|
355
|
-
}
|
|
318
|
+
const MALFORMED_JSON_MESSAGE = 'Azure AI Foundry returned malformed JSON.';
|
|
319
|
+
function callContext(req, method) {
|
|
320
|
+
return { provider: 'azure-foundry', model: req.model, method };
|
|
356
321
|
}
|
|
357
|
-
function
|
|
358
|
-
|
|
359
|
-
const output = outputTokens ?? 0;
|
|
360
|
-
return { inputTokens: input, outputTokens: output, totalTokens: totalTokens ?? input + output };
|
|
322
|
+
function parseJson(content, req, method) {
|
|
323
|
+
return parseProviderJson(content, callContext(req, method), MALFORMED_JSON_MESSAGE);
|
|
361
324
|
}
|
|
362
325
|
function toFinishReason(value) {
|
|
363
326
|
switch (value) {
|
|
@@ -369,7 +332,26 @@ function toFinishReason(value) {
|
|
|
369
332
|
return 'tool_calls';
|
|
370
333
|
case 'content_filter':
|
|
371
334
|
return 'content_filter';
|
|
335
|
+
case 'function_call':
|
|
336
|
+
return 'tool_calls';
|
|
372
337
|
default:
|
|
373
338
|
return 'error';
|
|
374
339
|
}
|
|
375
340
|
}
|
|
341
|
+
function toOutcome(value) {
|
|
342
|
+
const finishReason = toFinishReason(value);
|
|
343
|
+
return {
|
|
344
|
+
finishReason,
|
|
345
|
+
...(typeof value === 'string' ? { providerFinishReason: value } : {})
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Stream outcome built from the tracked finish reason; `providerFinishReason`
|
|
350
|
+
* is omitted entirely when the provider never sent a finish reason.
|
|
351
|
+
*/
|
|
352
|
+
function streamOutcome(finishReason, providerFinishReason) {
|
|
353
|
+
return {
|
|
354
|
+
finishReason,
|
|
355
|
+
...(typeof providerFinishReason === 'string' ? { providerFinishReason } : {})
|
|
356
|
+
};
|
|
357
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@purista/harness-azure-foundry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Azure AI Foundry model provider adapter for @purista/harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"@azure/core-sse": "^2.3.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@vitest/coverage-v8": "^4.1.
|
|
48
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
49
49
|
"typescript": "^6.0.3",
|
|
50
|
-
"vitest": "^4.1.
|
|
50
|
+
"vitest": "^4.1.9"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@purista/harness": "
|
|
53
|
+
"@purista/harness": "^1.5.1"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
|
56
56
|
"node": ">=24.15.0"
|