@anthropic-ai/sdk
Version:
The official TypeScript library for the Anthropic API
91 lines (78 loc) • 3.3 kB
text/typescript
import type { FinalRequestOptions } from './request-options';
import { Stream } from '../core/streaming';
import { type BaseAnthropic } from '../client';
import { debugLogRequestDetails, loggerFor } from './utils/log';
import { releaseRequestSignal } from './request-signal';
import type { AbstractPage } from '../core/pagination';
export type APIResponseProps = {
response: Response;
options: FinalRequestOptions;
controller: AbortController;
requestLogID: string;
retryOfRequestLogID: string | undefined;
startTime: number;
};
export async function defaultParseResponse<T>(
client: BaseAnthropic,
props: APIResponseProps,
): Promise<WithRequestID<T>> {
const { response, requestLogID, retryOfRequestLogID, startTime } = props;
const body = await (async () => {
if (props.options.stream) {
loggerFor(client).debug('response', response.status, response.url, response.headers, response.body);
// Note: there is an invariant here that isn't represented in the type system
// that if you set `stream: true` the response type must also be `Stream<T>`
return Stream.fromSSEResponse(response, props.controller, client) as any;
}
// fetch refuses to read the body when the status code is 204.
if (response.status === 204) {
return null as T;
}
if (props.options.__binaryResponse) {
return response as unknown as T;
}
const contentType = response.headers.get('content-type');
const mediaType = contentType?.split(';')[0]?.trim();
const isJSON = mediaType?.includes('application/json') || mediaType?.endsWith('+json');
if (isJSON) {
const contentLength = response.headers.get('content-length');
if (contentLength === '0') {
// if there is no content we can't do anything
return undefined as T;
}
const json = await response.json();
return addResponseIDs(json as T, response);
}
const text = await response.text();
return text as unknown as T;
})().finally(() => {
// The body is settled (or parsing threw), so the caller-signal abort
// listener has nothing left to cancel. Streams release in their own
// teardown; a raw Response (`__binaryResponse`) keeps the listener so
// aborting an in-flight download still works.
if (!props.options.stream && !props.options.__binaryResponse) {
releaseRequestSignal(props.controller);
}
});
debugLogRequestDetails(loggerFor(client), `[${requestLogID}] response parsed`, {
retryOfRequestLogID,
url: response.url,
status: response.status,
body,
durationMs: Date.now() - startTime,
});
return body;
}
export type WithRequestID<T> =
T extends Array<any> | Response | AbstractPage<any> ? T
: T extends Record<string, any> ? T & { _request_id?: string | null; _workspace_id?: string | null }
: T;
export function addResponseIDs<T>(value: T, response: Response): WithRequestID<T> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return value as WithRequestID<T>;
}
return Object.defineProperties(value, {
_request_id: { value: response.headers.get('request-id'), enumerable: false },
_workspace_id: { value: response.headers.get('anthropic-workspace-id'), enumerable: false },
}) as WithRequestID<T>;
}