Spaces:
Build error
Build error
File size: 35,075 Bytes
c211499 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 |
import { VERSION } from './version';
import { Stream } from './streaming';
import {
AnthropicError,
APIError,
APIConnectionError,
APIConnectionTimeoutError,
APIUserAbortError,
} from './error';
import {
kind as shimsKind,
type Readable,
getDefaultAgent,
type Agent,
fetch,
type RequestInfo,
type RequestInit,
type Response,
type HeadersInit,
} from './_shims/index';
export { type Response };
import { isMultipartBody } from './uploads';
export {
maybeMultipartFormRequestOptions,
multipartFormRequestOptions,
createForm,
type Uploadable,
} from './uploads';
const MAX_RETRIES = 2;
export type Fetch = (url: RequestInfo, init?: RequestInit) => Promise<Response>;
type PromiseOrValue<T> = T | Promise<T>;
type APIResponseProps = {
response: Response;
options: FinalRequestOptions;
controller: AbortController;
};
async function defaultParseResponse<T>(props: APIResponseProps): Promise<T> {
const { response } = props;
if (props.options.stream) {
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) as any;
}
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
const json = await response.json();
debug('response', response.status, response.url, response.headers, json);
return json as T;
}
// TODO handle blob, arraybuffer, other content types, etc.
const text = await response.text();
debug('response', response.status, response.url, response.headers, text);
return text as any as T;
}
/**
* A subclass of `Promise` providing additional helper methods
* for interacting with the SDK.
*/
export class APIPromise<T> extends Promise<T> {
private parsedPromise: Promise<T> | undefined;
constructor(
private responsePromise: Promise<APIResponseProps>,
private parseResponse: (props: APIResponseProps) => PromiseOrValue<T> = defaultParseResponse,
) {
super((resolve) => {
// this is maybe a bit weird but this has to be a no-op to not implicitly
// parse the response body; instead .then, .catch, .finally are overridden
// to parse the response
resolve(null as any);
});
}
_thenUnwrap<U>(transform: (data: T) => U): APIPromise<U> {
return new APIPromise(this.responsePromise, async (props) => transform(await this.parseResponse(props)));
}
/**
* Gets the raw `Response` instance instead of parsing the response
* data.
*
* If you want to parse the response body but still get the `Response`
* instance, you can use {@link withResponse()}.
*
* 👋 Getting the wrong TypeScript type for `Response`?
* Try setting `"moduleResolution": "NodeNext"` if you can,
* or add one of these imports before your first `import … from '@anthropic-ai/sdk'`:
* - `import '@anthropic-ai/sdk/shims/node'` (if you're running on Node)
* - `import '@anthropic-ai/sdk/shims/web'` (otherwise)
*/
asResponse(): Promise<Response> {
return this.responsePromise.then((p) => p.response);
}
/**
* Gets the parsed response data and the raw `Response` instance.
*
* If you just want to get the raw `Response` instance without parsing it,
* you can use {@link asResponse()}.
*
*
* 👋 Getting the wrong TypeScript type for `Response`?
* Try setting `"moduleResolution": "NodeNext"` if you can,
* or add one of these imports before your first `import … from '@anthropic-ai/sdk'`:
* - `import '@anthropic-ai/sdk/shims/node'` (if you're running on Node)
* - `import '@anthropic-ai/sdk/shims/web'` (otherwise)
*/
async withResponse(): Promise<{ data: T; response: Response }> {
const [data, response] = await Promise.all([this.parse(), this.asResponse()]);
return { data, response };
}
private parse(): Promise<T> {
if (!this.parsedPromise) {
this.parsedPromise = this.responsePromise.then(this.parseResponse);
}
return this.parsedPromise;
}
override then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
): Promise<TResult1 | TResult2> {
return this.parse().then(onfulfilled, onrejected);
}
override catch<TResult = never>(
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null,
): Promise<T | TResult> {
return this.parse().catch(onrejected);
}
override finally(onfinally?: (() => void) | undefined | null): Promise<T> {
return this.parse().finally(onfinally);
}
}
export abstract class APIClient {
baseURL: string;
maxRetries: number;
timeout: number;
httpAgent: Agent | undefined;
private fetch: Fetch;
protected idempotencyHeader?: string;
constructor({
baseURL,
maxRetries,
timeout = 600000, // 10 minutes
httpAgent,
fetch: overridenFetch,
}: {
baseURL: string;
maxRetries?: number | undefined;
timeout: number | undefined;
httpAgent: Agent | undefined;
fetch: Fetch | undefined;
}) {
this.baseURL = baseURL;
this.maxRetries = validatePositiveInteger('maxRetries', maxRetries ?? MAX_RETRIES);
this.timeout = validatePositiveInteger('timeout', timeout);
this.httpAgent = httpAgent;
this.fetch = overridenFetch ?? fetch;
}
protected authHeaders(opts: FinalRequestOptions): Headers {
return {};
}
/**
* Override this to add your own default headers, for example:
*
* {
* ...super.defaultHeaders(),
* Authorization: 'Bearer 123',
* }
*/
protected defaultHeaders(opts: FinalRequestOptions): Headers {
return {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': this.getUserAgent(),
...getPlatformHeaders(),
...this.authHeaders(opts),
};
}
protected abstract defaultQuery(): DefaultQuery | undefined;
/**
* Override this to add your own headers validation:
*/
protected validateHeaders(headers: Headers, customHeaders: Headers) {}
protected defaultIdempotencyKey(): string {
return `stainless-node-retry-${uuid4()}`;
}
get<Req extends {}, Rsp>(path: string, opts?: PromiseOrValue<RequestOptions<Req>>): APIPromise<Rsp> {
return this.methodRequest('get', path, opts);
}
post<Req extends {}, Rsp>(path: string, opts?: PromiseOrValue<RequestOptions<Req>>): APIPromise<Rsp> {
return this.methodRequest('post', path, opts);
}
patch<Req extends {}, Rsp>(path: string, opts?: PromiseOrValue<RequestOptions<Req>>): APIPromise<Rsp> {
return this.methodRequest('patch', path, opts);
}
put<Req extends {}, Rsp>(path: string, opts?: PromiseOrValue<RequestOptions<Req>>): APIPromise<Rsp> {
return this.methodRequest('put', path, opts);
}
delete<Req extends {}, Rsp>(path: string, opts?: PromiseOrValue<RequestOptions<Req>>): APIPromise<Rsp> {
return this.methodRequest('delete', path, opts);
}
private methodRequest<Req extends {}, Rsp>(
method: HTTPMethod,
path: string,
opts?: PromiseOrValue<RequestOptions<Req>>,
): APIPromise<Rsp> {
return this.request(Promise.resolve(opts).then((opts) => ({ method, path, ...opts })));
}
getAPIList<Item, PageClass extends AbstractPage<Item> = AbstractPage<Item>>(
path: string,
Page: new (...args: any[]) => PageClass,
opts?: RequestOptions<any>,
): PagePromise<PageClass, Item> {
return this.requestAPIList(Page, { method: 'get', path, ...opts });
}
private calculateContentLength(body: unknown): string | null {
if (typeof body === 'string') {
if (typeof Buffer !== 'undefined') {
return Buffer.byteLength(body, 'utf8').toString();
}
if (typeof TextEncoder !== 'undefined') {
const encoder = new TextEncoder();
const encoded = encoder.encode(body);
return encoded.length.toString();
}
}
return null;
}
buildRequest<Req extends {}>(
options: FinalRequestOptions<Req>,
): { req: RequestInit; url: string; timeout: number } {
const { method, path, query, headers: headers = {} } = options;
const body =
isMultipartBody(options.body) ? options.body.body
: options.body ? JSON.stringify(options.body, null, 2)
: null;
const contentLength = this.calculateContentLength(body);
const url = this.buildURL(path!, query);
if ('timeout' in options) validatePositiveInteger('timeout', options.timeout);
const timeout = options.timeout ?? this.timeout;
const httpAgent = options.httpAgent ?? this.httpAgent ?? getDefaultAgent(url);
const minAgentTimeout = timeout + 1000;
if (
typeof (httpAgent as any)?.options?.timeout === 'number' &&
minAgentTimeout > ((httpAgent as any).options.timeout ?? 0)
) {
// Allow any given request to bump our agent active socket timeout.
// This may seem strange, but leaking active sockets should be rare and not particularly problematic,
// and without mutating agent we would need to create more of them.
// This tradeoff optimizes for performance.
(httpAgent as any).options.timeout = minAgentTimeout;
}
if (this.idempotencyHeader && method !== 'get') {
if (!options.idempotencyKey) options.idempotencyKey = this.defaultIdempotencyKey();
headers[this.idempotencyHeader] = options.idempotencyKey;
}
const reqHeaders: Record<string, string> = {
...(contentLength && { 'Content-Length': contentLength }),
...this.defaultHeaders(options),
...headers,
};
// let builtin fetch set the Content-Type for multipart bodies
if (isMultipartBody(options.body) && shimsKind !== 'node') {
delete reqHeaders['Content-Type'];
}
// Strip any headers being explicitly omitted with null
Object.keys(reqHeaders).forEach((key) => reqHeaders[key] === null && delete reqHeaders[key]);
const req: RequestInit = {
method,
...(body && { body: body as any }),
headers: reqHeaders,
...(httpAgent && { agent: httpAgent }),
// @ts-ignore node-fetch uses a custom AbortSignal type that is
// not compatible with standard web types
signal: options.signal ?? null,
};
this.validateHeaders(reqHeaders, headers);
return { req, url, timeout };
}
/**
* Used as a callback for mutating the given `RequestInit` object.
*
* This is useful for cases where you want to add certain headers based off of
* the request properties, e.g. `method` or `url`.
*/
protected async prepareRequest(
request: RequestInit,
{ url, options }: { url: string; options: FinalRequestOptions },
): Promise<void> {}
protected parseHeaders(headers: HeadersInit | null | undefined): Record<string, string> {
return (
!headers ? {}
: Symbol.iterator in headers ?
Object.fromEntries(Array.from(headers as Iterable<string[]>).map((header) => [...header]))
: { ...headers }
);
}
protected makeStatusError(
status: number | undefined,
error: Object | undefined,
message: string | undefined,
headers: Headers | undefined,
) {
return APIError.generate(status, error, message, headers);
}
request<Req extends {}, Rsp>(
options: PromiseOrValue<FinalRequestOptions<Req>>,
remainingRetries: number | null = null,
): APIPromise<Rsp> {
return new APIPromise(this.makeRequest(options, remainingRetries));
}
private async makeRequest(
optionsInput: PromiseOrValue<FinalRequestOptions>,
retriesRemaining: number | null,
): Promise<APIResponseProps> {
const options = await optionsInput;
if (retriesRemaining == null) {
retriesRemaining = options.maxRetries ?? this.maxRetries;
}
const { req, url, timeout } = this.buildRequest(options);
await this.prepareRequest(req, { url, options });
debug('request', url, options, req.headers);
if (options.signal?.aborted) {
throw new APIUserAbortError();
}
const controller = new AbortController();
const response = await this.fetchWithTimeout(url, req, timeout, controller).catch(castToError);
if (response instanceof Error) {
if (options.signal?.aborted) {
throw new APIUserAbortError();
}
if (retriesRemaining) {
return this.retryRequest(options, retriesRemaining);
}
if (response.name === 'AbortError') {
throw new APIConnectionTimeoutError();
}
throw new APIConnectionError({ cause: response });
}
const responseHeaders = createResponseHeaders(response.headers);
if (!response.ok) {
if (retriesRemaining && this.shouldRetry(response)) {
return this.retryRequest(options, retriesRemaining, responseHeaders);
}
const errText = await response.text().catch((e) => castToError(e).message);
const errJSON = safeJSON(errText);
const errMessage = errJSON ? undefined : errText;
debug('response', response.status, url, responseHeaders, errMessage);
const err = this.makeStatusError(response.status, errJSON, errMessage, responseHeaders);
throw err;
}
return { response, options, controller };
}
requestAPIList<Item = unknown, PageClass extends AbstractPage<Item> = AbstractPage<Item>>(
Page: new (...args: ConstructorParameters<typeof AbstractPage>) => PageClass,
options: FinalRequestOptions,
): PagePromise<PageClass, Item> {
const request = this.makeRequest(options, null);
return new PagePromise<PageClass, Item>(this, request, Page);
}
buildURL<Req extends Record<string, unknown>>(path: string, query: Req | null | undefined): string {
const url =
isAbsoluteURL(path) ?
new URL(path)
: new URL(this.baseURL + (this.baseURL.endsWith('/') && path.startsWith('/') ? path.slice(1) : path));
const defaultQuery = this.defaultQuery();
if (!isEmptyObj(defaultQuery)) {
query = { ...defaultQuery, ...query } as Req;
}
if (query) {
url.search = this.stringifyQuery(query);
}
return url.toString();
}
protected stringifyQuery(query: Record<string, unknown>): string {
return Object.entries(query)
.filter(([_, value]) => typeof value !== 'undefined')
.map(([key, value]) => {
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
}
if (value === null) {
return `${encodeURIComponent(key)}=`;
}
throw new AnthropicError(
`Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`,
);
})
.join('&');
}
async fetchWithTimeout(
url: RequestInfo,
init: RequestInit | undefined,
ms: number,
controller: AbortController,
): Promise<Response> {
const { signal, ...options } = init || {};
if (signal) signal.addEventListener('abort', () => controller.abort());
const timeout = setTimeout(() => controller.abort(), ms);
return (
this.getRequestClient()
// use undefined this binding; fetch errors if bound to something else in browser/cloudflare
.fetch.call(undefined, url, { signal: controller.signal as any, ...options })
.finally(() => {
clearTimeout(timeout);
})
);
}
protected getRequestClient(): RequestClient {
return { fetch: this.fetch };
}
private shouldRetry(response: Response): boolean {
// Note this is not a standard header.
const shouldRetryHeader = response.headers.get('x-should-retry');
// If the server explicitly says whether or not to retry, obey.
if (shouldRetryHeader === 'true') return true;
if (shouldRetryHeader === 'false') return false;
// Retry on request timeouts.
if (response.status === 408) return true;
// Retry on lock timeouts.
if (response.status === 409) return true;
// Retry on rate limits.
if (response.status === 429) return true;
// Retry internal errors.
if (response.status >= 500) return true;
return false;
}
private async retryRequest(
options: FinalRequestOptions,
retriesRemaining: number,
responseHeaders?: Headers | undefined,
): Promise<APIResponseProps> {
retriesRemaining -= 1;
// About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
let timeoutMillis: number | undefined;
const retryAfterHeader = responseHeaders?.['retry-after'];
if (retryAfterHeader) {
const timeoutSeconds = parseInt(retryAfterHeader);
if (!Number.isNaN(timeoutSeconds)) {
timeoutMillis = timeoutSeconds * 1000;
} else {
timeoutMillis = Date.parse(retryAfterHeader) - Date.now();
}
}
// If the API asks us to wait a certain amount of time (and it's a reasonable amount),
// just do what it says, but otherwise calculate a default
if (
!timeoutMillis ||
!Number.isInteger(timeoutMillis) ||
timeoutMillis <= 0 ||
timeoutMillis > 60 * 1000
) {
const maxRetries = options.maxRetries ?? this.maxRetries;
timeoutMillis = this.calculateDefaultRetryTimeoutMillis(retriesRemaining, maxRetries);
}
await sleep(timeoutMillis);
return this.makeRequest(options, retriesRemaining);
}
private calculateDefaultRetryTimeoutMillis(retriesRemaining: number, maxRetries: number): number {
const initialRetryDelay = 0.5;
const maxRetryDelay = 2;
const numRetries = maxRetries - retriesRemaining;
// Apply exponential backoff, but not more than the max.
const sleepSeconds = Math.min(initialRetryDelay * Math.pow(numRetries - 1, 2), maxRetryDelay);
// Apply some jitter, plus-or-minus half a second.
const jitter = Math.random() - 0.5;
return (sleepSeconds + jitter) * 1000;
}
private getUserAgent(): string {
return `${this.constructor.name}/JS ${VERSION}`;
}
}
export class APIResource {
protected client: APIClient;
constructor(client: APIClient) {
this.client = client;
this.get = client.get.bind(client);
this.post = client.post.bind(client);
this.patch = client.patch.bind(client);
this.put = client.put.bind(client);
this.delete = client.delete.bind(client);
this.getAPIList = client.getAPIList.bind(client);
}
protected get: APIClient['get'];
protected post: APIClient['post'];
protected patch: APIClient['patch'];
protected put: APIClient['put'];
protected delete: APIClient['delete'];
protected getAPIList: APIClient['getAPIList'];
}
export type PageInfo = { url: URL } | { params: Record<string, unknown> | null };
export abstract class AbstractPage<Item> implements AsyncIterable<Item> {
#client: APIClient;
protected options: FinalRequestOptions;
protected response: Response;
protected body: unknown;
constructor(client: APIClient, response: Response, body: unknown, options: FinalRequestOptions) {
this.#client = client;
this.options = options;
this.response = response;
this.body = body;
}
/**
* @deprecated Use nextPageInfo instead
*/
abstract nextPageParams(): Partial<Record<string, unknown>> | null;
abstract nextPageInfo(): PageInfo | null;
abstract getPaginatedItems(): Item[];
hasNextPage(): boolean {
const items = this.getPaginatedItems();
if (!items.length) return false;
return this.nextPageInfo() != null;
}
async getNextPage(): Promise<this> {
const nextInfo = this.nextPageInfo();
if (!nextInfo) {
throw new AnthropicError(
'No next page expected; please check `.hasNextPage()` before calling `.getNextPage()`.',
);
}
const nextOptions = { ...this.options };
if ('params' in nextInfo) {
nextOptions.query = { ...nextOptions.query, ...nextInfo.params };
} else if ('url' in nextInfo) {
const params = [...Object.entries(nextOptions.query || {}), ...nextInfo.url.searchParams.entries()];
for (const [key, value] of params) {
nextInfo.url.searchParams.set(key, value as any);
}
nextOptions.query = undefined;
nextOptions.path = nextInfo.url.toString();
}
return await this.#client.requestAPIList(this.constructor as any, nextOptions);
}
async *iterPages() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let page: AbstractPage<Item> = this;
yield page;
while (page.hasNextPage()) {
page = await page.getNextPage();
yield page;
}
}
async *[Symbol.asyncIterator]() {
for await (const page of this.iterPages()) {
for (const item of page.getPaginatedItems()) {
yield item;
}
}
}
}
/**
* This subclass of Promise will resolve to an instantiated Page once the request completes.
*
* It also implements AsyncIterable to allow auto-paginating iteration on an unawaited list call, eg:
*
* for await (const item of client.items.list()) {
* console.log(item)
* }
*/
export class PagePromise<
PageClass extends AbstractPage<Item>,
Item = ReturnType<PageClass['getPaginatedItems']>[number],
>
extends APIPromise<PageClass>
implements AsyncIterable<Item>
{
constructor(
client: APIClient,
request: Promise<APIResponseProps>,
Page: new (...args: ConstructorParameters<typeof AbstractPage>) => PageClass,
) {
super(
request,
async (props) => new Page(client, props.response, await defaultParseResponse(props), props.options),
);
}
/**
* Allow auto-paginating iteration on an unawaited list call, eg:
*
* for await (const item of client.items.list()) {
* console.log(item)
* }
*/
async *[Symbol.asyncIterator]() {
const page = await this;
for await (const item of page) {
yield item;
}
}
}
export const createResponseHeaders = (
headers: Awaited<ReturnType<Fetch>>['headers'],
): Record<string, string> => {
return new Proxy(
Object.fromEntries(
// @ts-ignore
headers.entries(),
),
{
get(target, name) {
const key = name.toString();
return target[key.toLowerCase()] || target[key];
},
},
);
};
type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
export type RequestClient = { fetch: Fetch };
export type Headers = Record<string, string | null | undefined>;
export type DefaultQuery = Record<string, string | undefined>;
export type KeysEnum<T> = { [P in keyof Required<T>]: true };
export type RequestOptions<Req extends {} = Record<string, unknown> | Readable> = {
method?: HTTPMethod;
path?: string;
query?: Req | undefined;
body?: Req | undefined;
headers?: Headers | undefined;
maxRetries?: number;
stream?: boolean | undefined;
timeout?: number;
httpAgent?: Agent;
signal?: AbortSignal | undefined | null;
idempotencyKey?: string;
};
// This is required so that we can determine if a given object matches the RequestOptions
// type at runtime. While this requires duplication, it is enforced by the TypeScript
// compiler such that any missing / extraneous keys will cause an error.
const requestOptionsKeys: KeysEnum<RequestOptions> = {
method: true,
path: true,
query: true,
body: true,
headers: true,
maxRetries: true,
stream: true,
timeout: true,
httpAgent: true,
signal: true,
idempotencyKey: true,
};
export const isRequestOptions = (obj: unknown): obj is RequestOptions<Record<string, unknown> | Readable> => {
return (
typeof obj === 'object' &&
obj !== null &&
!isEmptyObj(obj) &&
Object.keys(obj).every((k) => hasOwn(requestOptionsKeys, k))
);
};
export type FinalRequestOptions<Req extends {} = Record<string, unknown> | Readable> = RequestOptions<Req> & {
method: HTTPMethod;
path: string;
};
declare const Deno: any;
declare const EdgeRuntime: any;
type Arch = 'x32' | 'x64' | 'arm' | 'arm64' | `other:${string}` | 'unknown';
type PlatformName =
| 'MacOS'
| 'Linux'
| 'Windows'
| 'FreeBSD'
| 'OpenBSD'
| 'iOS'
| 'Android'
| `Other:${string}`
| 'Unknown';
type Browser = 'ie' | 'edge' | 'chrome' | 'firefox' | 'safari';
type PlatformProperties = {
'X-Stainless-Lang': 'js';
'X-Stainless-Package-Version': string;
'X-Stainless-OS': PlatformName;
'X-Stainless-Arch': Arch;
'X-Stainless-Runtime': 'node' | 'deno' | 'edge' | `browser:${Browser}` | 'unknown';
'X-Stainless-Runtime-Version': string;
};
const getPlatformProperties = (): PlatformProperties => {
if (typeof Deno !== 'undefined' && Deno.build != null) {
return {
'X-Stainless-Lang': 'js',
'X-Stainless-Package-Version': VERSION,
'X-Stainless-OS': normalizePlatform(Deno.build.os),
'X-Stainless-Arch': normalizeArch(Deno.build.arch),
'X-Stainless-Runtime': 'deno',
'X-Stainless-Runtime-Version': Deno.version,
};
}
if (typeof EdgeRuntime !== 'undefined') {
return {
'X-Stainless-Lang': 'js',
'X-Stainless-Package-Version': VERSION,
'X-Stainless-OS': 'Unknown',
'X-Stainless-Arch': `other:${EdgeRuntime}`,
'X-Stainless-Runtime': 'edge',
'X-Stainless-Runtime-Version': process.version,
};
}
// Check if Node.js
if (Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]') {
return {
'X-Stainless-Lang': 'js',
'X-Stainless-Package-Version': VERSION,
'X-Stainless-OS': normalizePlatform(process.platform),
'X-Stainless-Arch': normalizeArch(process.arch),
'X-Stainless-Runtime': 'node',
'X-Stainless-Runtime-Version': process.version,
};
}
const browserInfo = getBrowserInfo();
if (browserInfo) {
return {
'X-Stainless-Lang': 'js',
'X-Stainless-Package-Version': VERSION,
'X-Stainless-OS': 'Unknown',
'X-Stainless-Arch': 'unknown',
'X-Stainless-Runtime': `browser:${browserInfo.browser}`,
'X-Stainless-Runtime-Version': browserInfo.version,
};
}
// TODO add support for Cloudflare workers, etc.
return {
'X-Stainless-Lang': 'js',
'X-Stainless-Package-Version': VERSION,
'X-Stainless-OS': 'Unknown',
'X-Stainless-Arch': 'unknown',
'X-Stainless-Runtime': 'unknown',
'X-Stainless-Runtime-Version': 'unknown',
};
};
type BrowserInfo = {
browser: Browser;
version: string;
};
declare const navigator: { userAgent: string } | undefined;
// Note: modified from https://github.com/JS-DevTools/host-environment/blob/b1ab79ecde37db5d6e163c050e54fe7d287d7c92/src/isomorphic.browser.ts
function getBrowserInfo(): BrowserInfo | null {
if (typeof navigator === 'undefined' || !navigator) {
return null;
}
// NOTE: The order matters here!
const browserPatterns = [
{ key: 'edge' as const, pattern: /Edge(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
{ key: 'ie' as const, pattern: /MSIE(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
{ key: 'ie' as const, pattern: /Trident(?:.*rv\:(\d+)\.(\d+)(?:\.(\d+))?)?/ },
{ key: 'chrome' as const, pattern: /Chrome(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
{ key: 'firefox' as const, pattern: /Firefox(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
{ key: 'safari' as const, pattern: /(?:Version\W+(\d+)\.(\d+)(?:\.(\d+))?)?(?:\W+Mobile\S*)?\W+Safari/ },
];
// Find the FIRST matching browser
for (const { key, pattern } of browserPatterns) {
const match = pattern.exec(navigator.userAgent);
if (match) {
const major = match[1] || 0;
const minor = match[2] || 0;
const patch = match[3] || 0;
return { browser: key, version: `${major}.${minor}.${patch}` };
}
}
return null;
}
const normalizeArch = (arch: string): Arch => {
// Node docs:
// - https://nodejs.org/api/process.html#processarch
// Deno docs:
// - https://doc.deno.land/deno/stable/~/Deno.build
if (arch === 'x32') return 'x32';
if (arch === 'x86_64' || arch === 'x64') return 'x64';
if (arch === 'arm') return 'arm';
if (arch === 'aarch64' || arch === 'arm64') return 'arm64';
if (arch) return `other:${arch}`;
return 'unknown';
};
const normalizePlatform = (platform: string): PlatformName => {
// Node platforms:
// - https://nodejs.org/api/process.html#processplatform
// Deno platforms:
// - https://doc.deno.land/deno/stable/~/Deno.build
// - https://github.com/denoland/deno/issues/14799
platform = platform.toLowerCase();
// NOTE: this iOS check is untested and may not work
// Node does not work natively on IOS, there is a fork at
// https://github.com/nodejs-mobile/nodejs-mobile
// however it is unknown at the time of writing how to detect if it is running
if (platform.includes('ios')) return 'iOS';
if (platform === 'android') return 'Android';
if (platform === 'darwin') return 'MacOS';
if (platform === 'win32') return 'Windows';
if (platform === 'freebsd') return 'FreeBSD';
if (platform === 'openbsd') return 'OpenBSD';
if (platform === 'linux') return 'Linux';
if (platform) return `Other:${platform}`;
return 'Unknown';
};
let _platformHeaders: PlatformProperties;
const getPlatformHeaders = () => {
return (_platformHeaders ??= getPlatformProperties());
};
export const safeJSON = (text: string) => {
try {
return JSON.parse(text);
} catch (err) {
return undefined;
}
};
// https://stackoverflow.com/a/19709846
const startsWithSchemeRegexp = new RegExp('^(?:[a-z]+:)?//', 'i');
const isAbsoluteURL = (url: string): boolean => {
return startsWithSchemeRegexp.test(url);
};
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const validatePositiveInteger = (name: string, n: unknown): number => {
if (typeof n !== 'number' || !Number.isInteger(n)) {
throw new AnthropicError(`${name} must be an integer`);
}
if (n < 0) {
throw new AnthropicError(`${name} must be a positive integer`);
}
return n;
};
export const castToError = (err: any): Error => {
if (err instanceof Error) return err;
return new Error(err);
};
export const ensurePresent = <T>(value: T | null | undefined): T => {
if (value == null) throw new AnthropicError(`Expected a value to be given but received ${value} instead.`);
return value;
};
/**
* Read an environment variable.
*
* Will return undefined if the environment variable doesn't exist or cannot be accessed.
*/
export const readEnv = (env: string): string | undefined => {
if (typeof process !== 'undefined') {
return process.env?.[env] ?? undefined;
}
if (typeof Deno !== 'undefined') {
return Deno.env?.get?.(env);
}
return undefined;
};
export const coerceInteger = (value: unknown): number => {
if (typeof value === 'number') return Math.round(value);
if (typeof value === 'string') return parseInt(value, 10);
throw new AnthropicError(`Could not coerce ${value} (type: ${typeof value}) into a number`);
};
export const coerceFloat = (value: unknown): number => {
if (typeof value === 'number') return value;
if (typeof value === 'string') return parseFloat(value);
throw new AnthropicError(`Could not coerce ${value} (type: ${typeof value}) into a number`);
};
export const coerceBoolean = (value: unknown): boolean => {
if (typeof value === 'boolean') return value;
if (typeof value === 'string') return value === 'true';
return Boolean(value);
};
export const maybeCoerceInteger = (value: unknown): number | undefined => {
if (value === undefined) {
return undefined;
}
return coerceInteger(value);
};
export const maybeCoerceFloat = (value: unknown): number | undefined => {
if (value === undefined) {
return undefined;
}
return coerceFloat(value);
};
export const maybeCoerceBoolean = (value: unknown): boolean | undefined => {
if (value === undefined) {
return undefined;
}
return coerceBoolean(value);
};
// https://stackoverflow.com/a/34491287
export function isEmptyObj(obj: Object | null | undefined): boolean {
if (!obj) return true;
for (const _k in obj) return false;
return true;
}
// https://eslint.org/docs/latest/rules/no-prototype-builtins
export function hasOwn(obj: Object, key: string): boolean {
return Object.prototype.hasOwnProperty.call(obj, key);
}
export function debug(action: string, ...args: any[]) {
if (typeof process !== 'undefined' && process.env['DEBUG'] === 'true') {
console.log(`Anthropic:DEBUG:${action}`, ...args);
}
}
/**
* https://stackoverflow.com/a/2117523
*/
const uuid4 = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
};
export const isRunningInBrowser = () => {
return (
// @ts-ignore
typeof window !== 'undefined' &&
// @ts-ignore
typeof window.document !== 'undefined' &&
// @ts-ignore
typeof navigator !== 'undefined'
);
};
export interface HeadersProtocol {
get: (header: string) => string | null | undefined;
}
export type HeadersLike = Record<string, string | string[] | undefined> | HeadersProtocol;
export const isHeadersProtocol = (headers: any): headers is HeadersProtocol => {
return typeof headers?.get === 'function';
};
export const getRequiredHeader = (headers: HeadersLike, header: string): string => {
const lowerCasedHeader = header.toLowerCase();
if (isHeadersProtocol(headers)) {
// to deal with the case where the header looks like Stainless-Event-Id
const intercapsHeader =
header[0]?.toUpperCase() +
header.substring(1).replace(/([^\w])(\w)/g, (_m, g1, g2) => g1 + g2.toUpperCase());
for (const key of [header, lowerCasedHeader, header.toUpperCase(), intercapsHeader]) {
const value = headers.get(key);
if (value) {
return value;
}
}
}
for (const [key, value] of Object.entries(headers)) {
if (key.toLowerCase() === lowerCasedHeader) {
if (Array.isArray(value)) {
if (value.length <= 1) return value[0];
console.warn(`Received ${value.length} entries for the ${header} header, using the first entry.`);
return value[0];
}
return value;
}
}
throw new Error(`Could not find ${header} header`);
};
/**
* Encodes a string to Base64 format.
*/
export const toBase64 = (str: string | null | undefined): string => {
if (!str) return '';
if (typeof Buffer !== 'undefined') {
return Buffer.from(str).toString('base64');
}
if (typeof btoa !== 'undefined') {
return btoa(str);
}
throw new AnthropicError('Cannot generate b64 string; Expected `Buffer` or `btoa` to be defined');
};
|