Skip to content

Commit da86456

Browse files
feat(client): add parse method (#226)
* feat(client): add parse method * chore(internal): add prettier@2 as a dev dep --------- Co-authored-by: Robert Craigie <robert@craigie.dev>
1 parent 4c495f6 commit da86456

7 files changed

Lines changed: 116 additions & 15 deletions

File tree

jest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const config: JestConfigWithTsJest = {
1717
'<rootDir>/deno_tests/',
1818
],
1919
testPathIgnorePatterns: ['scripts'],
20+
prettierPath: require.resolve('prettier-2'),
2021
};
2122

2223
export default config;

package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"tsconfig-paths": "^4.0.0",
4646
"typescript": "^4.8.2",
4747
"typescript-eslint": "^8.24.0",
48+
"prettier-2": "npm:prettier@^2",
4849
"zod": "^3.23.8"
4950
},
5051
"resolutions": {
@@ -73,5 +74,13 @@
7374
"import": "./dist/*.mjs",
7475
"require": "./dist/*.js"
7576
}
77+
},
78+
"peerDependencies": {
79+
"zod": "^3.23.8"
80+
},
81+
"peerDependenciesMeta": {
82+
"zod": {
83+
"optional": true
84+
}
7685
}
77-
}
86+
}

src/helpers/zod.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ export function zodResponseFormat<ZodInput extends ZodType>(
6666
json_schema: {
6767
...props,
6868
name,
69-
// strict: true,
69+
// @ts-ignore
70+
strict: true,
7071
schema: zodToJsonSchema(zodObject, { name }),
7172
},
7273
},
@@ -96,7 +97,8 @@ export function zodFunction<Parameters extends ZodType>(options: {
9697
function: {
9798
name: options.name,
9899
parameters: zodToJsonSchema(options.parameters, { name: options.name }),
99-
// strict: true,
100+
// @ts-ignore
101+
strict: true,
100102
...(options.description ? { description: options.description } : undefined),
101103
},
102104
},

src/lib/parser.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@ type AnyChatChatParams =
1313
| ChatCompletionStreamingToolRunnerParams<any>
1414
| ChatCompletionStreamParams;
1515

16-
// note: we currently use a placeholder, structured output isn't yet supported by the API
17-
// export type ExtractParsedContentFromParams<Params extends AnyChatChatParams> =
18-
// Params['response_format'] extends AutoParseableResponseFormat<infer P> ? P : null;
19-
export type ExtractParsedContentFromParams<_Params extends AnyChatChatParams> = null;
16+
export type ExtractParsedContentFromParams<Params extends AnyChatChatParams> =
17+
Params['response_format'] extends AutoParseableResponseFormat<infer P> ? P : null;
2018

2119
export type AutoParseableResponseFormat<ParsedT> = ResponseFormatJSONSchema & {
2220
__output: ParsedT; // type-level only
@@ -151,9 +149,22 @@ export function parseChatCompletion<
151149
}
152150

153151
function parseResponseFormat<Params extends ChatChatParams, ParsedT = ExtractParsedContentFromParams<Params>>(
154-
_params: Params,
155-
_content: string,
152+
params: Params,
153+
content: string,
156154
): ParsedT | null {
155+
if ('response_format' in params && params.response_format?.type !== 'json_schema') {
156+
return null;
157+
}
158+
159+
if ('response_format' in params && params.response_format?.type === 'json_schema') {
160+
if (isAutoParsableResponseFormat(params.response_format)) {
161+
const response_format = params.response_format as AutoParseableResponseFormat<ParsedT>;
162+
return response_format.$parseRaw(content);
163+
}
164+
165+
return JSON.parse(content) as unknown as ParsedT;
166+
}
167+
157168
return null;
158169
}
159170

@@ -169,7 +180,10 @@ function parseToolCall<Params extends ChatChatParams>(
169180
function: {
170181
...toolCall.function,
171182
parsed_arguments:
172-
isAutoParsableTool(inputTool) ? inputTool.$parseRaw(toolCall.function.arguments) : null,
183+
isAutoParsableTool(inputTool) ? inputTool.$parseRaw(toolCall.function.arguments)
184+
: inputTool?.function && 'strict' in inputTool.function && inputTool.function.strict === true ?
185+
JSON.parse(toolCall.function.arguments)
186+
: null,
173187
},
174188
};
175189
}
@@ -182,7 +196,11 @@ export function shouldParseToolCall(params: ChatChatParams | null | undefined, t
182196
const inputTool = params.tools?.find(
183197
(inputTool) => inputTool.type === 'function' && inputTool.function?.name === toolCall.function.name,
184198
);
185-
return isAutoParsableTool(inputTool) || false;
199+
return (
200+
isAutoParsableTool(inputTool) ||
201+
(inputTool?.function && 'strict' in inputTool.function && inputTool.function.strict === true) ||
202+
false
203+
);
186204
}
187205

188206
export function hasAutoParseableInput(params: AnyChatChatParams): boolean {
@@ -206,5 +224,11 @@ export function validateInputTools(tools: ToolParam[] | undefined) {
206224
`Currently only \`function\` tool types support auto-parsing; Received \`${tool.type}\``,
207225
);
208226
}
227+
228+
if (!tool.function || !('strict' in tool.function) || tool.function.strict !== true) {
229+
throw new WriterError(
230+
`The \`${tool.function?.name}\` tool is not marked with \`strict: true\`. Only strict function tools can be auto-parsed`,
231+
);
232+
}
209233
}
210234
}

src/resources/chat.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { APIPromise } from '../core/api-promise';
77
import { Stream } from '../core/streaming';
88
import { RequestOptions } from '../internal/request-options';
99
import { ToolCall } from './shared';
10+
import { ResponseFormatJSONSchema } from './shared';
1011
import { ChatCompletionStream, ChatCompletionStreamParams } from '../lib/ChatCompletionStream';
11-
import { ExtractParsedContentFromParams } from '../lib/parser';
12+
import { validateInputTools, maybeParseChatCompletion, ExtractParsedContentFromParams } from '../lib/parser';
1213

1314
export class Chat extends APIResource {
1415
/**
@@ -31,6 +32,57 @@ export class Chat extends APIResource {
3132
| APIPromise<Stream<ChatCompletionChunk>>;
3233
}
3334

35+
/**
36+
* Create a completion and also parse the response
37+
*
38+
* This method automatically parses the response content into a structured format
39+
* based on the provided response_format. It uses either:
40+
*
41+
* 1. A Zod schema provided with zodResponseFormat() to validate and parse the output
42+
* 2. The raw JSON schema response if json_schema is specified directly
43+
*
44+
* For tools, it will parse function arguments into structured objects if the tools
45+
* are created using zodFunction().
46+
*
47+
* ```ts
48+
* const completion = await client.chat.parse({
49+
* model: 'palmyra-x-004',
50+
* messages: [
51+
* { role: 'system', content: 'You are a helpful math tutor.' },
52+
* { role: 'user', content: 'solve 8x + 31 = 2' },
53+
* ],
54+
* response_format: zodResponseFormat(
55+
* z.object({
56+
* steps: z.array(z.object({
57+
* explanation: z.string(),
58+
* answer: z.string(),
59+
* })),
60+
* final_answer: z.string(),
61+
* }),
62+
* 'math_answer',
63+
* ),
64+
* });
65+
*
66+
* const message = completion.choices[0]?.message;
67+
* if (message?.parsed) {
68+
* console.log(message.parsed);
69+
* console.log(message.parsed.final_answer);
70+
* }
71+
* ```
72+
*/
73+
parse<Params extends ChatCompletionParseParams, ParsedT = ExtractParsedContentFromParams<Params>>(
74+
body: Params,
75+
options?: RequestOptions,
76+
): APIPromise<ParsedChatCompletion<ParsedT>> {
77+
if (body.tools) {
78+
validateInputTools(body.tools);
79+
}
80+
81+
return this.chat(body, options)._thenUnwrap((completion) =>
82+
maybeParseChatCompletion(completion, body),
83+
) as APIPromise<ParsedChatCompletion<ParsedT>>;
84+
}
85+
3486
/**
3587
* Creates a chat completion stream
3688
*/
@@ -644,7 +696,9 @@ export interface ParsedChatCompletion<ParsedT> extends ChatCompletion {
644696
choices: Array<ParsedChoice<ParsedT>>;
645697
}
646698

647-
export type ChatCompletionParseParams = ChatChatParamsNonStreaming;
699+
export interface ChatCompletionParseParams extends ChatChatParamsNonStreaming {
700+
response_format?: ResponseFormatJSONSchema;
701+
}
648702

649703
export declare namespace Chat {
650704
export {

tests/lib/ChatCompletionStream.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ describe('.stream()', () => {
3333
"logprobs": null,
3434
"message": {
3535
"content": "{"city":"San Francisco","units":"c"}",
36-
"parsed": null,
36+
"parsed": {
37+
"city": "San Francisco",
38+
"units": "c",
39+
},
3740
"refusal": null,
3841
"role": "assistant",
3942
"tool_calls": [],
@@ -189,7 +192,10 @@ describe('.stream()', () => {
189192
},
190193
"message": {
191194
"content": "{"city":"San Francisco","units":"f"}",
192-
"parsed": null,
195+
"parsed": {
196+
"city": "San Francisco",
197+
"units": "f",
198+
},
193199
"refusal": null,
194200
"role": "assistant",
195201
"tool_calls": [],

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2854,6 +2854,11 @@ prelude-ls@^1.2.1:
28542854
resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
28552855
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
28562856

2857+
"prettier-2@npm:prettier@^2":
2858+
version "2.8.8"
2859+
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
2860+
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
2861+
28572862
prettier-linter-helpers@^1.0.0:
28582863
version "1.0.0"
28592864
resolved "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz"

0 commit comments

Comments
 (0)