Skip to content

Commit c70280b

Browse files
author
Stainless Bot
committed
feat: feat(client): add ._request_id property to object responses (#1078)
1 parent 0223582 commit c70280b

File tree

4 files changed

+165
-17
lines changed

4 files changed

+165
-17
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,17 @@ Error codes are as followed:
361361
| >=500 | `InternalServerError` |
362362
| N/A | `APIConnectionError` |
363363

364+
## Request IDs
365+
366+
> For more information on debugging requests, see [these docs](https://platform.openai.com/docs/api-reference/debugging-requests)
367+
368+
All object responses in the SDK provide a `_request_id` property which is added from the `x-request-id` response header so that you can quickly log failing requests and report them back to OpenAI.
369+
370+
```ts
371+
const completion = await client.chat.completions.create({ messages: [{ role: 'user', content: 'Say this is a test' }], model: 'gpt-4' });
372+
console.log(completion._request_id) // req_123
373+
```
374+
364375
## Microsoft Azure OpenAI
365376

366377
To use this library with [Azure OpenAI](https://learn.microsoft.com/azure/ai-services/openai/overview), use the `AzureOpenAI`

src/core.ts

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ type APIResponseProps = {
3737
controller: AbortController;
3838
};
3939

40-
async function defaultParseResponse<T>(props: APIResponseProps): Promise<T> {
40+
async function defaultParseResponse<T>(props: APIResponseProps): Promise<WithRequestID<T>> {
4141
const { response } = props;
4242
if (props.options.stream) {
4343
debug('response', response.status, response.url, response.headers, response.body);
@@ -54,11 +54,11 @@ async function defaultParseResponse<T>(props: APIResponseProps): Promise<T> {
5454

5555
// fetch refuses to read the body when the status code is 204.
5656
if (response.status === 204) {
57-
return null as T;
57+
return null as WithRequestID<T>;
5858
}
5959

6060
if (props.options.__binaryResponse) {
61-
return response as unknown as T;
61+
return response as unknown as WithRequestID<T>;
6262
}
6363

6464
const contentType = response.headers.get('content-type');
@@ -69,26 +69,44 @@ async function defaultParseResponse<T>(props: APIResponseProps): Promise<T> {
6969

7070
debug('response', response.status, response.url, response.headers, json);
7171

72-
return json as T;
72+
return _addRequestID(json, response);
7373
}
7474

7575
const text = await response.text();
7676
debug('response', response.status, response.url, response.headers, text);
7777

7878
// TODO handle blob, arraybuffer, other content types, etc.
79-
return text as unknown as T;
79+
return text as unknown as WithRequestID<T>;
80+
}
81+
82+
type WithRequestID<T> =
83+
T extends Array<any> | Response | AbstractPage<any> ? T
84+
: T extends Record<string, any> ? T & { _request_id?: string | null }
85+
: T;
86+
87+
function _addRequestID<T>(value: T, response: Response): WithRequestID<T> {
88+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
89+
return value as WithRequestID<T>;
90+
}
91+
92+
return Object.defineProperty(value, '_request_id', {
93+
value: response.headers.get('x-request-id'),
94+
enumerable: false,
95+
}) as WithRequestID<T>;
8096
}
8197

8298
/**
8399
* A subclass of `Promise` providing additional helper methods
84100
* for interacting with the SDK.
85101
*/
86-
export class APIPromise<T> extends Promise<T> {
87-
private parsedPromise: Promise<T> | undefined;
102+
export class APIPromise<T> extends Promise<WithRequestID<T>> {
103+
private parsedPromise: Promise<WithRequestID<T>> | undefined;
88104

89105
constructor(
90106
private responsePromise: Promise<APIResponseProps>,
91-
private parseResponse: (props: APIResponseProps) => PromiseOrValue<T> = defaultParseResponse,
107+
private parseResponse: (
108+
props: APIResponseProps,
109+
) => PromiseOrValue<WithRequestID<T>> = defaultParseResponse,
92110
) {
93111
super((resolve) => {
94112
// this is maybe a bit weird but this has to be a no-op to not implicitly
@@ -99,7 +117,9 @@ export class APIPromise<T> extends Promise<T> {
99117
}
100118

101119
_thenUnwrap<U>(transform: (data: T) => U): APIPromise<U> {
102-
return new APIPromise(this.responsePromise, async (props) => transform(await this.parseResponse(props)));
120+
return new APIPromise(this.responsePromise, async (props) =>
121+
_addRequestID(transform(await this.parseResponse(props)), props.response),
122+
);
103123
}
104124

105125
/**
@@ -136,27 +156,27 @@ export class APIPromise<T> extends Promise<T> {
136156
return { data, response };
137157
}
138158

139-
private parse(): Promise<T> {
159+
private parse(): Promise<WithRequestID<T>> {
140160
if (!this.parsedPromise) {
141-
this.parsedPromise = this.responsePromise.then(this.parseResponse);
161+
this.parsedPromise = this.responsePromise.then(this.parseResponse) as any as Promise<WithRequestID<T>>;
142162
}
143163
return this.parsedPromise;
144164
}
145165

146-
override then<TResult1 = T, TResult2 = never>(
147-
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
166+
override then<TResult1 = WithRequestID<T>, TResult2 = never>(
167+
onfulfilled?: ((value: WithRequestID<T>) => TResult1 | PromiseLike<TResult1>) | undefined | null,
148168
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
149169
): Promise<TResult1 | TResult2> {
150170
return this.parse().then(onfulfilled, onrejected);
151171
}
152172

153173
override catch<TResult = never>(
154174
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null,
155-
): Promise<T | TResult> {
175+
): Promise<WithRequestID<T> | TResult> {
156176
return this.parse().catch(onrejected);
157177
}
158178

159-
override finally(onfinally?: (() => void) | undefined | null): Promise<T> {
179+
override finally(onfinally?: (() => void) | undefined | null): Promise<WithRequestID<T>> {
160180
return this.parse().finally(onfinally);
161181
}
162182
}
@@ -706,7 +726,13 @@ export class PagePromise<
706726
) {
707727
super(
708728
request,
709-
async (props) => new Page(client, props.response, await defaultParseResponse(props), props.options),
729+
async (props) =>
730+
new Page(
731+
client,
732+
props.response,
733+
await defaultParseResponse(props),
734+
props.options,
735+
) as WithRequestID<PageClass>,
710736
);
711737
}
712738

tests/responses.test.ts

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import { createResponseHeaders } from 'openai/core';
1+
import { APIPromise, createResponseHeaders } from 'openai/core';
2+
import OpenAI from 'openai/index';
23
import { Headers } from 'openai/_shims/index';
4+
import { Response } from 'node-fetch';
5+
import { compareType } from './utils/typing';
36

47
describe('response parsing', () => {
58
// TODO: test unicode characters
@@ -23,3 +26,102 @@ describe('response parsing', () => {
2326
expect(headers['content-type']).toBe('text/xml, application/json');
2427
});
2528
});
29+
30+
describe('request id', () => {
31+
test('types', () => {
32+
compareType<Awaited<APIPromise<string>>, string>(true);
33+
compareType<Awaited<APIPromise<number>>, number>(true);
34+
compareType<Awaited<APIPromise<null>>, null>(true);
35+
compareType<Awaited<APIPromise<void>>, void>(true);
36+
compareType<Awaited<APIPromise<Response>>, Response>(true);
37+
compareType<Awaited<APIPromise<Response>>, Response>(true);
38+
compareType<Awaited<APIPromise<{ foo: string }>>, { foo: string } & { _request_id?: string | null }>(
39+
true,
40+
);
41+
compareType<Awaited<APIPromise<Array<{ foo: string }>>>, Array<{ foo: string }>>(true);
42+
});
43+
44+
test('object response', async () => {
45+
const client = new OpenAI({
46+
apiKey: 'dummy',
47+
fetch: async () =>
48+
new Response(JSON.stringify({ id: 'bar' }), {
49+
headers: { 'x-request-id': 'req_id_xxx', 'content-type': 'application/json' },
50+
}),
51+
});
52+
53+
const rsp = await client.chat.completions.create({ messages: [], model: 'gpt-4' });
54+
expect(rsp.id).toBe('bar');
55+
expect(rsp._request_id).toBe('req_id_xxx');
56+
expect(JSON.stringify(rsp)).toBe('{"id":"bar"}');
57+
});
58+
59+
test('envelope response', async () => {
60+
const promise = new APIPromise<{ data: { foo: string } }>(
61+
(async () => {
62+
return {
63+
response: new Response(JSON.stringify({ data: { foo: 'bar' } }), {
64+
headers: { 'x-request-id': 'req_id_xxx', 'content-type': 'application/json' },
65+
}),
66+
controller: {} as any,
67+
options: {} as any,
68+
};
69+
})(),
70+
)._thenUnwrap((d) => d.data);
71+
72+
const rsp = await promise;
73+
expect(rsp.foo).toBe('bar');
74+
expect(rsp._request_id).toBe('req_id_xxx');
75+
});
76+
77+
test('page response', async () => {
78+
const client = new OpenAI({
79+
apiKey: 'dummy',
80+
fetch: async () =>
81+
new Response(JSON.stringify({ data: [{ foo: 'bar' }] }), {
82+
headers: { 'x-request-id': 'req_id_xxx', 'content-type': 'application/json' },
83+
}),
84+
});
85+
86+
const page = await client.fineTuning.jobs.list();
87+
expect(page.data).toMatchObject([{ foo: 'bar' }]);
88+
expect((page as any)._request_id).toBeUndefined();
89+
});
90+
91+
test('array response', async () => {
92+
const promise = new APIPromise<Array<{ foo: string }>>(
93+
(async () => {
94+
return {
95+
response: new Response(JSON.stringify([{ foo: 'bar' }]), {
96+
headers: { 'x-request-id': 'req_id_xxx', 'content-type': 'application/json' },
97+
}),
98+
controller: {} as any,
99+
options: {} as any,
100+
};
101+
})(),
102+
);
103+
104+
const rsp = await promise;
105+
expect(rsp.length).toBe(1);
106+
expect(rsp[0]).toMatchObject({ foo: 'bar' });
107+
expect((rsp as any)._request_id).toBeUndefined();
108+
});
109+
110+
test('string response', async () => {
111+
const promise = new APIPromise<string>(
112+
(async () => {
113+
return {
114+
response: new Response('hello world', {
115+
headers: { 'x-request-id': 'req_id_xxx', 'content-type': 'application/text' },
116+
}),
117+
controller: {} as any,
118+
options: {} as any,
119+
};
120+
})(),
121+
);
122+
123+
const result = await promise;
124+
expect(result).toBe('hello world');
125+
expect((result as any)._request_id).toBeUndefined();
126+
});
127+
});

tests/utils/typing.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false;
2+
3+
export const expectType = <T>(_expression: T): void => {
4+
return;
5+
};
6+
7+
export const compareType = <T1, T2>(_expression: Equal<T1, T2>): void => {
8+
return;
9+
};

0 commit comments

Comments
 (0)