Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions src/extra/structChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,23 +118,43 @@ export function convertToParsedChatCompletionResponse<T extends z.ZodTypeAny>(re
for (const _choice of response.choices) {
if (_choice.message === null || typeof _choice.message === 'undefined') {
parsedChoices.push({..._choice, message: undefined});
} else if (
_choice.message.content !== null
&& typeof _choice.message.content !== 'undefined'
&& !Array.isArray(_choice.message.content)
) {
let parsed: z.infer<T> | undefined;
try {
parsed = responseFormat.safeParse(JSON.parse(_choice.message.content)).data;
} catch {
// JSON.parse can throw if the model returns malformed JSON
// (e.g. truncated output when finish_reason is "length").
// Leave parsed as undefined so callers can detect the failure.
parsed = undefined;
}
parsedChoices.push({
..._choice,
message: {
..._choice.message,
parsed,
},
});
} else {
if (_choice.message.content !== null && typeof _choice.message.content !== 'undefined' && !Array.isArray(_choice.message.content)) {
// content is null, undefined, or an array of content chunks;
// preserve the choice without a parsed field.
parsedChoices.push({
..._choice,
message: {
..._choice.message,
parsed: responseFormat.safeParse(JSON.parse(_choice.message.content)).data,
}
parsed: undefined,
},
});
}
}
}
return {
...response,
choices: parsedChoices,
};

}

// Function to convert Zod schema to strict JSON schema
Expand Down
131 changes: 131 additions & 0 deletions tests/extra/structChat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,137 @@ describe("convertToParsedChatCompletionResponse", () => {
convertToParsedChatCompletionResponse(raw_response, MathDemonstration)
).toStrictEqual(ccr_response);
});

it("should set parsed to undefined when content is malformed JSON", () => {
const truncatedResponse: components.ChatCompletionResponse = {
id: "test-malformed",
object: "chat.completion",
model: "mistral-tiny-latest",
usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 },
created: 1700000000,
choices: [
{
index: 0,
message: {
content: '{"steps": [{"explanation": "truncated',
toolCalls: null,
prefix: false,
role: "assistant",
},
finishReason: "length",
},
],
};

const result = convertToParsedChatCompletionResponse(
truncatedResponse,
MathDemonstration,
);

expect(result.choices).toHaveLength(1);
expect(result.choices![0].message).toBeDefined();
expect(result.choices![0].message!.parsed).toBeUndefined();
expect(result.choices![0].message!.content).toBe(
'{"steps": [{"explanation": "truncated',
);
});

it("should set parsed to undefined when content is valid JSON but fails schema validation", () => {
const wrongShapeResponse: components.ChatCompletionResponse = {
id: "test-wrong-shape",
object: "chat.completion",
model: "mistral-tiny-latest",
usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 },
created: 1700000000,
choices: [
{
index: 0,
message: {
content: '{"wrong_field": "not matching schema"}',
toolCalls: null,
prefix: false,
role: "assistant",
},
finishReason: "stop",
},
],
};

const result = convertToParsedChatCompletionResponse(
wrongShapeResponse,
MathDemonstration,
);

expect(result.choices).toHaveLength(1);
expect(result.choices![0].message).toBeDefined();
expect(result.choices![0].message!.parsed).toBeUndefined();
});

it("should preserve choices when content is null", () => {
const nullContentResponse: components.ChatCompletionResponse = {
id: "test-null-content",
object: "chat.completion",
model: "mistral-tiny-latest",
usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 },
created: 1700000000,
choices: [
{
index: 0,
message: {
content: null,
toolCalls: null,
prefix: false,
role: "assistant",
},
finishReason: "stop",
},
],
};

const result = convertToParsedChatCompletionResponse(
nullContentResponse,
MathDemonstration,
);

expect(result.choices).toHaveLength(1);
expect(result.choices![0].message).toBeDefined();
expect(result.choices![0].message!.parsed).toBeUndefined();
});

it("should return empty choices array for empty choices", () => {
const emptyChoicesResponse: components.ChatCompletionResponse = {
id: "test-empty",
object: "chat.completion",
model: "mistral-tiny-latest",
usage: { promptTokens: 10, completionTokens: 0, totalTokens: 10 },
created: 1700000000,
choices: [],
};

const result = convertToParsedChatCompletionResponse(
emptyChoicesResponse,
MathDemonstration,
);

expect(result.choices).toEqual([]);
});

it("should return undefined choices when choices is undefined", () => {
const noChoicesResponse: components.ChatCompletionResponse = {
id: "test-undefined",
object: "chat.completion",
model: "mistral-tiny-latest",
usage: { promptTokens: 10, completionTokens: 0, totalTokens: 10 },
created: 1700000000,
};

const result = convertToParsedChatCompletionResponse(
noChoicesResponse,
MathDemonstration,
);

expect(result.choices).toBeUndefined();
});
});

describe("responseFormatFromZodObject", () => {
Expand Down