TypeScript SDK mostly working

Ok so this is still pretty rough, and notably there's no reporting for streaming. But for non-streaming requests I've verified that this does in fact report requests locally.
This commit is contained in:
Kyle Corbitt
2023-08-14 23:22:27 -07:00
parent 5da62fdc29
commit 8f4e7f7e2e
42 changed files with 1478 additions and 1155 deletions

View File

@@ -72,6 +72,7 @@
"nextjs-cors": "^2.1.2",
"nextjs-routes": "^2.0.1",
"openai": "4.0.0-beta.7",
"openpipe": "workspace:*",
"pg": "^8.11.2",
"pluralize": "^8.0.0",
"posthog-js": "^1.75.3",
@@ -100,8 +101,7 @@
"uuid": "^9.0.0",
"vite-tsconfig-paths": "^4.2.0",
"zod": "^3.21.4",
"zustand": "^4.3.9",
"openpipe": "workspace:*"
"zustand": "^4.3.9"
},
"devDependencies": {
"@openapi-contrib/openapi-schema-to-json-schema": "^4.0.5",
@@ -129,6 +129,7 @@
"eslint-plugin-unused-imports": "^2.0.0",
"monaco-editor": "^0.40.0",
"openapi-typescript": "^6.3.4",
"openapi-typescript-codegen": "^0.25.0",
"prisma": "^4.14.0",
"raw-loader": "^4.0.2",
"typescript": "^5.0.4",

View File

@@ -1,54 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
import {
type ChatCompletionChunk,
type ChatCompletion,
type CompletionCreateParams,
} from "openai/resources/chat";
import { type CompletionResponse } from "../types";
import { isArray, isString, omit } from "lodash-es";
import { openai } from "~/server/utils/openai";
import { isArray, isString } from "lodash-es";
import { APIError } from "openai";
const mergeStreamedChunks = (
base: ChatCompletion | null,
chunk: ChatCompletionChunk,
): ChatCompletion => {
if (base === null) {
return mergeStreamedChunks({ ...chunk, choices: [] }, chunk);
}
const choices = [...base.choices];
for (const choice of chunk.choices) {
const baseChoice = choices.find((c) => c.index === choice.index);
if (baseChoice) {
baseChoice.finish_reason = choice.finish_reason ?? baseChoice.finish_reason;
baseChoice.message = baseChoice.message ?? { role: "assistant" };
if (choice.delta?.content)
baseChoice.message.content =
((baseChoice.message.content as string) ?? "") + (choice.delta.content ?? "");
if (choice.delta?.function_call) {
const fnCall = baseChoice.message.function_call ?? {};
fnCall.name =
((fnCall.name as string) ?? "") + ((choice.delta.function_call.name as string) ?? "");
fnCall.arguments =
((fnCall.arguments as string) ?? "") +
((choice.delta.function_call.arguments as string) ?? "");
}
} else {
// @ts-expect-error the types are correctly telling us that finish_reason
// could be null, but don't want to fix it right now.
choices.push({ ...omit(choice, "delta"), message: { role: "assistant", ...choice.delta } });
}
}
const merged: ChatCompletion = {
...base,
choices,
};
return merged;
};
import { type ChatCompletion, type CompletionCreateParams } from "openai/resources/chat";
import mergeChunks from "openpipe/src/openai/mergeChunks";
import { openai } from "~/server/utils/openai";
import { type CompletionResponse } from "../types";
export async function getCompletion(
input: CompletionCreateParams,
@@ -59,7 +15,6 @@ export async function getCompletion(
try {
if (onStream) {
console.log("got started");
const resp = await openai.chat.completions.create(
{ ...input, stream: true },
{
@@ -67,11 +22,9 @@ export async function getCompletion(
},
);
for await (const part of resp) {
console.log("got part", part);
finalCompletion = mergeStreamedChunks(finalCompletion, part);
finalCompletion = mergeChunks(finalCompletion, part);
onStream(finalCompletion);
}
console.log("got final", finalCompletion);
if (!finalCompletion) {
return {
type: "error",

View File

@@ -107,7 +107,7 @@ export const v1ApiRouter = createOpenApiRouter({
.default({}),
}),
)
.output(z.void())
.output(z.object({ status: z.literal("ok") }))
.mutation(async ({ input, ctx }) => {
const reqPayload = await reqValidator.spa(input.reqPayload);
const respPayload = await respValidator.spa(input.respPayload);
@@ -166,6 +166,7 @@ export const v1ApiRouter = createOpenApiRouter({
]);
await createTags(newLoggedCallId, input.tags);
return { status: "ok" };
}),
localTestingOnlyGetLatestLoggedCall: openApiProtectedProc
.meta({

View File

@@ -3,6 +3,7 @@ import { openApiDocument } from "~/pages/api/v1/openapi.json";
import fs from "fs";
import path from "path";
import { execSync } from "child_process";
import { generate } from "openapi-typescript-codegen";
const scriptPath = import.meta.url.replace("file://", "");
const clientLibsPath = path.join(path.dirname(scriptPath), "../../../../client-libs");
@@ -18,13 +19,20 @@ console.log("Generating TypeScript client");
const tsClientPath = path.join(clientLibsPath, "typescript/src/codegen");
fs.rmSync(tsClientPath, { recursive: true, force: true });
fs.mkdirSync(tsClientPath, { recursive: true });
execSync(
`pnpm dlx @openapitools/openapi-generator-cli generate -i "${schemaPath}" -g typescript-axios -o "${tsClientPath}"`,
{
stdio: "inherit",
},
);
await generate({
input: openApiDocument,
output: tsClientPath,
clientName: "OPClient",
httpClient: "node",
});
// execSync(
// `pnpm run openapi generate --input "${schemaPath}" --output "${tsClientPath}" --name OPClient --client node`,
// {
// stdio: "inherit",
// },
// );
console.log("Generating Python client");

View File

@@ -1,7 +1,6 @@
import { type ClientOptions } from "openai";
import fs from "fs";
import path from "path";
import OpenAI from "openpipe/src/openai";
import OpenAI, { type ClientOptions } from "openpipe/src/openai";
import { env } from "~/env.mjs";
@@ -16,7 +15,13 @@ try {
config = JSON.parse(jsonData.toString());
} catch (error) {
// Set a dummy key so it doesn't fail at build time
config = { apiKey: env.OPENAI_API_KEY ?? "dummy-key" };
config = {
apiKey: env.OPENAI_API_KEY ?? "dummy-key",
openpipe: {
apiKey: env.OPENPIPE_API_KEY,
baseUrl: "http://localhost:3000/api/v1",
},
};
}
// export const openai = env.OPENPIPE_API_KEY ? new OpenAI.OpenAI(config) : new OriginalOpenAI(config);

View File

@@ -137,7 +137,21 @@
"description": "Successful response",
"content": {
"application/json": {
"schema": {}
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"ok"
]
}
},
"required": [
"status"
],
"additionalProperties": false
}
}
}
},

View File

@@ -6,6 +6,7 @@ import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.report_json_body import ReportJsonBody
from ...models.report_response_200 import ReportResponse200
from ...types import Response
@@ -24,16 +25,22 @@ def _get_kwargs(
}
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]:
def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[ReportResponse200]:
if response.status_code == HTTPStatus.OK:
return None
response_200 = ReportResponse200.from_dict(response.json())
return response_200
if client.raise_on_unexpected_status:
raise errors.UnexpectedStatus(response.status_code, response.content)
else:
return None
def _build_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Response[Any]:
def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[ReportResponse200]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
@@ -46,7 +53,7 @@ def sync_detailed(
*,
client: AuthenticatedClient,
json_body: ReportJsonBody,
) -> Response[Any]:
) -> Response[ReportResponse200]:
"""Report an API call
Args:
@@ -57,7 +64,7 @@ def sync_detailed(
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Any]
Response[ReportResponse200]
"""
kwargs = _get_kwargs(
@@ -71,11 +78,11 @@ def sync_detailed(
return _build_response(client=client, response=response)
async def asyncio_detailed(
def sync(
*,
client: AuthenticatedClient,
json_body: ReportJsonBody,
) -> Response[Any]:
) -> Optional[ReportResponse200]:
"""Report an API call
Args:
@@ -86,7 +93,31 @@ async def asyncio_detailed(
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Any]
ReportResponse200
"""
return sync_detailed(
client=client,
json_body=json_body,
).parsed
async def asyncio_detailed(
*,
client: AuthenticatedClient,
json_body: ReportJsonBody,
) -> Response[ReportResponse200]:
"""Report an API call
Args:
json_body (ReportJsonBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[ReportResponse200]
"""
kwargs = _get_kwargs(
@@ -96,3 +127,29 @@ async def asyncio_detailed(
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
*,
client: AuthenticatedClient,
json_body: ReportJsonBody,
) -> Optional[ReportResponse200]:
"""Report an API call
Args:
json_body (ReportJsonBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
ReportResponse200
"""
return (
await asyncio_detailed(
client=client,
json_body=json_body,
)
).parsed

View File

@@ -12,6 +12,8 @@ from .local_testing_only_get_latest_logged_call_response_200_tags import (
)
from .report_json_body import ReportJsonBody
from .report_json_body_tags import ReportJsonBodyTags
from .report_response_200 import ReportResponse200
from .report_response_200_status import ReportResponse200Status
__all__ = (
"CheckCacheJsonBody",
@@ -22,4 +24,6 @@ __all__ = (
"LocalTestingOnlyGetLatestLoggedCallResponse200Tags",
"ReportJsonBody",
"ReportJsonBodyTags",
"ReportResponse200",
"ReportResponse200Status",
)

View File

@@ -0,0 +1,40 @@
from typing import Any, Dict, Type, TypeVar
from attrs import define
from ..models.report_response_200_status import ReportResponse200Status
T = TypeVar("T", bound="ReportResponse200")
@define
class ReportResponse200:
"""
Attributes:
status (ReportResponse200Status):
"""
status: ReportResponse200Status
def to_dict(self) -> Dict[str, Any]:
status = self.status.value
field_dict: Dict[str, Any] = {}
field_dict.update(
{
"status": status,
}
)
return field_dict
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
d = src_dict.copy()
status = ReportResponse200Status(d.pop("status"))
report_response_200 = cls(
status=status,
)
return report_response_200

View File

@@ -0,0 +1,8 @@
from enum import Enum
class ReportResponse200Status(str, Enum):
OK = "ok"
def __str__(self) -> str:
return str(self.value)

View File

@@ -19,7 +19,7 @@ configured_client = AuthenticatedClient(
def _get_tags(openpipe_options):
tags = openpipe_options.get("tags") or {}
tags["$sdk"] = "python"
tags["$sdk_version"] = version
tags["$sdk.version"] = version
return ReportJsonBodyTags.from_dict(tags)

View File

@@ -156,7 +156,7 @@ async def test_caching():
completion2 = await openai.ChatCompletion.acreate(
model="gpt-3.5-turbo",
messages=[{"role": "system", "content": "count to 10"}],
messages=messages,
openpipe={"cache": True},
)
assert completion2.openpipe.cache_status == "HIT"

View File

@@ -1,3 +1,3 @@
// main.ts or index.ts at the root level
export * as OpenAI from './openai';
export * as OpenAILegacy from './openai-legacy';
export * as OpenAI from "./src/openai";
export * as OpenAILegacy from "./src/openai-legacy";

View File

@@ -4,7 +4,8 @@
"type": "module",
"description": "Metrics and auto-evaluation for LLM calls",
"scripts": {
"build": "tsc"
"build": "tsc",
"test": "vitest"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -12,7 +13,8 @@
"author": "",
"license": "Apache-2.0",
"dependencies": {
"axios": "^0.26.0",
"form-data": "^4.0.0",
"node-fetch": "^3.3.2",
"openai-beta": "npm:openai@4.0.0-beta.7",
"openai-legacy": "npm:openai@3.3.0"
},
@@ -20,6 +22,7 @@
"@types/node": "^20.4.8",
"dotenv": "^16.3.1",
"tsx": "^3.12.7",
"typescript": "^5.0.4"
"typescript": "^5.0.4",
"vitest": "^0.33.0"
}
}

View File

@@ -1,4 +0,0 @@
wwwroot/*.js
node_modules
typings
dist

View File

@@ -1 +0,0 @@
# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm

View File

@@ -1,23 +0,0 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md

View File

@@ -1,9 +0,0 @@
.gitignore
.npmignore
.openapi-generator-ignore
api.ts
base.ts
common.ts
configuration.ts
git_push.sh
index.ts

View File

@@ -0,0 +1,35 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BaseHttpRequest } from './core/BaseHttpRequest';
import type { OpenAPIConfig } from './core/OpenAPI';
import { NodeHttpRequest } from './core/NodeHttpRequest';
import { DefaultService } from './services/DefaultService';
type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest;
export class OPClient {
public readonly default: DefaultService;
public readonly request: BaseHttpRequest;
constructor(config?: Partial<OpenAPIConfig>, HttpRequest: HttpRequestConstructor = NodeHttpRequest) {
this.request = new HttpRequest({
BASE: config?.BASE ?? 'https://app.openpipe.ai/api/v1',
VERSION: config?.VERSION ?? '0.1.1',
WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
CREDENTIALS: config?.CREDENTIALS ?? 'include',
TOKEN: config?.TOKEN,
USERNAME: config?.USERNAME,
PASSWORD: config?.PASSWORD,
HEADERS: config?.HEADERS,
ENCODE_PATH: config?.ENCODE_PATH,
});
this.default = new DefaultService(this.request);
}
}

View File

@@ -1,455 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* OpenPipe API
* The public API for reporting API calls to OpenPipe
*
* The version of the OpenAPI document: 0.1.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import type { Configuration } from './configuration';
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
import globalAxios from 'axios';
// Some imports not used depending on template conditions
// @ts-ignore
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common';
import type { RequestArgs } from './base';
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError } from './base';
/**
*
* @export
* @interface CheckCache200Response
*/
export interface CheckCache200Response {
/**
* JSON-encoded response payload
* @type {any}
* @memberof CheckCache200Response
*/
'respPayload'?: any;
}
/**
*
* @export
* @interface CheckCacheDefaultResponse
*/
export interface CheckCacheDefaultResponse {
/**
*
* @type {string}
* @memberof CheckCacheDefaultResponse
*/
'message': string;
/**
*
* @type {string}
* @memberof CheckCacheDefaultResponse
*/
'code': string;
/**
*
* @type {Array<CheckCacheDefaultResponseIssuesInner>}
* @memberof CheckCacheDefaultResponse
*/
'issues'?: Array<CheckCacheDefaultResponseIssuesInner>;
}
/**
*
* @export
* @interface CheckCacheDefaultResponseIssuesInner
*/
export interface CheckCacheDefaultResponseIssuesInner {
/**
*
* @type {string}
* @memberof CheckCacheDefaultResponseIssuesInner
*/
'message': string;
}
/**
*
* @export
* @interface CheckCacheRequest
*/
export interface CheckCacheRequest {
/**
* Unix timestamp in milliseconds
* @type {number}
* @memberof CheckCacheRequest
*/
'requestedAt': number;
/**
* JSON-encoded request payload
* @type {any}
* @memberof CheckCacheRequest
*/
'reqPayload'?: any;
/**
* Extra tags to attach to the call for filtering. Eg { \"userId\": \"123\", \"promptId\": \"populate-title\" }
* @type {{ [key: string]: string; }}
* @memberof CheckCacheRequest
*/
'tags'?: { [key: string]: string; };
}
/**
*
* @export
* @interface LocalTestingOnlyGetLatestLoggedCall200Response
*/
export interface LocalTestingOnlyGetLatestLoggedCall200Response {
/**
*
* @type {string}
* @memberof LocalTestingOnlyGetLatestLoggedCall200Response
*/
'createdAt': string;
/**
*
* @type {boolean}
* @memberof LocalTestingOnlyGetLatestLoggedCall200Response
*/
'cacheHit': boolean;
/**
*
* @type {{ [key: string]: string; }}
* @memberof LocalTestingOnlyGetLatestLoggedCall200Response
*/
'tags': { [key: string]: string; };
/**
*
* @type {LocalTestingOnlyGetLatestLoggedCall200ResponseModelResponse}
* @memberof LocalTestingOnlyGetLatestLoggedCall200Response
*/
'modelResponse': LocalTestingOnlyGetLatestLoggedCall200ResponseModelResponse | null;
}
/**
*
* @export
* @interface LocalTestingOnlyGetLatestLoggedCall200ResponseModelResponse
*/
export interface LocalTestingOnlyGetLatestLoggedCall200ResponseModelResponse {
/**
*
* @type {string}
* @memberof LocalTestingOnlyGetLatestLoggedCall200ResponseModelResponse
*/
'id': string;
/**
*
* @type {number}
* @memberof LocalTestingOnlyGetLatestLoggedCall200ResponseModelResponse
*/
'statusCode': number | null;
/**
*
* @type {string}
* @memberof LocalTestingOnlyGetLatestLoggedCall200ResponseModelResponse
*/
'errorMessage': string | null;
/**
*
* @type {any}
* @memberof LocalTestingOnlyGetLatestLoggedCall200ResponseModelResponse
*/
'reqPayload'?: any;
/**
*
* @type {any}
* @memberof LocalTestingOnlyGetLatestLoggedCall200ResponseModelResponse
*/
'respPayload'?: any;
}
/**
*
* @export
* @interface ReportRequest
*/
export interface ReportRequest {
/**
* Unix timestamp in milliseconds
* @type {number}
* @memberof ReportRequest
*/
'requestedAt': number;
/**
* Unix timestamp in milliseconds
* @type {number}
* @memberof ReportRequest
*/
'receivedAt': number;
/**
* JSON-encoded request payload
* @type {any}
* @memberof ReportRequest
*/
'reqPayload'?: any;
/**
* JSON-encoded response payload
* @type {any}
* @memberof ReportRequest
*/
'respPayload'?: any;
/**
* HTTP status code of response
* @type {number}
* @memberof ReportRequest
*/
'statusCode'?: number;
/**
* User-friendly error message
* @type {string}
* @memberof ReportRequest
*/
'errorMessage'?: string;
/**
* Extra tags to attach to the call for filtering. Eg { \"userId\": \"123\", \"promptId\": \"populate-title\" }
* @type {{ [key: string]: string; }}
* @memberof ReportRequest
*/
'tags'?: { [key: string]: string; };
}
/**
* DefaultApi - axios parameter creator
* @export
*/
export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
* Check if a prompt is cached
* @param {CheckCacheRequest} checkCacheRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
checkCache: async (checkCacheRequest: CheckCacheRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'checkCacheRequest' is not null or undefined
assertParamExists('checkCache', 'checkCacheRequest', checkCacheRequest)
const localVarPath = `/check-cache`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication Authorization required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(checkCacheRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* Get the latest logged call (only for local testing)
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
localTestingOnlyGetLatestLoggedCall: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/local-testing-only-get-latest-logged-call`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication Authorization required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* Report an API call
* @param {ReportRequest} reportRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
report: async (reportRequest: ReportRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'reportRequest' is not null or undefined
assertParamExists('report', 'reportRequest', reportRequest)
const localVarPath = `/report`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication Authorization required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(reportRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
}
};
/**
* DefaultApi - functional programming interface
* @export
*/
export const DefaultApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration)
return {
/**
* Check if a prompt is cached
* @param {CheckCacheRequest} checkCacheRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async checkCache(checkCacheRequest: CheckCacheRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CheckCache200Response>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.checkCache(checkCacheRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
* Get the latest logged call (only for local testing)
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async localTestingOnlyGetLatestLoggedCall(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<LocalTestingOnlyGetLatestLoggedCall200Response>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.localTestingOnlyGetLatestLoggedCall(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
* Report an API call
* @param {ReportRequest} reportRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async report(reportRequest: ReportRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<any>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.report(reportRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
/**
* DefaultApi - factory interface
* @export
*/
export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = DefaultApiFp(configuration)
return {
/**
* Check if a prompt is cached
* @param {CheckCacheRequest} checkCacheRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
checkCache(checkCacheRequest: CheckCacheRequest, options?: any): AxiosPromise<CheckCache200Response> {
return localVarFp.checkCache(checkCacheRequest, options).then((request) => request(axios, basePath));
},
/**
* Get the latest logged call (only for local testing)
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
localTestingOnlyGetLatestLoggedCall(options?: any): AxiosPromise<LocalTestingOnlyGetLatestLoggedCall200Response> {
return localVarFp.localTestingOnlyGetLatestLoggedCall(options).then((request) => request(axios, basePath));
},
/**
* Report an API call
* @param {ReportRequest} reportRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
report(reportRequest: ReportRequest, options?: any): AxiosPromise<any> {
return localVarFp.report(reportRequest, options).then((request) => request(axios, basePath));
},
};
};
/**
* DefaultApi - object-oriented interface
* @export
* @class DefaultApi
* @extends {BaseAPI}
*/
export class DefaultApi extends BaseAPI {
/**
* Check if a prompt is cached
* @param {CheckCacheRequest} checkCacheRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DefaultApi
*/
public checkCache(checkCacheRequest: CheckCacheRequest, options?: AxiosRequestConfig) {
return DefaultApiFp(this.configuration).checkCache(checkCacheRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
* Get the latest logged call (only for local testing)
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DefaultApi
*/
public localTestingOnlyGetLatestLoggedCall(options?: AxiosRequestConfig) {
return DefaultApiFp(this.configuration).localTestingOnlyGetLatestLoggedCall(options).then((request) => request(this.axios, this.basePath));
}
/**
* Report an API call
* @param {ReportRequest} reportRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DefaultApi
*/
public report(reportRequest: ReportRequest, options?: AxiosRequestConfig) {
return DefaultApiFp(this.configuration).report(reportRequest, options).then((request) => request(this.axios, this.basePath));
}
}

View File

@@ -1,72 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* OpenPipe API
* The public API for reporting API calls to OpenPipe
*
* The version of the OpenAPI document: 0.1.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import type { Configuration } from './configuration';
// Some imports not used depending on template conditions
// @ts-ignore
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
import globalAxios from 'axios';
export const BASE_PATH = "https://app.openpipe.ai/api/v1".replace(/\/+$/, "");
/**
*
* @export
*/
export const COLLECTION_FORMATS = {
csv: ",",
ssv: " ",
tsv: "\t",
pipes: "|",
};
/**
*
* @export
* @interface RequestArgs
*/
export interface RequestArgs {
url: string;
options: AxiosRequestConfig;
}
/**
*
* @export
* @class BaseAPI
*/
export class BaseAPI {
protected configuration: Configuration | undefined;
constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) {
if (configuration) {
this.configuration = configuration;
this.basePath = configuration.basePath || this.basePath;
}
}
};
/**
*
* @export
* @class RequiredError
* @extends {Error}
*/
export class RequiredError extends Error {
constructor(public field: string, msg?: string) {
super(msg);
this.name = "RequiredError"
}
}

View File

@@ -1,150 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* OpenPipe API
* The public API for reporting API calls to OpenPipe
*
* The version of the OpenAPI document: 0.1.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import type { Configuration } from "./configuration";
import type { RequestArgs } from "./base";
import type { AxiosInstance, AxiosResponse } from 'axios';
import { RequiredError } from "./base";
/**
*
* @export
*/
export const DUMMY_BASE_URL = 'https://example.com'
/**
*
* @throws {RequiredError}
* @export
*/
export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) {
if (paramValue === null || paramValue === undefined) {
throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`);
}
}
/**
*
* @export
*/
export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) {
if (configuration && configuration.apiKey) {
const localVarApiKeyValue = typeof configuration.apiKey === 'function'
? await configuration.apiKey(keyParamName)
: await configuration.apiKey;
object[keyParamName] = localVarApiKeyValue;
}
}
/**
*
* @export
*/
export const setBasicAuthToObject = function (object: any, configuration?: Configuration) {
if (configuration && (configuration.username || configuration.password)) {
object["auth"] = { username: configuration.username, password: configuration.password };
}
}
/**
*
* @export
*/
export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) {
if (configuration && configuration.accessToken) {
const accessToken = typeof configuration.accessToken === 'function'
? await configuration.accessToken()
: await configuration.accessToken;
object["Authorization"] = "Bearer " + accessToken;
}
}
/**
*
* @export
*/
export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) {
if (configuration && configuration.accessToken) {
const localVarAccessTokenValue = typeof configuration.accessToken === 'function'
? await configuration.accessToken(name, scopes)
: await configuration.accessToken;
object["Authorization"] = "Bearer " + localVarAccessTokenValue;
}
}
function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void {
if (parameter == null) return;
if (typeof parameter === "object") {
if (Array.isArray(parameter)) {
(parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key));
}
else {
Object.keys(parameter).forEach(currentKey =>
setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`)
);
}
}
else {
if (urlSearchParams.has(key)) {
urlSearchParams.append(key, parameter);
}
else {
urlSearchParams.set(key, parameter);
}
}
}
/**
*
* @export
*/
export const setSearchParams = function (url: URL, ...objects: any[]) {
const searchParams = new URLSearchParams(url.search);
setFlattenedQueryParams(searchParams, objects);
url.search = searchParams.toString();
}
/**
*
* @export
*/
export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) {
const nonString = typeof value !== 'string';
const needsSerialization = nonString && configuration && configuration.isJsonMime
? configuration.isJsonMime(requestOptions.headers['Content-Type'])
: nonString;
return needsSerialization
? JSON.stringify(value !== undefined ? value : {})
: (value || "");
}
/**
*
* @export
*/
export const toPathString = function (url: URL) {
return url.pathname + url.search + url.hash
}
/**
*
* @export
*/
export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {
return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url};
return axios.request<T, R>(axiosRequestArgs);
};
}

View File

@@ -1,101 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* OpenPipe API
* The public API for reporting API calls to OpenPipe
*
* The version of the OpenAPI document: 0.1.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface ConfigurationParameters {
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
username?: string;
password?: string;
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
basePath?: string;
baseOptions?: any;
formDataCtor?: new () => any;
}
export class Configuration {
/**
* parameter for apiKey security
* @param name security name
* @memberof Configuration
*/
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
username?: string;
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
password?: string;
/**
* parameter for oauth2 security
* @param name security name
* @param scopes oauth2 scope
* @memberof Configuration
*/
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
/**
* override base path
*
* @type {string}
* @memberof Configuration
*/
basePath?: string;
/**
* base options for axios calls
*
* @type {any}
* @memberof Configuration
*/
baseOptions?: any;
/**
* The FormData constructor that will be used to create multipart form data
* requests. You can inject this here so that execution environments that
* do not support the FormData class can still run the generated client.
*
* @type {new () => FormData}
*/
formDataCtor?: new () => any;
constructor(param: ConfigurationParameters = {}) {
this.apiKey = param.apiKey;
this.username = param.username;
this.password = param.password;
this.accessToken = param.accessToken;
this.basePath = param.basePath;
this.baseOptions = param.baseOptions;
this.formDataCtor = param.formDataCtor;
}
/**
* Check if the given MIME is a JSON MIME.
* JSON MIME examples:
* application/json
* application/json; charset=UTF8
* APPLICATION/JSON
* application/vnd.company+json
* @param mime - MIME (Multipurpose Internet Mail Extensions)
* @return True if the given MIME is JSON, false otherwise.
*/
public isJsonMime(mime: string): boolean {
const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i');
return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json');
}
}

View File

@@ -0,0 +1,25 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
export class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: any;
public readonly request: ApiRequestOptions;
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
super(message);
this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
}

View File

@@ -0,0 +1,17 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ApiRequestOptions = {
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly url: string;
readonly path?: Record<string, any>;
readonly cookies?: Record<string, any>;
readonly headers?: Record<string, any>;
readonly query?: Record<string, any>;
readonly formData?: Record<string, any>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
};

View File

@@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ApiResult = {
readonly url: string;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly body: any;
};

View File

@@ -0,0 +1,14 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { CancelablePromise } from './CancelablePromise';
import type { OpenAPIConfig } from './OpenAPI';
export abstract class BaseHttpRequest {
constructor(public readonly config: OpenAPIConfig) {}
public abstract request<T>(options: ApiRequestOptions): CancelablePromise<T>;
}

View File

@@ -0,0 +1,131 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export class CancelError extends Error {
constructor(message: string) {
super(message);
this.name = 'CancelError';
}
public get isCancelled(): boolean {
return true;
}
}
export interface OnCancel {
readonly isResolved: boolean;
readonly isRejected: boolean;
readonly isCancelled: boolean;
(cancelHandler: () => void): void;
}
export class CancelablePromise<T> implements Promise<T> {
#isResolved: boolean;
#isRejected: boolean;
#isCancelled: boolean;
readonly #cancelHandlers: (() => void)[];
readonly #promise: Promise<T>;
#resolve?: (value: T | PromiseLike<T>) => void;
#reject?: (reason?: any) => void;
constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
onCancel: OnCancel
) => void
) {
this.#isResolved = false;
this.#isRejected = false;
this.#isCancelled = false;
this.#cancelHandlers = [];
this.#promise = new Promise<T>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isResolved = true;
this.#resolve?.(value);
};
const onReject = (reason?: any): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isRejected = true;
this.#reject?.(reason);
};
const onCancel = (cancelHandler: () => void): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#cancelHandlers.push(cancelHandler);
};
Object.defineProperty(onCancel, 'isResolved', {
get: (): boolean => this.#isResolved,
});
Object.defineProperty(onCancel, 'isRejected', {
get: (): boolean => this.#isRejected,
});
Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this.#isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
get [Symbol.toStringTag]() {
return "Cancellable Promise";
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2> {
return this.#promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<T | TResult> {
return this.#promise.catch(onRejected);
}
public finally(onFinally?: (() => void) | null): Promise<T> {
return this.#promise.finally(onFinally);
}
public cancel(): void {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isCancelled = true;
if (this.#cancelHandlers.length) {
try {
for (const cancelHandler of this.#cancelHandlers) {
cancelHandler();
}
} catch (error) {
console.warn('Cancellation threw an error', error);
return;
}
}
this.#cancelHandlers.length = 0;
this.#reject?.(new CancelError('Request aborted'));
}
public get isCancelled(): boolean {
return this.#isCancelled;
}
}

View File

@@ -0,0 +1,26 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
import { BaseHttpRequest } from './BaseHttpRequest';
import type { CancelablePromise } from './CancelablePromise';
import type { OpenAPIConfig } from './OpenAPI';
import { request as __request } from './request';
export class NodeHttpRequest extends BaseHttpRequest {
constructor(config: OpenAPIConfig) {
super(config);
}
/**
* Request method
* @param options The request options from the service
* @returns CancelablePromise<T>
* @throws ApiError
*/
public override request<T>(options: ApiRequestOptions): CancelablePromise<T> {
return __request(this.config, options);
}
}

View File

@@ -0,0 +1,32 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
type Headers = Record<string, string>;
export type OpenAPIConfig = {
BASE: string;
VERSION: string;
WITH_CREDENTIALS: boolean;
CREDENTIALS: 'include' | 'omit' | 'same-origin';
TOKEN?: string | Resolver<string> | undefined;
USERNAME?: string | Resolver<string> | undefined;
PASSWORD?: string | Resolver<string> | undefined;
HEADERS?: Headers | Resolver<Headers> | undefined;
ENCODE_PATH?: ((path: string) => string) | undefined;
};
export const OpenAPI: OpenAPIConfig = {
BASE: 'https://app.openpipe.ai/api/v1',
VERSION: '0.1.1',
WITH_CREDENTIALS: false,
CREDENTIALS: 'include',
TOKEN: undefined,
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};

View File

@@ -0,0 +1,341 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import FormData from "form-data";
import fetch, { Headers } from "node-fetch";
import type { RequestInit, Response } from "node-fetch";
// @ts-expect-error TODO maybe I need an older node-fetch or something?
import type { AbortSignal } from "node-fetch/externals";
import { ApiError } from "./ApiError";
import type { ApiRequestOptions } from "./ApiRequestOptions";
import type { ApiResult } from "./ApiResult";
import { CancelablePromise } from "./CancelablePromise";
import type { OnCancel } from "./CancelablePromise";
import type { OpenAPIConfig } from "./OpenAPI";
export const isDefined = <T>(
value: T | null | undefined
): value is Exclude<T, null | undefined> => {
return value !== undefined && value !== null;
};
export const isString = (value: any): value is string => {
return typeof value === "string";
};
export const isStringWithValue = (value: any): value is string => {
return isString(value) && value !== "";
};
export const isBlob = (value: any): value is Blob => {
return (
typeof value === "object" &&
typeof value.type === "string" &&
typeof value.stream === "function" &&
typeof value.arrayBuffer === "function" &&
typeof value.constructor === "function" &&
typeof value.constructor.name === "string" &&
/^(Blob|File)$/.test(value.constructor.name) &&
/^(Blob|File)$/.test(value[Symbol.toStringTag])
);
};
export const isFormData = (value: any): value is FormData => {
return value instanceof FormData;
};
export const base64 = (str: string): string => {
try {
return btoa(str);
} catch (err) {
// @ts-ignore
return Buffer.from(str).toString("base64");
}
};
export const getQueryString = (params: Record<string, any>): string => {
const qs: string[] = [];
const append = (key: string, value: any) => {
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
};
const process = (key: string, value: any) => {
if (isDefined(value)) {
if (Array.isArray(value)) {
value.forEach((v) => {
process(key, v);
});
} else if (typeof value === "object") {
Object.entries(value).forEach(([k, v]) => {
process(`${key}[${k}]`, v);
});
} else {
append(key, value);
}
}
};
Object.entries(params).forEach(([key, value]) => {
process(key, value);
});
if (qs.length > 0) {
return `?${qs.join("&")}`;
}
return "";
};
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
const encoder = config.ENCODE_PATH || encodeURI;
const path = options.url
.replace("{api-version}", config.VERSION)
.replace(/{(.*?)}/g, (substring: string, group: string) => {
if (options.path?.hasOwnProperty(group)) {
return encoder(String(options.path[group]));
}
return substring;
});
const url = `${config.BASE}${path}`;
if (options.query) {
return `${url}${getQueryString(options.query)}`;
}
return url;
};
export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
if (options.formData) {
const formData = new FormData();
const process = (key: string, value: any) => {
if (isString(value) || isBlob(value)) {
formData.append(key, value);
} else {
formData.append(key, JSON.stringify(value));
}
};
Object.entries(options.formData)
.filter(([_, value]) => isDefined(value))
.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((v) => process(key, v));
} else {
process(key, value);
}
});
return formData;
}
return undefined;
};
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
export const resolve = async <T>(
options: ApiRequestOptions,
resolver?: T | Resolver<T>
): Promise<T | undefined> => {
if (typeof resolver === "function") {
return (resolver as Resolver<T>)(options);
}
return resolver;
};
export const getHeaders = async (
config: OpenAPIConfig,
options: ApiRequestOptions
): Promise<Headers> => {
const token = await resolve(options, config.TOKEN);
const username = await resolve(options, config.USERNAME);
const password = await resolve(options, config.PASSWORD);
const additionalHeaders = await resolve(options, config.HEADERS);
const headers = Object.entries({
Accept: "application/json",
...additionalHeaders,
...options.headers,
})
.filter(([_, value]) => isDefined(value))
.reduce(
(headers, [key, value]) => ({
...headers,
[key]: String(value),
}),
{} as Record<string, string>
);
if (isStringWithValue(token)) {
headers["Authorization"] = `Bearer ${token}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers["Authorization"] = `Basic ${credentials}`;
}
if (options.body) {
if (options.mediaType) {
headers["Content-Type"] = options.mediaType;
} else if (isBlob(options.body)) {
headers["Content-Type"] = "application/octet-stream";
} else if (isString(options.body)) {
headers["Content-Type"] = "text/plain";
} else if (!isFormData(options.body)) {
headers["Content-Type"] = "application/json";
}
}
return new Headers(headers);
};
export const getRequestBody = (options: ApiRequestOptions): any => {
if (options.body !== undefined) {
if (options.mediaType?.includes("/json")) {
return JSON.stringify(options.body);
} else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) {
return options.body as any;
} else {
return JSON.stringify(options.body);
}
}
return undefined;
};
export const sendRequest = async (
options: ApiRequestOptions,
url: string,
body: any,
formData: FormData | undefined,
headers: Headers,
onCancel: OnCancel
): Promise<Response> => {
const controller = new AbortController();
const request: RequestInit = {
headers,
method: options.method,
body: body ?? formData,
signal: controller.signal as AbortSignal,
};
onCancel(() => controller.abort());
return await fetch(url, request);
};
export const getResponseHeader = (
response: Response,
responseHeader?: string
): string | undefined => {
if (responseHeader) {
const content = response.headers.get(responseHeader);
if (isString(content)) {
return content;
}
}
return undefined;
};
export const getResponseBody = async (response: Response): Promise<any> => {
if (response.status !== 204) {
try {
const contentType = response.headers.get("Content-Type");
if (contentType) {
const jsonTypes = ["application/json", "application/problem+json"];
const isJSON = jsonTypes.some((type) => contentType.toLowerCase().startsWith(type));
if (isJSON) {
return await response.json();
} else {
return await response.text();
}
}
} catch (error) {
console.error(error);
}
}
return undefined;
};
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
const errors: Record<number, string> = {
400: "Bad Request",
401: "Unauthorized",
403: "Forbidden",
404: "Not Found",
500: "Internal Server Error",
502: "Bad Gateway",
503: "Service Unavailable",
...options.errors,
};
const error = errors[result.status];
if (error) {
throw new ApiError(options, result, error);
}
if (!result.ok) {
const errorStatus = result.status ?? "unknown";
const errorStatusText = result.statusText ?? "unknown";
const errorBody = (() => {
try {
return JSON.stringify(result.body, null, 2);
} catch (e) {
return undefined;
}
})();
throw new ApiError(
options,
result,
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
);
}
};
/**
* Request method
* @param config The OpenAPI configuration object
* @param options The request options from the service
* @returns CancelablePromise<T>
* @throws ApiError
*/
export const request = <T>(
config: OpenAPIConfig,
options: ApiRequestOptions
): CancelablePromise<T> => {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(config, options);
if (!onCancel.isCancelled) {
const response = await sendRequest(options, url, body, formData, headers, onCancel);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader ?? responseBody,
};
catchErrorCodes(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}
});
};

View File

@@ -1,57 +0,0 @@
#!/bin/sh
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
#
# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com"
git_user_id=$1
git_repo_id=$2
release_note=$3
git_host=$4
if [ "$git_host" = "" ]; then
git_host="github.com"
echo "[INFO] No command line input provided. Set \$git_host to $git_host"
fi
if [ "$git_user_id" = "" ]; then
git_user_id="GIT_USER_ID"
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
fi
if [ "$git_repo_id" = "" ]; then
git_repo_id="GIT_REPO_ID"
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
fi
if [ "$release_note" = "" ]; then
release_note="Minor update"
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
fi
# Initialize the local directory as a Git repository
git init
# Adds the files in the local repository and stages them for commit.
git add .
# Commits the tracked changes and prepares them to be pushed to a remote repository.
git commit -m "$release_note"
# Sets the new remote
git_remote=$(git remote)
if [ "$git_remote" = "" ]; then # git remote not defined
if [ "$GIT_TOKEN" = "" ]; then
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git
else
git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git
fi
fi
git pull origin master
# Pushes (Forces) the changes in the local repository up to the remote repository
echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git"
git push origin master 2>&1 | grep -v 'To https'

View File

@@ -1,18 +1,13 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* OpenPipe API
* The public API for reporting API calls to OpenPipe
*
* The version of the OpenAPI document: 0.1.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export { OPClient } from './OPClient';
export { ApiError } from './core/ApiError';
export { BaseHttpRequest } from './core/BaseHttpRequest';
export { CancelablePromise, CancelError } from './core/CancelablePromise';
export { OpenAPI } from './core/OpenAPI';
export type { OpenAPIConfig } from './core/OpenAPI';
export * from "./api";
export * from "./configuration";
export { DefaultService } from './services/DefaultService';

View File

@@ -0,0 +1,118 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { CancelablePromise } from '../core/CancelablePromise';
import type { BaseHttpRequest } from '../core/BaseHttpRequest';
export class DefaultService {
constructor(public readonly httpRequest: BaseHttpRequest) {}
/**
* Check if a prompt is cached
* @param requestBody
* @returns any Successful response
* @throws ApiError
*/
public checkCache(
requestBody: {
/**
* Unix timestamp in milliseconds
*/
requestedAt: number;
/**
* JSON-encoded request payload
*/
reqPayload?: any;
/**
* Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }
*/
tags?: Record<string, string>;
},
): CancelablePromise<{
/**
* JSON-encoded response payload
*/
respPayload?: any;
}> {
return this.httpRequest.request({
method: 'POST',
url: '/check-cache',
body: requestBody,
mediaType: 'application/json',
});
}
/**
* Report an API call
* @param requestBody
* @returns any Successful response
* @throws ApiError
*/
public report(
requestBody: {
/**
* Unix timestamp in milliseconds
*/
requestedAt: number;
/**
* Unix timestamp in milliseconds
*/
receivedAt: number;
/**
* JSON-encoded request payload
*/
reqPayload?: any;
/**
* JSON-encoded response payload
*/
respPayload?: any;
/**
* HTTP status code of response
*/
statusCode?: number;
/**
* User-friendly error message
*/
errorMessage?: string;
/**
* Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }
*/
tags?: Record<string, string>;
},
): CancelablePromise<{
status: 'ok';
}> {
return this.httpRequest.request({
method: 'POST',
url: '/report',
body: requestBody,
mediaType: 'application/json',
});
}
/**
* Get the latest logged call (only for local testing)
* @returns any Successful response
* @throws ApiError
*/
public localTestingOnlyGetLatestLoggedCall(): CancelablePromise<{
createdAt: string;
cacheHit: boolean;
tags: Record<string, string | null>;
modelResponse: {
id: string;
statusCode: number | null;
errorMessage: string | null;
reqPayload?: any;
respPayload?: any;
} | null;
} | null> {
return this.httpRequest.request({
method: 'GET',
url: '/local-testing-only-get-latest-logged-call',
});
}
}

View File

@@ -1,90 +1,85 @@
// import * as openPipeClient from "../codegen";
// import * as openai from "openai-legacy";
// import { version } from "../package.json";
import * as openPipeClient from "../codegen";
import * as openai from "openai-legacy";
import { version } from "../../package.json";
// // Anything we don't override we want to pass through to openai directly
// export * as openAILegacy from "openai-legacy";
// Anything we don't override we want to pass through to openai directly
export * as openAILegacy from "openai-legacy";
// type OPConfigurationParameters = {
// apiKey?: string;
// basePath?: string;
// };
type OPConfigurationParameters = {
apiKey?: string;
basePath?: string;
};
// export class Configuration extends openai.Configuration {
// public qkConfig?: openPipeClient.Configuration;
export class Configuration extends openai.Configuration {
public qkConfig?: openPipeClient.Configuration;
// constructor(
// config: openai.ConfigurationParameters & {
// opParameters?: OPConfigurationParameters;
// }
// ) {
// super(config);
// if (config.opParameters) {
// this.qkConfig = new openPipeClient.Configuration(config.opParameters);
// }
// }
// }
constructor(
config: openai.ConfigurationParameters & {
opParameters?: OPConfigurationParameters;
}
) {
super(config);
if (config.opParameters) {
this.qkConfig = new openPipeClient.Configuration(config.opParameters);
}
}
}
// type CreateChatCompletion = InstanceType<
// typeof openai.OpenAIApi
// >["createChatCompletion"];
type CreateChatCompletion = InstanceType<typeof openai.OpenAIApi>["createChatCompletion"];
// export class OpenAIApi extends openai.OpenAIApi {
// public openPipeApi?: openPipeClient.DefaultApi;
export class OpenAIApi extends openai.OpenAIApi {
public openPipeApi?: openPipeClient.DefaultApi;
// constructor(config: Configuration) {
// super(config);
// if (config.qkConfig) {
// this.openPipeApi = new openPipeClient.DefaultApi(config.qkConfig);
// }
// }
constructor(config: Configuration) {
super(config);
if (config.qkConfig) {
this.openPipeApi = new openPipeClient.DefaultApi(config.qkConfig);
}
}
// public async createChatCompletion(
// createChatCompletionRequest: Parameters<CreateChatCompletion>[0],
// options?: Parameters<CreateChatCompletion>[1]
// ): ReturnType<CreateChatCompletion> {
// const requestedAt = Date.now();
// let resp: Awaited<ReturnType<CreateChatCompletion>> | null = null;
// let respPayload: openai.CreateChatCompletionResponse | null = null;
// let statusCode: number | undefined = undefined;
// let errorMessage: string | undefined;
// try {
// resp = await super.createChatCompletion(
// createChatCompletionRequest,
// options
// );
// respPayload = resp.data;
// statusCode = resp.status;
// } catch (err) {
// console.error("Error in createChatCompletion");
// if ("isAxiosError" in err && err.isAxiosError) {
// errorMessage = err.response?.data?.error?.message;
// respPayload = err.response?.data;
// statusCode = err.response?.status;
// } else if ("message" in err) {
// errorMessage = err.message.toString();
// }
// throw err;
// } finally {
// this.openPipeApi
// ?.externalApiReport({
// requestedAt,
// receivedAt: Date.now(),
// reqPayload: createChatCompletionRequest,
// respPayload: respPayload,
// statusCode: statusCode,
// errorMessage,
// tags: {
// client: "openai-js",
// clientVersion: version,
// },
// })
// .catch((err) => {
// console.error("Error reporting to OP", err);
// });
// }
public async createChatCompletion(
createChatCompletionRequest: Parameters<CreateChatCompletion>[0],
options?: Parameters<CreateChatCompletion>[1]
): ReturnType<CreateChatCompletion> {
const requestedAt = Date.now();
let resp: Awaited<ReturnType<CreateChatCompletion>> | null = null;
let respPayload: openai.CreateChatCompletionResponse | null = null;
let statusCode: number | undefined = undefined;
let errorMessage: string | undefined;
try {
resp = await super.createChatCompletion(createChatCompletionRequest, options);
respPayload = resp.data;
statusCode = resp.status;
} catch (err) {
console.error("Error in createChatCompletion");
if ("isAxiosError" in err && err.isAxiosError) {
errorMessage = err.response?.data?.error?.message;
respPayload = err.response?.data;
statusCode = err.response?.status;
} else if ("message" in err) {
errorMessage = err.message.toString();
}
throw err;
} finally {
this.openPipeApi
?.externalApiReport({
requestedAt,
receivedAt: Date.now(),
reqPayload: createChatCompletionRequest,
respPayload: respPayload,
statusCode: statusCode,
errorMessage,
tags: {
client: "openai-js",
clientVersion: version,
},
})
.catch((err) => {
console.error("Error reporting to OP", err);
});
}
// console.log("done");
// return resp;
// }
// }
console.log("done");
return resp;
}
}

View File

@@ -0,0 +1,126 @@
import dotenv from "dotenv";
import { expect, test } from "vitest";
import OpenAI from ".";
import {
CompletionCreateParams,
CreateChatCompletionRequestMessage,
} from "openai-beta/resources/chat/completions";
import { OPClient } from "../codegen";
dotenv.config({ path: "../.env" });
const oaiClient = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
openpipe: {
apiKey: process.env.OPENPIPE_API_KEY,
baseUrl: "http://localhost:3000/api/v1",
},
});
const opClient = new OPClient({
BASE: "http://localhost:3000/api/v1",
TOKEN: process.env.OPENPIPE_API_KEY,
});
const lastLoggedCall = async () => opClient.default.localTestingOnlyGetLatestLoggedCall();
test("basic call", async () => {
const payload: CompletionCreateParams = {
model: "gpt-3.5-turbo",
messages: [{ role: "system", content: "count to 3" }],
};
const completion = await oaiClient.chat.completions.create({
...payload,
openpipe: {
tags: { promptId: "test" },
},
});
await completion.openpipe.reportingFinished;
const lastLogged = await lastLoggedCall();
expect(lastLogged?.modelResponse?.reqPayload).toMatchObject(payload);
expect(completion).toMatchObject(lastLogged?.modelResponse?.respPayload);
expect(lastLogged?.tags).toMatchObject({ promptId: "test" });
});
const randomString = (length: number) => {
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return Array.from(
{ length },
() => characters[Math.floor(Math.random() * characters.length)]
).join("");
};
test.skip("streaming", async () => {
const completion = await oaiClient.chat.completions.create({
model: "gpt-3.5-turbo",
messages: [{ role: "system", content: "count to 4" }],
stream: true,
});
let merged = null;
for await (const chunk of completion) {
merged = merge_openai_chunks(merged, chunk);
}
const lastLogged = await lastLoggedCall();
expect(lastLogged?.modelResponse?.respPayload.choices[0].message.content).toBe(
merged.choices[0].message.content
);
});
test.skip("bad call streaming", async () => {
try {
await oaiClient.chat.completions.create({
model: "gpt-3.5-turbo-blaster",
messages: [{ role: "system", content: "count to 10" }],
stream: true,
});
} catch (e) {
const lastLogged = await lastLoggedCall();
expect(lastLogged?.modelResponse?.errorMessage).toBe(
"The model `gpt-3.5-turbo-blaster` does not exist"
);
expect(lastLogged?.modelResponse?.statusCode).toBe(404);
}
});
test("bad call", async () => {
try {
await oaiClient.chat.completions.create({
model: "gpt-3.5-turbo-booster",
messages: [{ role: "system", content: "count to 10" }],
});
} catch (e) {
const lastLogged = await lastLoggedCall();
expect(lastLogged?.modelResponse?.errorMessage).toBe(
"The model `gpt-3.5-turbo-booster` does not exist"
);
expect(lastLogged?.modelResponse?.statusCode).toBe(404);
}
});
test("caching", async () => {
const message: CreateChatCompletionRequestMessage = {
role: "system",
content: `repeat '${randomString(10)}'`,
};
const completion = await oaiClient.chat.completions.create({
model: "gpt-3.5-turbo",
messages: [message],
openpipe: { cache: true },
});
expect(completion.openpipe.cacheStatus).toBe("MISS");
await completion.openpipe.reportingFinished;
const firstLogged = await lastLoggedCall();
expect(completion.choices[0].message.content).toBe(
firstLogged?.modelResponse?.respPayload.choices[0].message.content
);
const completion2 = await oaiClient.chat.completions.create({
model: "gpt-3.5-turbo",
messages: [message],
openpipe: { cache: true },
});
expect(completion2.openpipe.cacheStatus).toBe("HIT");
});

View File

@@ -1,109 +1,154 @@
import * as openai from "openai-beta";
import * as Core from "openai-beta/core";
import { readEnv, type RequestOptions } from "openai-beta/core";
import { CompletionCreateParams } from "openai-beta/resources/chat/completions";
import axios from "axios";
import {
ChatCompletion,
ChatCompletionChunk,
CompletionCreateParams,
Completions,
} from "openai-beta/resources/chat/completions";
import * as openPipeClient from "../codegen";
interface ClientOptions extends openai.ClientOptions {
openPipeApiKey?: string;
openPipeBaseUrl?: string;
}
import { DefaultService, OPClient } from "../codegen";
import { Stream } from "openai-beta/streaming";
import { OpenPipeArgs, OpenPipeMeta, type OpenPipeConfig, getTags } from "../shared";
export type ClientOptions = openai.ClientOptions & { openpipe?: OpenPipeConfig };
export default class OpenAI extends openai.OpenAI {
public openPipeApi?: openPipeClient.DefaultApi;
public opClient?: OPClient;
constructor({
openPipeApiKey = readEnv("OPENPIPE_API_KEY"),
openPipeBaseUrl = readEnv("OPENPIPE_BASE_URL") ?? `https://app.openpipe.ai/v1`,
...opts
}: ClientOptions = {}) {
super({ ...opts });
constructor({ openpipe, ...options }: ClientOptions = {}) {
super({ ...options });
const openPipeApiKey = openpipe?.apiKey ?? readEnv("OPENPIPE_API_KEY");
if (openPipeApiKey) {
const axiosInstance = axios.create({
baseURL: openPipeBaseUrl,
headers: {
Authorization: `Bearer ${openPipeApiKey}`,
},
});
this.openPipeApi = new openPipeClient.DefaultApi(
new openPipeClient.Configuration({
apiKey: openPipeApiKey,
basePath: openPipeBaseUrl,
}),
undefined,
axiosInstance
this.chat.setClient(
new OPClient({
BASE:
openpipe?.baseUrl ?? readEnv("OPENPIPE_BASE_URL") ?? "https://app.openpipe.ai/api/v1",
TOKEN: openPipeApiKey,
})
);
}
// Override the chat property
this.chat = new ExtendedChat(this);
if (openPipeApiKey === undefined) {
console.error(
"The OPENPIPE_API_KEY environment variable is missing or empty; either provide it, or instantiate the OpenPipe client with an openPipeApiKey option, like new OpenPipe({ openPipeApiKey: undefined })."
} else {
console.warn(
"You're using the OpenPipe client without an API key. No completion requests will be logged."
);
}
}
chat: WrappedChat = new WrappedChat(this);
}
class ExtendedChat extends openai.OpenAI.Chat {
completions: ExtendedCompletions;
class WrappedChat extends openai.OpenAI.Chat {
setClient(client: OPClient) {
this.completions.opClient = client;
}
constructor(openaiInstance: OpenAI) {
super(openaiInstance);
// Initialize the new completions instance
this.completions = new ExtendedCompletions(openaiInstance);
completions: InstrumentedCompletions = new InstrumentedCompletions(this.client);
}
class InstrumentedCompletions extends openai.OpenAI.Chat.Completions {
opClient?: OPClient;
constructor(client: openai.OpenAI, opClient?: OPClient) {
super(client);
this.opClient = opClient;
}
_report(args: Parameters<DefaultService["report"]>[0]) {
try {
return this.opClient ? this.opClient.default.report(args) : Promise.resolve();
} catch (e) {
console.error(e);
return Promise.resolve();
}
}
class ExtendedCompletions extends openai.OpenAI.Chat.Completions {
private openaiInstance: OpenAI;
constructor(openaiInstance: OpenAI) {
super(openaiInstance);
this.openaiInstance = openaiInstance;
}
create(
body: CompletionCreateParams.CreateChatCompletionRequestNonStreaming & OpenPipeArgs,
options?: Core.RequestOptions
): Promise<Core.APIResponse<ChatCompletion & { openpipe: OpenPipeMeta }>>;
create(
body: CompletionCreateParams.CreateChatCompletionRequestStreaming & OpenPipeArgs,
options?: Core.RequestOptions
): Promise<Core.APIResponse<Stream<ChatCompletionChunk>>>;
async create(
params:
| CompletionCreateParams.CreateChatCompletionRequestNonStreaming
| CompletionCreateParams.CreateChatCompletionRequestStreaming,
options?: RequestOptions,
tags?: Record<string, string>
): Promise<any> {
// // Your pre API call logic here
// console.log("Doing pre API call...");
{ openpipe, ...body }: CompletionCreateParams & OpenPipeArgs,
options?: Core.RequestOptions
): Promise<
Core.APIResponse<(ChatCompletion & { openpipe: OpenPipeMeta }) | Stream<ChatCompletionChunk>>
> {
console.log("LALALA REPORT", this.opClient);
const requestedAt = Date.now();
const cacheRequested = openpipe?.cache ?? false;
// // Determine the type of request
// if (params.hasOwnProperty("stream") && params.stream === true) {
// const result = await super.create(
// params as CompletionCreateParams.CreateChatCompletionRequestStreaming,
// options
// );
// // Your post API call logic here
// console.log("Doing post API call for Streaming...");
// return result;
// } else {
// const requestedAt = Date.now();
const result = await super.create(
params as CompletionCreateParams.CreateChatCompletionRequestNonStreaming,
options
);
return result;
// await this.openaiInstance.openPipeApi?.externalApiReport({
// requestedAt,
// receivedAt: Date.now(),
// reqPayload: params,
// respPayload: result,
// statusCode: 200,
// errorMessage: undefined,
// tags,
// });
if (cacheRequested) {
try {
const cached = await this.opClient?.default
.checkCache({
requestedAt,
reqPayload: body,
tags: getTags(openpipe),
})
.then((res) => res.respPayload);
// console.log("GOT RESULT", result);
// return result;
// }
if (cached) {
return {
...cached,
openpipe: {
cacheStatus: "HIT",
reportingFinished: Promise.resolve(),
},
};
}
} catch (e) {
console.error(e);
}
}
let reportingFinished: OpenPipeMeta["reportingFinished"] = Promise.resolve();
try {
if (body.stream) {
const stream = await super.create(body, options);
// Do some logging of each chunk here
return stream;
} else {
const response = await super.create(body, options);
reportingFinished = this._report({
requestedAt,
receivedAt: Date.now(),
reqPayload: body,
respPayload: response,
statusCode: 200,
tags: getTags(openpipe),
});
return {
...response,
openpipe: {
cacheStatus: cacheRequested ? "MISS" : "SKIP",
reportingFinished,
},
};
}
} catch (error: unknown) {
if (error instanceof openai.APIError) {
const rawMessage = error.message as string | string[];
const message = Array.isArray(rawMessage) ? rawMessage.join(", ") : rawMessage;
reportingFinished = this._report({
requestedAt,
receivedAt: Date.now(),
reqPayload: body,
respPayload: error.error,
statusCode: error.status,
errorMessage: message,
tags: getTags(openpipe),
});
}
throw error;
}
}
}

View File

@@ -0,0 +1,42 @@
import { ChatCompletion, ChatCompletionChunk } from "openai-beta/resources/chat";
export default function mergeChunks(
base: ChatCompletion | null,
chunk: ChatCompletionChunk
): ChatCompletion {
if (base === null) {
return mergeChunks({ ...chunk, choices: [] }, chunk);
}
const choices = [...base.choices];
for (const choice of chunk.choices) {
const baseChoice = choices.find((c) => c.index === choice.index);
if (baseChoice) {
baseChoice.finish_reason = choice.finish_reason ?? baseChoice.finish_reason;
baseChoice.message = baseChoice.message ?? { role: "assistant" };
if (choice.delta?.content)
baseChoice.message.content =
((baseChoice.message.content as string) ?? "") + (choice.delta.content ?? "");
if (choice.delta?.function_call) {
const fnCall = baseChoice.message.function_call ?? {};
fnCall.name =
((fnCall.name as string) ?? "") + ((choice.delta.function_call.name as string) ?? "");
fnCall.arguments =
((fnCall.arguments as string) ?? "") +
((choice.delta.function_call.arguments as string) ?? "");
}
} else {
// @ts-expect-error the types are correctly telling us that finish_reason
// could be null, but don't want to fix it right now.
choices.push({ ...omit(choice, "delta"), message: { role: "assistant", ...choice.delta } });
}
}
const merged: ChatCompletion = {
...base,
choices,
};
return merged;
}

View File

@@ -0,0 +1,26 @@
import pkg from "../package.json";
export type OpenPipeConfig = {
apiKey?: string;
baseUrl?: string;
};
export type OpenPipeArgs = {
openpipe?: { cache?: boolean; tags?: Record<string, string> };
};
export type OpenPipeMeta = {
cacheStatus: "HIT" | "MISS" | "SKIP";
// We report your call to OpenPipe asynchronously in the background. If you
// need to wait until the report is sent to take further action, you can await
// this promise.
reportingFinished: Promise<void | { status: "ok" }>;
};
export const getTags = (args: OpenPipeArgs["openpipe"]): Record<string, string> => ({
...args?.tags,
...(args?.cache ? { $cache: args.cache?.toString() } : {}),
$sdk: "typescript",
"$sdk.version": pkg.version,
});

View File

@@ -15,10 +15,7 @@
"incremental": true,
"noUncheckedIndexedAccess": true,
"baseUrl": ".",
"outDir": "dist",
"paths": {
"~/*": ["./src/*"]
}
"outDir": "dist"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]

162
pnpm-lock.yaml generated
View File

@@ -321,6 +321,9 @@ importers:
openapi-typescript:
specifier: ^6.3.4
version: 6.3.4
openapi-typescript-codegen:
specifier: ^0.25.0
version: 0.25.0
prisma:
specifier: ^4.14.0
version: 4.14.0
@@ -339,9 +342,12 @@ importers:
client-libs/typescript:
dependencies:
axios:
specifier: ^0.26.0
version: 0.26.0
form-data:
specifier: ^4.0.0
version: 4.0.0
node-fetch:
specifier: ^3.3.2
version: 3.3.2
openai-beta:
specifier: npm:openai@4.0.0-beta.7
version: /openai@4.0.0-beta.7
@@ -361,6 +367,9 @@ importers:
typescript:
specifier: ^5.0.4
version: 5.0.4
vitest:
specifier: ^0.33.0
version: 0.33.0
packages:
@@ -402,6 +411,15 @@ packages:
lodash.clonedeep: 4.5.0
dev: false
/@apidevtools/json-schema-ref-parser@9.0.9:
resolution: {integrity: sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w==}
dependencies:
'@jsdevtools/ono': 7.1.3
'@types/json-schema': 7.0.12
call-me-maybe: 1.0.2
js-yaml: 4.1.0
dev: true
/@babel/code-frame@7.22.10:
resolution: {integrity: sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==}
engines: {node: '>=6.9.0'}
@@ -2398,7 +2416,6 @@ packages:
/@jsdevtools/ono@7.1.3:
resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
dev: false
/@monaco-editor/loader@1.3.3(monaco-editor@0.40.0):
resolution: {integrity: sha512-6KKF4CTzcJiS8BJwtxtfyYt9shBiEv32ateQ9T4UVogwn4HM/uPo9iJd2Dmbkpz8CM6Y0PDUpjnZzCwC+eYo2Q==}
@@ -2958,7 +2975,7 @@ packages:
/@types/connect@3.4.35:
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
dependencies:
'@types/node': 18.16.0
'@types/node': 20.4.10
dev: true
/@types/cookie@0.4.1:
@@ -3062,7 +3079,7 @@ packages:
resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 18.16.0
'@types/node': 20.4.10
dev: false
/@types/hast@2.3.5:
@@ -3122,7 +3139,7 @@ packages:
/@types/node-fetch@2.6.4:
resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==}
dependencies:
'@types/node': 18.16.0
'@types/node': 20.4.10
form-data: 3.0.1
dev: false
@@ -3206,7 +3223,7 @@ packages:
resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==}
dependencies:
'@types/mime': 1.3.2
'@types/node': 18.16.0
'@types/node': 20.4.10
dev: true
/@types/serve-static@1.15.2:
@@ -3933,12 +3950,16 @@ packages:
/call-me-maybe@1.0.2:
resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==}
dev: false
/callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
/camelcase@6.3.0:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
dev: true
/camelize@1.0.1:
resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==}
dev: false
@@ -4116,6 +4137,11 @@ packages:
resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==}
dev: false
/commander@11.0.0:
resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==}
engines: {node: '>=16'}
dev: true
/commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
@@ -4379,6 +4405,11 @@ packages:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
dev: true
/data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
dev: false
/date-fns@2.30.0:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
engines: {node: '>=0.11'}
@@ -4614,7 +4645,7 @@ packages:
dependencies:
'@types/cookie': 0.4.1
'@types/cors': 2.8.13
'@types/node': 18.16.0
'@types/node': 20.4.10
accepts: 1.3.8
base64id: 2.0.0
cookie: 0.4.2
@@ -5250,6 +5281,14 @@ packages:
format: 0.2.2
dev: false
/fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.2.1
dev: false
/fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
dev: false
@@ -5367,6 +5406,13 @@ packages:
web-streams-polyfill: 4.0.0-beta.3
dev: false
/formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
dependencies:
fetch-blob: 3.2.0
dev: false
/forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
@@ -5401,6 +5447,15 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/fs-extra@11.1.1:
resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==}
engines: {node: '>=14.14'}
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.1.0
universalify: 2.0.0
dev: true
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
@@ -5620,6 +5675,19 @@ packages:
uncrypto: 0.1.3
dev: false
/handlebars@4.7.8:
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
engines: {node: '>=0.4.7'}
hasBin: true
dependencies:
minimist: 1.2.8
neo-async: 2.6.2
source-map: 0.6.1
wordwrap: 1.0.0
optionalDependencies:
uglify-js: 3.17.4
dev: true
/has-bigints@1.0.2:
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
dev: true
@@ -6014,7 +6082,7 @@ packages:
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
engines: {node: '>= 10.13.0'}
dependencies:
'@types/node': 20.4.10
'@types/node': 18.16.0
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -6049,6 +6117,14 @@ packages:
/json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
/json-schema-ref-parser@9.0.9:
resolution: {integrity: sha512-qcP2lmGy+JUoQJ4DOQeLaZDqH9qSkeGCK3suKWxJXS82dg728Mn3j97azDMaOUmJAN4uCq91LdPx4K7E8F1a7Q==}
engines: {node: '>=10'}
deprecated: Please switch to @apidevtools/json-schema-ref-parser
dependencies:
'@apidevtools/json-schema-ref-parser': 9.0.9
dev: true
/json-schema-to-typescript@13.0.2:
resolution: {integrity: sha512-TCaEVW4aI2FmMQe7f98mvr3/oiVmXEC1xZjkTZ9L/BSoTXFlC7p64mD5AD2d8XWycNBQZUnHwXL5iVXt1HWwNQ==}
engines: {node: '>=12.0.0'}
@@ -6097,6 +6173,14 @@ packages:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
dev: true
/jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
dependencies:
universalify: 2.0.0
optionalDependencies:
graceful-fs: 4.2.11
dev: true
/jsonschema@1.4.1:
resolution: {integrity: sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==}
dev: false
@@ -6558,6 +6642,15 @@ packages:
whatwg-url: 5.0.0
dev: false
/node-fetch@3.3.2:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
data-uri-to-buffer: 4.0.1
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
dev: false
/node-mocks-http@1.12.2:
resolution: {integrity: sha512-xhWwC0dh35R9rf0j3bRZXuISXdHxxtMx0ywZQBwjrg3yl7KpRETzogfeCamUIjltpn0Fxvs/ZhGJul1vPLrdJQ==}
engines: {node: '>=0.6'}
@@ -6715,6 +6808,17 @@ packages:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
dev: false
/openapi-typescript-codegen@0.25.0:
resolution: {integrity: sha512-nN/TnIcGbP58qYgwEEy5FrAAjePcYgfMaCe3tsmYyTgI3v4RR9v8os14L+LEWDvV50+CmqiyTzRkKKtJeb6Ybg==}
hasBin: true
dependencies:
camelcase: 6.3.0
commander: 11.0.0
fs-extra: 11.1.1
handlebars: 4.7.8
json-schema-ref-parser: 9.0.9
dev: true
/openapi-typescript@5.4.1:
resolution: {integrity: sha512-AGB2QiZPz4rE7zIwV3dRHtoUC/CWHhUjuzGXvtmMQN2AFV8xCTLKcZUHLcdPQmt/83i22nRE7+TxXOXkK+gf4Q==}
engines: {node: '>= 14.0.0'}
@@ -8355,6 +8459,14 @@ packages:
/ufo@1.2.0:
resolution: {integrity: sha512-RsPyTbqORDNDxqAdQPQBpgqhWle1VcTSou/FraClYlHf6TZnQcGslpLcAphNR+sQW4q5lLWLbOsRlh9j24baQg==}
/uglify-js@3.17.4:
resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==}
engines: {node: '>=0.8.0'}
hasBin: true
requiresBuild: true
dev: true
optional: true
/unbox-primitive@1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
dependencies:
@@ -8382,6 +8494,11 @@ packages:
tiny-inflate: 1.0.3
dev: false
/universalify@2.0.0:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 10.0.0'}
dev: true
/unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
@@ -8547,7 +8664,7 @@ packages:
d3-timer: 3.0.1
dev: false
/vite-node@0.33.0(@types/node@18.16.0):
/vite-node@0.33.0(@types/node@20.4.10):
resolution: {integrity: sha512-19FpHYbwWWxDr73ruNahC+vtEdza52kA90Qb3La98yZ0xULqV8A5JLNPUff0f5zID4984tW7l3DH2przTJUZSw==}
engines: {node: '>=v14.18.0'}
hasBin: true
@@ -8557,7 +8674,7 @@ packages:
mlly: 1.4.0
pathe: 1.1.1
picocolors: 1.0.0
vite: 4.4.9(@types/node@18.16.0)
vite: 4.4.9(@types/node@20.4.10)
transitivePeerDependencies:
- '@types/node'
- less
@@ -8585,7 +8702,7 @@ packages:
- typescript
dev: false
/vite@4.4.9(@types/node@18.16.0):
/vite@4.4.9(@types/node@20.4.10):
resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
@@ -8613,7 +8730,7 @@ packages:
terser:
optional: true
dependencies:
'@types/node': 18.16.0
'@types/node': 20.4.10
esbuild: 0.18.20
postcss: 8.4.27
rollup: 3.28.0
@@ -8654,7 +8771,7 @@ packages:
dependencies:
'@types/chai': 4.3.5
'@types/chai-subset': 1.3.3
'@types/node': 18.16.0
'@types/node': 20.4.10
'@vitest/expect': 0.33.0
'@vitest/runner': 0.33.0
'@vitest/snapshot': 0.33.0
@@ -8673,8 +8790,8 @@ packages:
strip-literal: 1.3.0
tinybench: 2.5.0
tinypool: 0.6.0
vite: 4.4.9(@types/node@18.16.0)
vite-node: 0.33.0(@types/node@18.16.0)
vite: 4.4.9(@types/node@20.4.10)
vite-node: 0.33.0(@types/node@20.4.10)
why-is-node-running: 2.2.2
transitivePeerDependencies:
- less
@@ -8693,6 +8810,11 @@ packages:
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
/web-streams-polyfill@3.2.1:
resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==}
engines: {node: '>= 8'}
dev: false
/web-streams-polyfill@4.0.0-beta.3:
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
engines: {node: '>= 14'}
@@ -8788,6 +8910,10 @@ packages:
stackback: 0.0.2
dev: true
/wordwrap@1.0.0:
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
dev: true
/wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}