Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

release: 0.12.2 #251

1 change: 1 addition & 0 deletions .github/workflows/handle-release-pr-title-edit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
startsWith(github.event.pull_request.head.ref, 'release-please--') &&
github.event.pull_request.state == 'open' &&
github.event.sender.login != 'stainless-bot' &&
github.event.sender.login != 'stainless-app' &&
github.repository == 'anthropics/anthropic-sdk-typescript'
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.12.1"
".": "0.12.2"
}
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# Changelog

## 0.12.2 (2024-01-18)

Full Changelog: [v0.12.1...v0.12.2](https://github.com/anthropics/anthropic-sdk-typescript/compare/v0.12.1...v0.12.2)

### Bug Fixes

* **ci:** ignore stainless-app edits to release PR title ([#258](https://github.com/anthropics/anthropic-sdk-typescript/issues/258)) ([87e4ba8](https://github.com/anthropics/anthropic-sdk-typescript/commit/87e4ba82c5b498f881db9590edbfd68c8aba0930))
* **types:** accept undefined for optional client options ([#257](https://github.com/anthropics/anthropic-sdk-typescript/issues/257)) ([a0e2c4a](https://github.com/anthropics/anthropic-sdk-typescript/commit/a0e2c4a4c4a269ad011d9a6c717c1ded2405711b))
* use default base url if BASE_URL env var is blank ([#250](https://github.com/anthropics/anthropic-sdk-typescript/issues/250)) ([e38f32f](https://github.com/anthropics/anthropic-sdk-typescript/commit/e38f32f52398f3a082eb745e85179242ecee7663))


### Chores

* **internal:** debug logging for retries; speculative retry-after-ms support ([#256](https://github.com/anthropics/anthropic-sdk-typescript/issues/256)) ([b4b70fd](https://github.com/anthropics/anthropic-sdk-typescript/commit/b4b70fdbee45dd2a68e46135db45b61381538ae8))
* **internal:** narrow type into stringifyQuery ([#253](https://github.com/anthropics/anthropic-sdk-typescript/issues/253)) ([3f42e07](https://github.com/anthropics/anthropic-sdk-typescript/commit/3f42e0702ab55cd841c0dc186732028d2fb9f5bb))


### Documentation

* fix missing async in readme code sample ([#255](https://github.com/anthropics/anthropic-sdk-typescript/issues/255)) ([553fb37](https://github.com/anthropics/anthropic-sdk-typescript/commit/553fb37159a9424a40df1e0f6bb36962ba9f5be8))
* **readme:** improve api reference ([#254](https://github.com/anthropics/anthropic-sdk-typescript/issues/254)) ([3721927](https://github.com/anthropics/anthropic-sdk-typescript/commit/3721927e895d42c167e2464f30f7f2addb690ec6))

## 0.12.1 (2024-01-08)

Full Changelog: [v0.12.0...v0.12.1](https://github.com/anthropics/anthropic-sdk-typescript/compare/v0.12.0...v0.12.1)
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This library provides convenient access to the Anthropic REST API from server-si

For the AWS Bedrock API, see [`@anthropic-ai/bedrock-sdk`](https://github.com/anthropics/anthropic-bedrock-typescript).

The API documentation can be found [here](https://docs.anthropic.com/claude/reference/).
The REST API documentation can be found [on docs.anthropic.com](https://docs.anthropic.com/claude/reference/). The full API of this library can be found in [api.md](https://www.github.com/anthropics/anthropic-sdk-typescript/blob/main/api.md).

## Installation

Expand Down Expand Up @@ -304,8 +304,8 @@ import { fetch } from 'undici'; // as one example
import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic({
fetch: (url: RequestInfo, init?: RequestInfo): Response => {
console.log('About to make request', url, init);
fetch: async (url: RequestInfo, init?: RequestInfo): Promise<Response> => {
console.log('About to make a request', url, init);
const response = await fetch(url, init);
console.log('Got response', response);
return response;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@anthropic-ai/sdk",
"version": "0.12.1",
"version": "0.12.2",
"description": "The official TypeScript library for the Anthropic API",
"author": "Anthropic <support@anthropic.com>",
"types": "dist/index.d.ts",
Expand Down
38 changes: 24 additions & 14 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,14 +417,17 @@ export abstract class APIClient {

if (!response.ok) {
if (retriesRemaining && this.shouldRetry(response)) {
const retryMessage = `retrying, ${retriesRemaining} attempts remaining`;
debug(`response (error; ${retryMessage})`, response.status, url, responseHeaders);
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;
const retryMessage = retriesRemaining ? `(error; no more retries left)` : `(error; not retryable)`;

debug('response', response.status, url, responseHeaders, errMessage);
debug(`response (error; ${retryMessage})`, response.status, url, responseHeaders, errMessage);

const err = this.makeStatusError(response.status, errJSON, errMessage, responseHeaders);
throw err;
Expand Down Expand Up @@ -452,8 +455,8 @@ export abstract class APIClient {
query = { ...defaultQuery, ...query } as Req;
}

if (query) {
url.search = this.stringifyQuery(query);
if (typeof query === 'object' && query && !Array.isArray(query)) {
url.search = this.stringifyQuery(query as Record<string, unknown>);
}

return url.toString();
Expand Down Expand Up @@ -529,11 +532,21 @@ export abstract class APIClient {
retriesRemaining: number,
responseHeaders?: Headers | undefined,
): Promise<APIResponseProps> {
// About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
let timeoutMillis: number | undefined;

// Note the `retry-after-ms` header may not be standard, but is a good idea and we'd like proactive support for it.
const retryAfterMillisHeader = responseHeaders?.['retry-after-ms'];
if (retryAfterMillisHeader) {
const timeoutMs = parseFloat(retryAfterMillisHeader);
if (!Number.isNaN(timeoutMs)) {
timeoutMillis = timeoutMs;
}
}

// About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
const retryAfterHeader = responseHeaders?.['retry-after'];
if (retryAfterHeader) {
const timeoutSeconds = parseInt(retryAfterHeader);
if (retryAfterHeader && !timeoutMillis) {
const timeoutSeconds = parseFloat(retryAfterHeader);
if (!Number.isNaN(timeoutSeconds)) {
timeoutMillis = timeoutSeconds * 1000;
} else {
Expand All @@ -543,12 +556,7 @@ export abstract class APIClient {

// 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
) {
if (!(timeoutMillis && 0 <= timeoutMillis && timeoutMillis < 60 * 1000)) {
const maxRetries = options.maxRetries ?? this.maxRetries;
timeoutMillis = this.calculateDefaultRetryTimeoutMillis(retriesRemaining, maxRetries);
}
Expand Down Expand Up @@ -961,14 +969,16 @@ export const ensurePresent = <T>(value: T | null | undefined): T => {
/**
* Read an environment variable.
*
* Trims beginning and trailing whitespace.
*
* 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;
return process.env?.[env]?.trim() ?? undefined;
}
if (typeof Deno !== 'undefined') {
return Deno.env?.get?.(env);
return Deno.env?.get?.(env)?.trim();
}
return undefined;
};
Expand Down
10 changes: 5 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ export interface ClientOptions {
/**
* Defaults to process.env['ANTHROPIC_API_KEY'].
*/
apiKey?: string | null;
apiKey?: string | null | undefined;

/**
* Defaults to process.env['ANTHROPIC_AUTH_TOKEN'].
*/
authToken?: string | null;
authToken?: string | null | undefined;

/**
* Override the default base URL for the API, e.g., "https://api.example.com/v2/"
Expand Down Expand Up @@ -84,8 +84,8 @@ export class Anthropic extends Core.APIClient {
/**
* API Client for interfacing with the Anthropic API.
*
* @param {string | null} [opts.apiKey=process.env['ANTHROPIC_API_KEY'] ?? null]
* @param {string | null} [opts.authToken=process.env['ANTHROPIC_AUTH_TOKEN'] ?? null]
* @param {string | null | undefined} [opts.apiKey=process.env['ANTHROPIC_API_KEY'] ?? null]
* @param {string | null | undefined} [opts.authToken=process.env['ANTHROPIC_AUTH_TOKEN'] ?? null]
* @param {string} [opts.baseURL=process.env['ANTHROPIC_BASE_URL'] ?? https://api.anthropic.com] - Override the default base URL for the API.
* @param {number} [opts.timeout=10 minutes] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out.
* @param {number} [opts.httpAgent] - An HTTP agent used to manage HTTP(s) connections.
Expand All @@ -104,7 +104,7 @@ export class Anthropic extends Core.APIClient {
apiKey,
authToken,
...opts,
baseURL: baseURL ?? `https://api.anthropic.com`,
baseURL: baseURL || `https://api.anthropic.com`,
};

super({
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = '0.12.1'; // x-release-please-version
export const VERSION = '0.12.2'; // x-release-please-version
77 changes: 72 additions & 5 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ describe('instantiate client', () => {
});

afterEach(() => {
process.env['SINK_BASE_URL'] = undefined;
process.env['ANTHROPIC_BASE_URL'] = undefined;
});

test('explicit option', () => {
Expand All @@ -153,6 +153,18 @@ describe('instantiate client', () => {
const client = new Anthropic({ apiKey: 'my-anthropic-api-key' });
expect(client.baseURL).toEqual('https://example.com/from_env');
});

test('empty env variable', () => {
process.env['ANTHROPIC_BASE_URL'] = ''; // empty
const client = new Anthropic({ apiKey: 'my-anthropic-api-key' });
expect(client.baseURL).toEqual('https://api.anthropic.com');
});

test('blank env variable', () => {
process.env['ANTHROPIC_BASE_URL'] = ' '; // blank
const client = new Anthropic({ apiKey: 'my-anthropic-api-key' });
expect(client.baseURL).toEqual('https://api.anthropic.com');
});
});

test('maxRetries option is correctly set', () => {
Expand Down Expand Up @@ -211,17 +223,18 @@ describe('request building', () => {
});

describe('retries', () => {
test('single retry', async () => {
test('retry on timeout', async () => {
let count = 0;
const testFetch = async (url: RequestInfo, { signal }: RequestInit = {}): Promise<Response> => {
if (!count++)
if (count++ === 0) {
return new Promise(
(resolve, reject) => signal?.addEventListener('abort', () => reject(new Error('timed out'))),
);
}
return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
};

const client = new Anthropic({ apiKey: 'my-anthropic-api-key', timeout: 2000, fetch: testFetch });
const client = new Anthropic({ apiKey: 'my-anthropic-api-key', timeout: 10, fetch: testFetch });

expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 });
expect(count).toEqual(2);
Expand All @@ -232,5 +245,59 @@ describe('retries', () => {
.then((r) => r.text()),
).toEqual(JSON.stringify({ a: 1 }));
expect(count).toEqual(3);
}, 10000);
});

test('retry on 429 with retry-after', async () => {
let count = 0;
const testFetch = async (url: RequestInfo, { signal }: RequestInit = {}): Promise<Response> => {
if (count++ === 0) {
return new Response(undefined, {
status: 429,
headers: {
'Retry-After': '0.1',
},
});
}
return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
};

const client = new Anthropic({ apiKey: 'my-anthropic-api-key', fetch: testFetch });

expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 });
expect(count).toEqual(2);
expect(
await client
.request({ path: '/foo', method: 'get' })
.asResponse()
.then((r) => r.text()),
).toEqual(JSON.stringify({ a: 1 }));
expect(count).toEqual(3);
});

test('retry on 429 with retry-after-ms', async () => {
let count = 0;
const testFetch = async (url: RequestInfo, { signal }: RequestInit = {}): Promise<Response> => {
if (count++ === 0) {
return new Response(undefined, {
status: 429,
headers: {
'Retry-After-Ms': '10',
},
});
}
return new Response(JSON.stringify({ a: 1 }), { headers: { 'Content-Type': 'application/json' } });
};

const client = new Anthropic({ apiKey: 'my-anthropic-api-key', fetch: testFetch });

expect(await client.request({ path: '/foo', method: 'get' })).toEqual({ a: 1 });
expect(count).toEqual(2);
expect(
await client
.request({ path: '/foo', method: 'get' })
.asResponse()
.then((r) => r.text()),
).toEqual(JSON.stringify({ a: 1 }));
expect(count).toEqual(3);
});
});
Loading