@purista/harness-openai 1.2.1 → 1.2.3

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 +53 -32
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -67,33 +67,31 @@ class OpenAiModelProvider extends BaseModelProvider {
67
67
  req.signal.throwIfAborted();
68
68
  const stream = await createChatCompletion(this.client, req, true);
69
69
  let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
70
+ let finishReason = 'stop';
71
+ const toolState = new Map();
70
72
  for await (const chunk of stream) {
71
73
  req.signal.throwIfAborted();
72
- const choice = chunk.choices[0];
74
+ // The usage chunk arrives with an empty choices array, so read it first.
75
+ if (chunk.usage) {
76
+ usage = toUsage(chunk.usage.prompt_tokens, chunk.usage.completion_tokens);
77
+ }
78
+ const choice = chunk.choices?.[0];
73
79
  if (!choice)
74
80
  continue;
75
81
  if (choice.delta?.content) {
76
82
  yield { kind: 'delta', text: choice.delta.content };
77
83
  }
78
84
  if (choice.delta?.tool_calls) {
79
- for (const call of choice.delta.tool_calls) {
80
- if (!call.function?.name || !call.id)
81
- continue;
82
- yield {
83
- kind: 'tool_call',
84
- call: {
85
- id: call.id,
86
- name: call.function.name,
87
- arguments: parseToolArgs(call.function.arguments, req, 'textStream')
88
- }
89
- };
90
- }
85
+ accumulateToolCallDeltas(toolState, choice.delta.tool_calls);
91
86
  }
92
- if (chunk.usage) {
93
- usage = toUsage(chunk.usage.prompt_tokens, chunk.usage.completion_tokens);
87
+ if (choice.finish_reason) {
88
+ finishReason = toFinishReason(choice.finish_reason);
94
89
  }
95
90
  }
96
- yield { kind: 'finish', usage, finishReason: 'stop' };
91
+ for (const call of finalizeStreamToolCalls(toolState, req, 'textStream')) {
92
+ yield { kind: 'tool_call', call };
93
+ }
94
+ yield { kind: 'finish', usage, finishReason };
97
95
  }
98
96
  async doObject(req) {
99
97
  req.signal.throwIfAborted();
@@ -112,10 +110,15 @@ class OpenAiModelProvider extends BaseModelProvider {
112
110
  req.signal.throwIfAborted();
113
111
  let partial = '';
114
112
  let usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
113
+ let finishReason = 'stop';
114
+ const toolState = new Map();
115
115
  const stream = await createChatCompletion(this.client, req, true);
116
116
  for await (const chunk of stream) {
117
117
  req.signal.throwIfAborted();
118
- const choice = chunk.choices[0];
118
+ if (chunk.usage) {
119
+ usage = toUsage(chunk.usage.prompt_tokens, chunk.usage.completion_tokens);
120
+ }
121
+ const choice = chunk.choices?.[0];
119
122
  if (!choice)
120
123
  continue;
121
124
  if (choice.delta?.content) {
@@ -123,25 +126,17 @@ class OpenAiModelProvider extends BaseModelProvider {
123
126
  yield { kind: 'partial', partial: safePartialJson(partial) };
124
127
  }
125
128
  if (choice.delta?.tool_calls) {
126
- for (const call of choice.delta.tool_calls) {
127
- if (!call.function?.name || !call.id)
128
- continue;
129
- yield {
130
- kind: 'tool_call',
131
- call: {
132
- id: call.id,
133
- name: call.function.name,
134
- arguments: parseToolArgs(call.function.arguments, req, 'objectStream')
135
- }
136
- };
137
- }
129
+ accumulateToolCallDeltas(toolState, choice.delta.tool_calls);
138
130
  }
139
- if (chunk.usage) {
140
- usage = toUsage(chunk.usage.prompt_tokens, chunk.usage.completion_tokens);
131
+ if (choice.finish_reason) {
132
+ finishReason = toFinishReason(choice.finish_reason);
141
133
  }
142
134
  }
135
+ for (const call of finalizeStreamToolCalls(toolState, req, 'objectStream')) {
136
+ yield { kind: 'tool_call', call };
137
+ }
143
138
  const object = parseJson(partial || '{}', req, 'objectStream');
144
- yield { kind: 'finish', object, usage, finishReason: 'stop' };
139
+ yield { kind: 'finish', object, usage, finishReason };
145
140
  }
146
141
  async doEmbed(req) {
147
142
  req.signal.throwIfAborted();
@@ -200,11 +195,14 @@ async function createChatCompletion(client, req, stream) {
200
195
  model: req.model,
201
196
  messages,
202
197
  stream,
198
+ // OpenAI only emits a usage chunk during streaming when this is set.
199
+ ...(stream ? { stream_options: { include_usage: true } } : {}),
203
200
  tools: toTools(req.tools),
204
201
  temperature: req.call?.temperature ?? req.defaults?.temperature,
205
202
  max_tokens: req.call?.maxTokens ?? req.defaults?.maxTokens,
206
203
  top_p: req.call?.topP ?? req.defaults?.topP,
207
204
  stop: req.call?.stopSequences ?? req.defaults?.stopSequences,
205
+ ...(req.tools && (req.call?.parallelToolCalls ?? req.defaults?.parallelToolCalls) !== undefined ? { parallel_tool_calls: req.call?.parallelToolCalls ?? req.defaults?.parallelToolCalls } : {}),
208
206
  response_format: toResponseFormat(req),
209
207
  ...bodyOptions
210
208
  }, { ...requestOptions, signal: req.signal });
@@ -287,6 +285,29 @@ function toTools(tools) {
287
285
  }
288
286
  }));
289
287
  }
288
+ function accumulateToolCallDeltas(state, deltas) {
289
+ for (const delta of deltas) {
290
+ const index = typeof delta?.index === 'number' ? delta.index : 0;
291
+ const existing = state.get(index) ?? { args: '' };
292
+ if (delta?.id)
293
+ existing.id = String(delta.id);
294
+ if (delta?.function?.name)
295
+ existing.name = String(delta.function.name);
296
+ if (typeof delta?.function?.arguments === 'string')
297
+ existing.args += delta.function.arguments;
298
+ state.set(index, existing);
299
+ }
300
+ }
301
+ function finalizeStreamToolCalls(state, req, method) {
302
+ return [...state.entries()]
303
+ .sort((a, b) => a[0] - b[0])
304
+ .filter(([, call]) => call.id && call.name)
305
+ .map(([, call]) => ({
306
+ id: call.id,
307
+ name: call.name,
308
+ arguments: parseToolArgs(call.args || undefined, req, method)
309
+ }));
310
+ }
290
311
  function parseToolArgs(argumentsText, req, method) {
291
312
  if (!argumentsText)
292
313
  return {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@purista/harness-openai",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "OpenAI model provider adapter for @purista/harness.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",