UNPKG

portkey-ai

Version:
388 lines (352 loc) 10.3 kB
import KeepAliveAgent from 'agentkeepalive'; import type { Agent } from 'node:http'; import { APIResponseType, ApiClientInterface, Headers, } from './_types/generalTypes'; import { createHeaders } from './apis'; import { PORTKEY_HEADER_PREFIX } from './constants'; import { APIConnectionError, APIConnectionTimeoutError, APIError, APIUserAbortError, } from './error'; import { Stream, createResponseHeaders, safeJSON } from './streaming'; import { castToError, getPlatformProperties, parseBody } from './utils'; import { VERSION } from './version'; const defaultHttpAgent: Agent = new KeepAliveAgent({ keepAlive: true, timeout: 5 * 60 * 1000, }); function getFetch(): Fetch { if (typeof window !== 'undefined' && window.fetch) { return window.fetch.bind(window); } if (typeof global !== 'undefined' && global.fetch) { return global.fetch; } if (typeof fetch !== 'undefined') { return fetch; } throw new Error('Fetch is not available in this environment'); } export type Fetch = (url: string, init?: RequestInit) => Promise<Response>; export type HTTPMethod = 'post' | 'get' | 'put' | 'delete'; export type FinalRequestOptions = RequestOptions & { method: HTTPMethod; path: string; }; export type RequestOptions = { method?: HTTPMethod; path?: string; query?: Record<string, any> | undefined; body?: Record<string, any> | undefined; headers?: Headers | undefined; httpAgent?: Agent; stream?: boolean | undefined; signal?: AbortSignal | undefined | null; extraHeaders?: Record<string, string>; }; type APIResponseProps = { response: Response; options: FinalRequestOptions; responseHeaders: globalThis.Headers; }; type PromiseOrValue<T> = T | Promise<T>; async function defaultParseResponse<T>(props: APIResponseProps): Promise<T> { const { response } = props; if (props.options.stream) { return new Stream(response) as any; } const contentType = response.headers.get('content-type'); if (contentType?.includes('application/json')) { const headers = defaultParseHeaders(props); const json = { ...((await response.json()) as any), getHeaders: () => headers, }; return json as T; } const text = await response.text(); return text as any as T; } function defaultParseHeaders(props: APIResponseProps): Record<string, string> { const { responseHeaders } = props; const parsedHeaders = createResponseHeaders(responseHeaders); const prefix = PORTKEY_HEADER_PREFIX; const filteredHeaders = Object.entries(parsedHeaders) .filter(([key, _]) => key.startsWith(prefix)) // eslint-disable-line @typescript-eslint/no-unused-vars .map(([key, value]) => [key.replace(prefix, ''), value]); return Object.fromEntries(filteredHeaders); } 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); }); } 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 { apiKey: string | null; baseURL: string; customHeaders: Record<string, string>; responseHeaders: Record<string, string>; portkeyHeaders: Record<string, string>; maxRetries = 1; private fetch: Fetch; constructor({ apiKey, baseURL, config, virtualKey, traceID, metadata, provider, Authorization, cacheForceRefresh, debug, customHost, openaiProject, openaiOrganization, awsSecretAccessKey, awsAccessKeyId, awsSessionToken, awsRegion, vertexProjectId, vertexRegion, workersAiAccountId, azureResourceName, azureDeploymentId, azureApiVersion, azureEndpointName, huggingfaceBaseUrl, forwardHeaders, cacheNamespace, requestTimeout, strictOpenAiCompliance = false, anthropicBeta, anthropicVersion, mistralFimCompletion, dangerouslyAllowBrowser, vertexStorageBucketName, providerFileName, providerModel, awsS3Bucket, awsS3ObjectKey, awsBedrockModel, fireworksAccountId, ...rest }: ApiClientInterface) { this.apiKey = apiKey ?? ''; this.baseURL = baseURL ?? ''; this.customHeaders = createHeaders({ apiKey, config, virtualKey, traceID, metadata, provider, Authorization, cacheForceRefresh, debug, customHost, cacheNamespace, openaiProject, openaiOrganization, awsSecretAccessKey, awsAccessKeyId, awsSessionToken, awsRegion, vertexProjectId, vertexRegion, workersAiAccountId, azureResourceName, azureDeploymentId, azureApiVersion, azureEndpointName, huggingfaceBaseUrl, forwardHeaders, requestTimeout, strictOpenAiCompliance, anthropicVersion, mistralFimCompletion, anthropicBeta, dangerouslyAllowBrowser, vertexStorageBucketName, providerFileName, providerModel, awsS3Bucket, awsS3ObjectKey, awsBedrockModel, fireworksAccountId, ...rest, }); this.portkeyHeaders = this.defaultHeaders(); this.fetch = getFetch(); this.responseHeaders = {}; } protected defaultHeaders(): Record<string, string> { return { 'Content-Type': 'application/json', [`${PORTKEY_HEADER_PREFIX}package-version`]: `portkey-${VERSION}`, ...getPlatformProperties(), }; } _post<Rsp extends APIResponseType>( path: string, opts?: RequestOptions ): APIPromise<Rsp> { return this.methodRequest('post', path, opts); } _put<Rsp extends APIResponseType>( path: string, opts?: RequestOptions ): APIPromise<Rsp> { return this.methodRequest('put', path, opts); } _get<Rsp extends APIResponseType>( path: string, opts?: RequestOptions ): APIPromise<Rsp> { return this.methodRequest('get', path, opts); } _delete<Rsp extends APIResponseType>( path: string, opts?: RequestOptions ): APIPromise<Rsp> { return this.methodRequest('delete', path, opts); } protected generateError( status: number | undefined, errorResponse: object | undefined, message: string | undefined, headers: Headers | undefined ): APIError { return APIError.generate(status, errorResponse, message, headers); } _shouldRetry(response: Response): boolean { const retryStatusCode = response.status; const retryTraceId = response.headers.get('x-portkey-trace-id'); const retryRequestId = response.headers.get('x-portkey-request-id'); const retryGatewayException = response.headers.get( 'x-portkey-gateway-exception' ); if ( retryStatusCode < 500 || retryTraceId || retryRequestId || retryGatewayException ) { return false; } return true; } async request( opts: FinalRequestOptions, retryCount = 0 ): Promise<APIResponseProps> { // Build the request. const { req, url } = this.buildRequest(opts); // Make the call to rubeus. const response = await this.fetch(url, req).catch(castToError); // Parse the response and check for errors. if (response instanceof Error) { if (opts.signal?.aborted) { throw new APIUserAbortError(); } if (retryCount < this.maxRetries) { return this.request(opts, retryCount + 1); } if (response.name === 'AbortError') { throw new APIConnectionTimeoutError({ message: `${response.message} \n STACK: ${response.stack}`, }); } throw new APIConnectionError({ cause: response }); } this.responseHeaders = createResponseHeaders(response.headers); if (!response.ok) { if (retryCount < this.maxRetries && this._shouldRetry(response)) { return this.request(opts, retryCount + 1); } const errText = await response.text().catch(() => 'Unknown'); const errJSON = safeJSON(errText); const errMessage = errJSON ? undefined : errText; throw this.generateError( response.status, errJSON, errMessage, this.responseHeaders ); } // Receive and format the response. return { response, options: opts, responseHeaders: response.headers }; } buildRequest(opts: FinalRequestOptions): { req: RequestInit; url: string } { const url = new URL(this.baseURL + opts.path); const { method, body, extraHeaders } = opts; const reqHeaders: Record<string, string> = { ...this.defaultHeaders(), ...this.customHeaders, ...extraHeaders, }; const agentConfig = typeof window === 'undefined' ? { agent: defaultHttpAgent } : {}; const req: RequestInit = { method, headers: reqHeaders, ...agentConfig, }; if (method !== 'get' && body !== undefined) { req.body = JSON.stringify(parseBody(body)); } return { req: req, url: url.toString() }; } methodRequest<Rsp>( method: HTTPMethod, path: string, opts?: RequestOptions ): APIPromise<Rsp> { return new APIPromise(this.request({ method, path, ...opts })); } }