Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f273530

Browse files
authoredJan 18, 2024
fix: handle system_fingerprint in streaming helpers (#636)
1 parent e48cd57 commit f273530

File tree

1 file changed

+81
-51
lines changed

1 file changed

+81
-51
lines changed
 

‎src/lib/ChatCompletionStream.ts

+81-51
Original file line numberDiff line numberDiff line change
@@ -156,24 +156,28 @@ export class ChatCompletionStream
156156
for (const { delta, finish_reason, index, logprobs = null, ...other } of chunk.choices) {
157157
let choice = snapshot.choices[index];
158158
if (!choice) {
159-
snapshot.choices[index] = { finish_reason, index, message: delta, logprobs, ...other };
160-
continue;
159+
choice = snapshot.choices[index] = { finish_reason, index, message: {}, logprobs, ...other };
161160
}
162161

163162
if (logprobs) {
164163
if (!choice.logprobs) {
165-
choice.logprobs = logprobs;
166-
} else if (logprobs.content) {
167-
choice.logprobs.content ??= [];
168-
choice.logprobs.content.push(...logprobs.content);
164+
choice.logprobs = Object.assign({}, logprobs);
165+
} else {
166+
const { content, ...rest } = logprobs;
167+
Object.assign(choice.logprobs, rest);
168+
if (content) {
169+
choice.logprobs.content ??= [];
170+
choice.logprobs.content.push(...content);
171+
}
169172
}
170173
}
171174

172175
if (finish_reason) choice.finish_reason = finish_reason;
173176
Object.assign(choice, other);
174177

175178
if (!delta) continue; // Shouldn't happen; just in case.
176-
const { content, function_call, role, tool_calls } = delta;
179+
const { content, function_call, role, tool_calls, ...rest } = delta;
180+
Object.assign(choice.message, rest);
177181

178182
if (content) choice.message.content = (choice.message.content || '') + content;
179183
if (role) choice.message.role = role;
@@ -190,8 +194,9 @@ export class ChatCompletionStream
190194
}
191195
if (tool_calls) {
192196
if (!choice.message.tool_calls) choice.message.tool_calls = [];
193-
for (const { index, id, type, function: fn } of tool_calls) {
197+
for (const { index, id, type, function: fn, ...rest } of tool_calls) {
194198
const tool_call = (choice.message.tool_calls[index] ??= {});
199+
Object.assign(tool_call, rest);
195200
if (id) tool_call.id = id;
196201
if (type) tool_call.type = type;
197202
if (fn) tool_call.function ??= { arguments: '' };
@@ -248,59 +253,72 @@ export class ChatCompletionStream
248253
}
249254

250255
function finalizeChatCompletion(snapshot: ChatCompletionSnapshot): ChatCompletion {
251-
const { id, choices, created, model } = snapshot;
256+
const { id, choices, created, model, system_fingerprint, ...rest } = snapshot;
252257
return {
258+
...rest,
253259
id,
254-
choices: choices.map(({ message, finish_reason, index, logprobs }): ChatCompletion.Choice => {
255-
if (!finish_reason) throw new OpenAIError(`missing finish_reason for choice ${index}`);
256-
const { content = null, function_call, tool_calls } = message;
257-
const role = message.role as 'assistant'; // this is what we expect; in theory it could be different which would make our types a slight lie but would be fine.
258-
if (!role) throw new OpenAIError(`missing role for choice ${index}`);
259-
if (function_call) {
260-
const { arguments: args, name } = function_call;
261-
if (args == null) throw new OpenAIError(`missing function_call.arguments for choice ${index}`);
262-
if (!name) throw new OpenAIError(`missing function_call.name for choice ${index}`);
260+
choices: choices.map(
261+
({ message, finish_reason, index, logprobs, ...choiceRest }): ChatCompletion.Choice => {
262+
if (!finish_reason) throw new OpenAIError(`missing finish_reason for choice ${index}`);
263+
const { content = null, function_call, tool_calls, ...messageRest } = message;
264+
const role = message.role as 'assistant'; // this is what we expect; in theory it could be different which would make our types a slight lie but would be fine.
265+
if (!role) throw new OpenAIError(`missing role for choice ${index}`);
266+
if (function_call) {
267+
const { arguments: args, name } = function_call;
268+
if (args == null) throw new OpenAIError(`missing function_call.arguments for choice ${index}`);
269+
if (!name) throw new OpenAIError(`missing function_call.name for choice ${index}`);
270+
return {
271+
...choiceRest,
272+
message: { content, function_call: { arguments: args, name }, role },
273+
finish_reason,
274+
index,
275+
logprobs,
276+
};
277+
}
278+
if (tool_calls) {
279+
return {
280+
...choiceRest,
281+
index,
282+
finish_reason,
283+
logprobs,
284+
message: {
285+
...messageRest,
286+
role,
287+
content,
288+
tool_calls: tool_calls.map((tool_call, i) => {
289+
const { function: fn, type, id, ...toolRest } = tool_call;
290+
const { arguments: args, name, ...fnRest } = fn || {};
291+
if (id == null)
292+
throw new OpenAIError(`missing choices[${index}].tool_calls[${i}].id\n${str(snapshot)}`);
293+
if (type == null)
294+
throw new OpenAIError(`missing choices[${index}].tool_calls[${i}].type\n${str(snapshot)}`);
295+
if (name == null)
296+
throw new OpenAIError(
297+
`missing choices[${index}].tool_calls[${i}].function.name\n${str(snapshot)}`,
298+
);
299+
if (args == null)
300+
throw new OpenAIError(
301+
`missing choices[${index}].tool_calls[${i}].function.arguments\n${str(snapshot)}`,
302+
);
303+
304+
return { ...toolRest, id, type, function: { ...fnRest, name, arguments: args } };
305+
}),
306+
},
307+
};
308+
}
263309
return {
264-
message: { content, function_call: { arguments: args, name }, role },
310+
...choiceRest,
311+
message: { ...messageRest, content, role },
265312
finish_reason,
266313
index,
267314
logprobs,
268315
};
269-
}
270-
if (tool_calls) {
271-
return {
272-
index,
273-
finish_reason,
274-
logprobs,
275-
message: {
276-
role,
277-
content,
278-
tool_calls: tool_calls.map((tool_call, i) => {
279-
const { function: fn, type, id } = tool_call;
280-
const { arguments: args, name } = fn || {};
281-
if (id == null)
282-
throw new OpenAIError(`missing choices[${index}].tool_calls[${i}].id\n${str(snapshot)}`);
283-
if (type == null)
284-
throw new OpenAIError(`missing choices[${index}].tool_calls[${i}].type\n${str(snapshot)}`);
285-
if (name == null)
286-
throw new OpenAIError(
287-
`missing choices[${index}].tool_calls[${i}].function.name\n${str(snapshot)}`,
288-
);
289-
if (args == null)
290-
throw new OpenAIError(
291-
`missing choices[${index}].tool_calls[${i}].function.arguments\n${str(snapshot)}`,
292-
);
293-
294-
return { id, type, function: { name, arguments: args } };
295-
}),
296-
},
297-
};
298-
}
299-
return { message: { content: content, role }, finish_reason, index, logprobs };
300-
}),
316+
},
317+
),
301318
created,
302319
model,
303320
object: 'chat.completion',
321+
...(system_fingerprint ? { system_fingerprint } : {}),
304322
};
305323
}
306324

@@ -333,6 +351,18 @@ export interface ChatCompletionSnapshot {
333351
* The model to generate the completion.
334352
*/
335353
model: string;
354+
355+
// Note we do not include an "object" type on the snapshot,
356+
// because the object is not a valid "chat.completion" until finalized.
357+
// object: 'chat.completion';
358+
359+
/**
360+
* This fingerprint represents the backend configuration that the model runs with.
361+
*
362+
* Can be used in conjunction with the `seed` request parameter to understand when
363+
* backend changes have been made that might impact determinism.
364+
*/
365+
system_fingerprint?: string;
336366
}
337367

338368
export namespace ChatCompletionSnapshot {

0 commit comments

Comments
 (0)
Please sign in to comment.