Skip to content

chore: unreleased changes #49

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

Merged
merged 7 commits into from
Jul 13, 2023
Merged
2 changes: 1 addition & 1 deletion examples/cancellation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async function main() {

const stream = await client.completions.create({
prompt: `${Anthropic.HUMAN_PROMPT}${question}${Anthropic.AI_PROMPT}:`,
model: 'claude-v1',
model: 'claude-2',
stream: true,
max_tokens_to_sample: 500,
});
Expand Down
2 changes: 1 addition & 1 deletion examples/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const client = new Anthropic(); // gets API Key from environment variable ANTHRO
async function main() {
const result = await client.completions.create({
prompt: `${Anthropic.HUMAN_PROMPT} how does a court case get to the Supreme Court? ${Anthropic.AI_PROMPT}`,
model: 'claude-v1.3',
model: 'claude-2',
max_tokens_to_sample: 300,
});
console.log(result.completion);
Expand Down
2 changes: 1 addition & 1 deletion examples/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ async function main() {

const stream = await client.completions.create({
prompt: `${Anthropic.HUMAN_PROMPT}${question}${Anthropic.AI_PROMPT}:`,
model: 'claude-v1',
model: 'claude-2',
stream: true,
max_tokens_to_sample: 500,
});
Expand Down
18 changes: 15 additions & 3 deletions src/core.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as qs from 'qs';
import { VERSION } from './version';
import { Stream } from './streaming';
import { APIError, APIConnectionError, APIConnectionTimeoutError } from './error';
import { APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError } from './error';
import type { Readable } from '@anthropic-ai/sdk/_shims/node-readable';
import { getDefaultAgent, type Agent } from '@anthropic-ai/sdk/_shims/agent';
import {
Expand Down Expand Up @@ -183,6 +183,9 @@ export abstract class APIClient {
...(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);
Expand Down Expand Up @@ -220,8 +223,15 @@ export abstract class APIClient {
const response = await this.fetchWithTimeout(url, req, timeout, controller).catch(castToError);

if (response instanceof Error) {
if (retriesRemaining) return this.retryRequest(options, retriesRemaining);
if (response.name === 'AbortError') throw new APIConnectionTimeoutError();
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 });
}

Expand Down Expand Up @@ -561,6 +571,7 @@ export type RequestOptions<Req extends {} = Record<string, unknown> | Readable>
stream?: boolean | undefined;
timeout?: number;
httpAgent?: Agent;
signal?: AbortSignal | undefined | null;
idempotencyKey?: string;
};

Expand All @@ -578,6 +589,7 @@ const requestOptionsKeys: KeysEnum<RequestOptions> = {
stream: true,
timeout: true,
httpAgent: true,
signal: true,
idempotencyKey: true,
};

Expand Down
8 changes: 8 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ export class APIError extends Error {
}
}

export class APIUserAbortError extends APIError {
override readonly status: undefined = undefined;

constructor({ message }: { message?: string } = {}) {
super(undefined, undefined, message || 'Request was aborted.', undefined);
}
}

export class APIConnectionError extends APIError {
override readonly status: undefined = undefined;

Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export class Anthropic extends Core.APIClient {
static APIError = Errors.APIError;
static APIConnectionError = Errors.APIConnectionError;
static APIConnectionTimeoutError = Errors.APIConnectionTimeoutError;
static APIUserAbortError = Errors.APIUserAbortError;
static NotFoundError = Errors.NotFoundError;
static ConflictError = Errors.ConflictError;
static RateLimitError = Errors.RateLimitError;
Expand All @@ -183,6 +184,7 @@ export const {
APIError,
APIConnectionError,
APIConnectionTimeoutError,
APIUserAbortError,
NotFoundError,
ConflictError,
RateLimitError,
Expand Down
4 changes: 3 additions & 1 deletion src/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export class Stream<Item> implements AsyncIterable<Item>, APIResponse<Stream<Ite
}

async *[Symbol.asyncIterator](): AsyncIterator<Item, any, undefined> {
let done = false;
try {
for await (const sse of this.iterMessages()) {
if (sse.event === 'completion') {
Expand All @@ -75,13 +76,14 @@ export class Stream<Item> implements AsyncIterable<Item>, APIResponse<Stream<Ite
throw APIError.generate(undefined, errJSON, errMessage, this.responseHeaders);
}
}
done = true;
} catch (e) {
// If the user calls `stream.controller.abort()`, we should exit without throwing.
if (e instanceof Error && e.name === 'AbortError') return;
throw e;
} finally {
// If the user `break`s, abort the ongoing request.
this.controller.abort();
if (!done) this.controller.abort();
}
}
}
Expand Down
31 changes: 29 additions & 2 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// File generated from our OpenAPI spec by Stainless.

import { Headers } from '@anthropic-ai/sdk/core';
import Anthropic from '@anthropic-ai/sdk';
import { Response } from '@anthropic-ai/sdk/_shims/fetch';
import { APIUserAbortError } from '@anthropic-ai/sdk';
import { Headers } from '@anthropic-ai/sdk/core';
import { Response, fetch as defaultFetch } from '@anthropic-ai/sdk/_shims/fetch';

describe('instantiate client', () => {
const env = process.env;
Expand Down Expand Up @@ -95,6 +96,32 @@ describe('instantiate client', () => {
expect(response).toEqual({ url: 'http://localhost:5000/foo', custom: true });
});

test('custom signal', async () => {
const client = new Anthropic({
baseURL: 'http://127.0.0.1:4010',
apiKey: 'my api key',
fetch: (...args) => {
return new Promise((resolve, reject) =>
setTimeout(
() =>
defaultFetch(...args)
.then(resolve)
.catch(reject),
300,
),
);
},
});

const controller = new AbortController();
setTimeout(() => controller.abort(), 200);

const spy = jest.spyOn(client, 'request');

await expect(client.get('/foo', { signal: controller.signal })).rejects.toThrowError(APIUserAbortError);
expect(spy).toHaveBeenCalledTimes(1);
});

describe('baseUrl', () => {
test('trailing slash', () => {
const client = new Anthropic({ baseURL: 'http://localhost:5000/custom/path/', apiKey: 'my api key' });
Expand Down