Compare commits

..

11 Commits

Author SHA1 Message Date
Kyle Corbitt
8f4e7f7e2e 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.
2023-08-14 23:22:27 -07:00
Kyle Corbitt
5da62fdc29 Merge pull request #156 from OpenPipe/move-api
Python package improvements
2023-08-14 19:45:14 -07:00
Kyle Corbitt
754e273049 Python package improvements
Added an endpoint for getting the actual stored responses, and used it to test and improve the python package.
2023-08-14 19:07:03 -07:00
Kyle Corbitt
2863dc2f89 Merge pull request #155 from OpenPipe/move-api
Move the external API into its own router
2023-08-14 17:02:34 -07:00
Kyle Corbitt
c4cef35717 Move the external API into its own router
Auth logic isn't shared between the clients anyway, so co-locating them is confusing since you can't use the same clients to call both. This also makes the codegen clients less verbose.
2023-08-14 16:56:50 -07:00
Kyle Corbitt
8552baf632 Merge pull request #154 from OpenPipe/broken-page
Cap the number of waiting messages we try to render
2023-08-14 15:47:48 -07:00
Kyle Corbitt
f41e2229ca Cap the number of waiting messages we try to render
If an cell was attempted several hours ago and never resolved, it crashes the UI because we try to render thousands of log messages once a second (eg. https://app.openpipe.ai/experiments/372d0827-186e-4a7d-a8a6-1bf7050eb5fd) We should probably have a different UI for cells that have hung for a long time to let you know you should just retry, but this quick fix should work for now.
2023-08-14 15:44:03 -07:00
arcticfly
e649f42c9c Await completions (#153)
* Continue polling stats while waiting for completions to finish

* Clarify convert to function call instructions
2023-08-14 13:03:48 -07:00
Kyle Corbitt
99f305483b Merge pull request #150 from OpenPipe/fix-build
(Probably) fixes the build
2023-08-14 07:59:20 -07:00
arcticfly
b28f4cad57 Remove scenarios header from output table card (#151) 2023-08-13 03:26:58 -07:00
Kyle Corbitt
df4a3a0950 (Probably) fixes the build
This probably fixes the build that I broke in https://github.com/OpenPipe/OpenPipe/pull/149. However, there's a small chance that it fixes it enough to deploy, but not enough to actually work. That would be bad, so not merging until I have time to monitor the deploy.
2023-08-12 23:50:31 -07:00
76 changed files with 2513 additions and 1375 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
**/node_modules/
.git
**/.venv/
**/.env*
**/.next/

View File

@@ -32,5 +32,5 @@ NEXT_PUBLIC_HOST="http://localhost:3000"
GITHUB_CLIENT_ID="your_client_id" GITHUB_CLIENT_ID="your_client_id"
GITHUB_CLIENT_SECRET="your_secret" GITHUB_CLIENT_SECRET="your_secret"
OPENPIPE_BASE_URL="http://localhost:3000/api" OPENPIPE_BASE_URL="http://localhost:3000/api/v1"
OPENPIPE_API_KEY="your_key" OPENPIPE_API_KEY="your_key"

View File

@@ -12,12 +12,11 @@ declare module "nextjs-routes" {
export type Route = export type Route =
| StaticRoute<"/account/signin"> | StaticRoute<"/account/signin">
| DynamicRoute<"/api/[...trpc]", { "trpc": string[] }>
| DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }> | DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }>
| StaticRoute<"/api/experiments/og-image"> | StaticRoute<"/api/experiments/og-image">
| StaticRoute<"/api/openapi">
| StaticRoute<"/api/sentry-example-api">
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }> | DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
| DynamicRoute<"/api/v1/[...trpc]", { "trpc": string[] }>
| StaticRoute<"/api/v1/openapi">
| StaticRoute<"/dashboard"> | StaticRoute<"/dashboard">
| DynamicRoute<"/data/[id]", { "id": string }> | DynamicRoute<"/data/[id]", { "id": string }>
| StaticRoute<"/data"> | StaticRoute<"/data">

View File

@@ -6,13 +6,13 @@ RUN yarn global add pnpm
# DEPS # DEPS
FROM base as deps FROM base as deps
WORKDIR /app WORKDIR /code
COPY prisma ./ COPY app/prisma app/package.json ./app/
COPY client-libs/typescript/package.json ./client-libs/typescript/
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
COPY package.json pnpm-lock.yaml ./ RUN cd app && pnpm install --frozen-lockfile
RUN pnpm install --frozen-lockfile
# BUILDER # BUILDER
FROM base as builder FROM base as builder
@@ -25,22 +25,24 @@ ARG NEXT_PUBLIC_SENTRY_DSN
ARG SENTRY_AUTH_TOKEN ARG SENTRY_AUTH_TOKEN
ARG NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS ARG NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS
WORKDIR /app WORKDIR /code
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /code/node_modules ./node_modules
COPY --from=deps /code/app/node_modules ./app/node_modules
COPY --from=deps /code/client-libs/typescript/node_modules ./client-libs/typescript/node_modules
COPY . . COPY . .
RUN SKIP_ENV_VALIDATION=1 pnpm build RUN cd app && SKIP_ENV_VALIDATION=1 pnpm build
# RUNNER # RUNNER
FROM base as runner FROM base as runner
WORKDIR /app WORKDIR /code/app
ENV NODE_ENV production ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED 1
COPY --from=builder /app/ ./ COPY --from=builder /code/ /code/
EXPOSE 3000 EXPOSE 3000
ENV PORT 3000 ENV PORT 3000
# Run the "run-prod.sh" script # Run the "run-prod.sh" script
CMD /app/run-prod.sh CMD /code/app/run-prod.sh

View File

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

View File

@@ -112,17 +112,17 @@ model ScenarioVariantCell {
model ModelResponse { model ModelResponse {
id String @id @default(uuid()) @db.Uuid id String @id @default(uuid()) @db.Uuid
cacheKey String cacheKey String
requestedAt DateTime? requestedAt DateTime?
receivedAt DateTime? receivedAt DateTime?
respPayload Json? respPayload Json?
cost Float? cost Float?
inputTokens Int? inputTokens Int?
outputTokens Int? outputTokens Int?
statusCode Int? statusCode Int?
errorMessage String? errorMessage String?
retryTime DateTime? retryTime DateTime?
outdated Boolean @default(false) outdated Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -273,8 +273,8 @@ model LoggedCall {
projectId String @db.Uuid projectId String @db.Uuid
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade) project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
model String? model String?
tags LoggedCallTag[] tags LoggedCallTag[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -295,7 +295,7 @@ model LoggedCallModelResponse {
errorMessage String? errorMessage String?
requestedAt DateTime requestedAt DateTime
receivedAt DateTime receivedAt DateTime
// Note: the function to calculate the cacheKey should include the project // Note: the function to calculate the cacheKey should include the project
// ID so we don't share cached responses between projects, which could be an // ID so we don't share cached responses between projects, which could be an
@@ -340,8 +340,8 @@ model ApiKey {
name String name String
apiKey String @unique apiKey String @unique
projectId String @db.Uuid projectId String @db.Uuid
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

View File

@@ -2,6 +2,7 @@ import { prisma } from "~/server/db";
import dedent from "dedent"; import dedent from "dedent";
import { generateNewCell } from "~/server/utils/generateNewCell"; import { generateNewCell } from "~/server/utils/generateNewCell";
import { promptConstructorVersion } from "~/promptConstructor/version"; import { promptConstructorVersion } from "~/promptConstructor/version";
import { env } from "~/env.mjs";
const defaultId = "11111111-1111-1111-1111-111111111111"; const defaultId = "11111111-1111-1111-1111-111111111111";
@@ -16,6 +17,16 @@ const project =
data: { id: defaultId }, data: { id: defaultId },
})); }));
if (env.OPENPIPE_API_KEY) {
await prisma.apiKey.create({
data: {
projectId: project.id,
name: "Default API Key",
apiKey: env.OPENPIPE_API_KEY,
},
});
}
await prisma.experiment.deleteMany({ await prisma.experiment.deleteMany({
where: { where: {
id: defaultId, id: defaultId,

View File

@@ -33,7 +33,7 @@ export default function OutputCell({
if (!templateHasVariables) disabledReason = "Add a value to the scenario variables to see output"; if (!templateHasVariables) disabledReason = "Add a value to the scenario variables to see output";
const [refetchInterval, setRefetchInterval] = useState(0); const [refetchInterval, setRefetchInterval] = useState<number | false>(false);
const { data: cell, isLoading: queryLoading } = api.scenarioVariantCells.get.useQuery( const { data: cell, isLoading: queryLoading } = api.scenarioVariantCells.get.useQuery(
{ scenarioId: scenario.id, variantId: variant.id }, { scenarioId: scenario.id, variantId: variant.id },
{ refetchInterval }, { refetchInterval },
@@ -64,7 +64,8 @@ export default function OutputCell({
cell.retrievalStatus === "PENDING" || cell.retrievalStatus === "PENDING" ||
cell.retrievalStatus === "IN_PROGRESS" || cell.retrievalStatus === "IN_PROGRESS" ||
hardRefetching; hardRefetching;
useEffect(() => setRefetchInterval(awaitingOutput ? 1000 : 0), [awaitingOutput]);
useEffect(() => setRefetchInterval(awaitingOutput ? 1000 : false), [awaitingOutput]);
// TODO: disconnect from socket if we're not streaming anymore // TODO: disconnect from socket if we're not streaming anymore
const streamedMessage = useSocket<OutputSchema>(cell?.id); const streamedMessage = useSocket<OutputSchema>(cell?.id);
@@ -120,8 +121,13 @@ export default function OutputCell({
? response.receivedAt.getTime() ? response.receivedAt.getTime()
: Date.now(); : Date.now();
if (response.requestedAt) { if (response.requestedAt) {
numWaitingMessages = Math.floor( numWaitingMessages = Math.min(
(relativeWaitingTime - response.requestedAt.getTime()) / WAITING_MESSAGE_INTERVAL, Math.floor(
(relativeWaitingTime - response.requestedAt.getTime()) / WAITING_MESSAGE_INTERVAL,
),
// Don't try to render more than 15, it'll use too much CPU and
// break the page
15,
); );
} }
return ( return (

View File

@@ -21,14 +21,18 @@ export default function VariantStats(props: { variant: PromptVariant }) {
outputTokens: 0, outputTokens: 0,
scenarioCount: 0, scenarioCount: 0,
outputCount: 0, outputCount: 0,
awaitingCompletions: false,
awaitingEvals: false, awaitingEvals: false,
}, },
refetchInterval, refetchInterval,
}, },
); );
// Poll every two seconds while we are waiting for LLM retrievals to finish // Poll every five seconds while we are waiting for LLM retrievals to finish
useEffect(() => setRefetchInterval(data.awaitingEvals ? 5000 : 0), [data.awaitingEvals]); useEffect(
() => setRefetchInterval(data.awaitingCompletions || data.awaitingEvals ? 5000 : 0),
[data.awaitingCompletions, data.awaitingEvals],
);
const [passColor, neutralColor, failColor] = useToken("colors", [ const [passColor, neutralColor, failColor] = useToken("colors", [
"green.500", "green.500",

View File

@@ -1,54 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
import { import { isArray, isString } from "lodash-es";
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 { APIError } from "openai"; import { APIError } from "openai";
import { type ChatCompletion, type CompletionCreateParams } from "openai/resources/chat";
const mergeStreamedChunks = ( import mergeChunks from "openpipe/src/openai/mergeChunks";
base: ChatCompletion | null, import { openai } from "~/server/utils/openai";
chunk: ChatCompletionChunk, import { type CompletionResponse } from "../types";
): 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;
};
export async function getCompletion( export async function getCompletion(
input: CompletionCreateParams, input: CompletionCreateParams,
@@ -59,7 +15,6 @@ export async function getCompletion(
try { try {
if (onStream) { if (onStream) {
console.log("got started");
const resp = await openai.chat.completions.create( const resp = await openai.chat.completions.create(
{ ...input, stream: true }, { ...input, stream: true },
{ {
@@ -67,11 +22,9 @@ export async function getCompletion(
}, },
); );
for await (const part of resp) { for await (const part of resp) {
console.log("got part", part); finalCompletion = mergeChunks(finalCompletion, part);
finalCompletion = mergeStreamedChunks(finalCompletion, part);
onStream(finalCompletion); onStream(finalCompletion);
} }
console.log("got final", finalCompletion);
if (!finalCompletion) { if (!finalCompletion) {
return { return {
type: "error", type: "error",

View File

@@ -120,9 +120,9 @@ export const refinementActions: Record<string, RefinementAction> = {
"Convert to function call": { "Convert to function call": {
icon: TfiThought, icon: TfiThought,
description: "Use function calls to get output from the model in a more structured way.", description: "Use function calls to get output from the model in a more structured way.",
instructions: `OpenAI functions are a specialized way for an LLM to return output. instructions: `OpenAI functions are a specialized way for an LLM to return its final output.
This is what a prompt looks like before adding a function: Example 1 before:
definePrompt("openai/ChatCompletion", { definePrompt("openai/ChatCompletion", {
model: "gpt-4", model: "gpt-4",
@@ -139,7 +139,7 @@ export const refinementActions: Record<string, RefinementAction> = {
], ],
}); });
This is what one looks like after adding a function: Example 1 after:
definePrompt("openai/ChatCompletion", { definePrompt("openai/ChatCompletion", {
model: "gpt-4", model: "gpt-4",
@@ -156,7 +156,7 @@ export const refinementActions: Record<string, RefinementAction> = {
], ],
functions: [ functions: [
{ {
name: "extract_sentiment", name: "log_extracted_sentiment",
parameters: { parameters: {
type: "object", // parameters must always be an object with a properties key type: "object", // parameters must always be an object with a properties key
properties: { // properties key is required properties: { // properties key is required
@@ -169,13 +169,13 @@ export const refinementActions: Record<string, RefinementAction> = {
}, },
], ],
function_call: { function_call: {
name: "extract_sentiment", name: "log_extracted_sentiment",
}, },
}); });
Here's another example of adding a function: =========
Before: Example 2 before:
definePrompt("openai/ChatCompletion", { definePrompt("openai/ChatCompletion", {
model: "gpt-3.5-turbo", model: "gpt-3.5-turbo",
@@ -197,7 +197,7 @@ export const refinementActions: Record<string, RefinementAction> = {
temperature: 0, temperature: 0,
}); });
After: Example 2 after:
definePrompt("openai/ChatCompletion", { definePrompt("openai/ChatCompletion", {
model: "gpt-3.5-turbo", model: "gpt-3.5-turbo",
@@ -215,7 +215,7 @@ export const refinementActions: Record<string, RefinementAction> = {
temperature: 0, temperature: 0,
functions: [ functions: [
{ {
name: "score_post", name: "log_post_score",
parameters: { parameters: {
type: "object", type: "object",
properties: { properties: {
@@ -227,13 +227,13 @@ export const refinementActions: Record<string, RefinementAction> = {
}, },
], ],
function_call: { function_call: {
name: "score_post", name: "log_post_score",
}, },
}); });
Another example =========
Before: Example 3 before:
definePrompt("openai/ChatCompletion", { definePrompt("openai/ChatCompletion", {
model: "gpt-3.5-turbo", model: "gpt-3.5-turbo",
@@ -246,7 +246,7 @@ export const refinementActions: Record<string, RefinementAction> = {
], ],
}); });
After: Example 3 after:
definePrompt("openai/ChatCompletion", { definePrompt("openai/ChatCompletion", {
model: "gpt-3.5-turbo", model: "gpt-3.5-turbo",
@@ -258,22 +258,25 @@ export const refinementActions: Record<string, RefinementAction> = {
], ],
functions: [ functions: [
{ {
name: "write_in_language", name: "log_translated_text",
parameters: { parameters: {
type: "object", type: "object",
properties: { properties: {
text: { translated_text: {
type: "string", type: "string",
description: "The text, written in the language specified in the prompt",
}, },
}, },
}, },
}, },
], ],
function_call: { function_call: {
name: "write_in_language", name: "log_translated_text",
}, },
}); });
=========
Add an OpenAI function that takes one or more nested parameters that match the expected output from this prompt.`, Add an OpenAI function that takes one or more nested parameters that match the expected output from this prompt.`,
}, },
}; };

View File

@@ -1,6 +0,0 @@
// A faulty API route to test Sentry's error monitoring
// @ts-expect-error just a test file, don't care about types
export default function handler(_req, res) {
throw new Error("Sentry Example API Route Error");
res.status(200).json({ name: "John Doe" });
}

View File

@@ -1,17 +1,14 @@
import { type NextApiRequest, type NextApiResponse } from "next"; import { type NextApiRequest, type NextApiResponse } from "next";
import cors from "nextjs-cors"; import cors from "nextjs-cors";
import { createOpenApiNextHandler } from "trpc-openapi"; import { createOpenApiNextHandler } from "trpc-openapi";
import { createProcedureCache } from "trpc-openapi/dist/adapters/node-http/procedures"; import { v1ApiRouter } from "~/server/api/external/v1Api.router";
import { appRouter } from "~/server/api/root.router"; import { createOpenApiContext } from "~/server/api/external/openApiTrpc";
import { createTRPCContext } from "~/server/api/trpc";
const openApiHandler = createOpenApiNextHandler({ const openApiHandler = createOpenApiNextHandler({
router: appRouter, router: v1ApiRouter,
createContext: createTRPCContext, createContext: createOpenApiContext,
}); });
const cache = createProcedureCache(appRouter);
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Setup CORS // Setup CORS
await cors(req, res); await cors(req, res);

View File

@@ -1,12 +1,12 @@
import { type NextApiRequest, type NextApiResponse } from "next"; import { type NextApiRequest, type NextApiResponse } from "next";
import { generateOpenApiDocument } from "trpc-openapi"; import { generateOpenApiDocument } from "trpc-openapi";
import { appRouter } from "~/server/api/root.router"; import { v1ApiRouter } from "~/server/api/external/v1Api.router";
export const openApiDocument = generateOpenApiDocument(appRouter, { export const openApiDocument = generateOpenApiDocument(v1ApiRouter, {
title: "OpenPipe API", title: "OpenPipe API",
description: "The public API for reporting API calls to OpenPipe", description: "The public API for reporting API calls to OpenPipe",
version: "0.1.0", version: "0.1.1",
baseUrl: "https://app.openpipe.ai/api", baseUrl: "https://app.openpipe.ai/api/v1",
}); });
// Respond with our OpenAPI schema // Respond with our OpenAPI schema
const hander = (req: NextApiRequest, res: NextApiResponse) => { const hander = (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -0,0 +1,95 @@
import type { ApiKey, Project } from "@prisma/client";
import { TRPCError, initTRPC } from "@trpc/server";
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
import superjson from "superjson";
import { type OpenApiMeta } from "trpc-openapi";
import { ZodError } from "zod";
import { prisma } from "~/server/db";
type CreateContextOptions = {
key:
| (ApiKey & {
project: Project;
})
| null;
};
/**
* This helper generates the "internals" for a tRPC context. If you need to use it, you can export
* it from here.
*
* Examples of things you may need it for:
* - testing, so we don't have to mock Next.js' req/res
* - tRPC's `createSSGHelpers`, where we don't have req/res
*
* @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
*/
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
return {
key: opts.key,
};
};
export const createOpenApiContext = async (opts: CreateNextContextOptions) => {
const { req, res } = opts;
const apiKey = req.headers.authorization?.split(" ")[1] as string | null;
if (!apiKey) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const key = await prisma.apiKey.findUnique({
where: { apiKey },
include: { project: true },
});
if (!key) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return createInnerTRPCContext({
key,
});
};
export type TRPCContext = Awaited<ReturnType<typeof createOpenApiContext>>;
const t = initTRPC
.context<typeof createOpenApiContext>()
.meta<OpenApiMeta>()
.create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
export const createOpenApiRouter = t.router;
export const openApiPublicProc = t.procedure;
/** Reusable middleware that enforces users are logged in before running the procedure. */
const enforceApiKey = t.middleware(async ({ ctx, next }) => {
if (!ctx.key) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: { key: ctx.key },
});
});
/**
* Protected (authenticated) procedure
*
* If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
* the session is valid and guarantees `ctx.session.user` is not null.
*
* @see https://trpc.io/docs/procedures
*/
export const openApiProtectedProc = t.procedure.use(enforceApiKey);

View File

@@ -2,9 +2,6 @@ import { type Prisma } from "@prisma/client";
import { type JsonValue } from "type-fest"; import { type JsonValue } from "type-fest";
import { z } from "zod"; import { z } from "zod";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
import { prisma } from "~/server/db"; import { prisma } from "~/server/db";
import { hashRequest } from "~/server/utils/hashObject"; import { hashRequest } from "~/server/utils/hashObject";
import modelProvider from "~/modelProviders/openai-ChatCompletion"; import modelProvider from "~/modelProviders/openai-ChatCompletion";
@@ -12,6 +9,7 @@ import {
type ChatCompletion, type ChatCompletion,
type CompletionCreateParams, type CompletionCreateParams,
} from "openai/resources/chat/completions"; } from "openai/resources/chat/completions";
import { createOpenApiRouter, openApiProtectedProc } from "./openApiTrpc";
const reqValidator = z.object({ const reqValidator = z.object({
model: z.string(), model: z.string(),
@@ -28,12 +26,12 @@ const respValidator = z.object({
), ),
}); });
export const externalApiRouter = createTRPCRouter({ export const v1ApiRouter = createOpenApiRouter({
checkCache: publicProcedure checkCache: openApiProtectedProc
.meta({ .meta({
openapi: { openapi: {
method: "POST", method: "POST",
path: "/v1/check-cache", path: "/check-cache",
description: "Check if a prompt is cached", description: "Check if a prompt is cached",
protect: true, protect: true,
}, },
@@ -47,7 +45,8 @@ export const externalApiRouter = createTRPCRouter({
.optional() .optional()
.describe( .describe(
'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }', 'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }',
), )
.default({}),
}), }),
) )
.output( .output(
@@ -56,18 +55,8 @@ export const externalApiRouter = createTRPCRouter({
}), }),
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const apiKey = ctx.apiKey;
if (!apiKey) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const key = await prisma.apiKey.findUnique({
where: { apiKey },
});
if (!key) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const reqPayload = await reqValidator.spa(input.reqPayload); const reqPayload = await reqValidator.spa(input.reqPayload);
const cacheKey = hashRequest(key.projectId, reqPayload as JsonValue); const cacheKey = hashRequest(ctx.key.projectId, reqPayload as JsonValue);
const existingResponse = await prisma.loggedCallModelResponse.findFirst({ const existingResponse = await prisma.loggedCallModelResponse.findFirst({
where: { cacheKey }, where: { cacheKey },
@@ -79,23 +68,24 @@ export const externalApiRouter = createTRPCRouter({
await prisma.loggedCall.create({ await prisma.loggedCall.create({
data: { data: {
projectId: key.projectId, projectId: ctx.key.projectId,
requestedAt: new Date(input.requestedAt), requestedAt: new Date(input.requestedAt),
cacheHit: true, cacheHit: true,
modelResponseId: existingResponse.id, modelResponseId: existingResponse.id,
}, },
}); });
await createTags(existingResponse.originalLoggedCallId, input.tags);
return { return {
respPayload: existingResponse.respPayload, respPayload: existingResponse.respPayload,
}; };
}), }),
report: publicProcedure report: openApiProtectedProc
.meta({ .meta({
openapi: { openapi: {
method: "POST", method: "POST",
path: "/v1/report", path: "/report",
description: "Report an API call", description: "Report an API call",
protect: true, protect: true,
}, },
@@ -113,26 +103,16 @@ export const externalApiRouter = createTRPCRouter({
.optional() .optional()
.describe( .describe(
'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }', 'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }',
), )
.default({}),
}), }),
) )
.output(z.void()) .output(z.object({ status: z.literal("ok") }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
console.log("GOT TAGS", input.tags);
const apiKey = ctx.apiKey;
if (!apiKey) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const key = await prisma.apiKey.findUnique({
where: { apiKey },
});
if (!key) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const reqPayload = await reqValidator.spa(input.reqPayload); const reqPayload = await reqValidator.spa(input.reqPayload);
const respPayload = await respValidator.spa(input.respPayload); const respPayload = await respValidator.spa(input.respPayload);
const requestHash = hashRequest(key.projectId, reqPayload as JsonValue); const requestHash = hashRequest(ctx.key.projectId, reqPayload as JsonValue);
const newLoggedCallId = uuidv4(); const newLoggedCallId = uuidv4();
const newModelResponseId = uuidv4(); const newModelResponseId = uuidv4();
@@ -151,7 +131,7 @@ export const externalApiRouter = createTRPCRouter({
prisma.loggedCall.create({ prisma.loggedCall.create({
data: { data: {
id: newLoggedCallId, id: newLoggedCallId,
projectId: key.projectId, projectId: ctx.key.projectId,
requestedAt: new Date(input.requestedAt), requestedAt: new Date(input.requestedAt),
cacheHit: false, cacheHit: false,
model, model,
@@ -185,14 +165,77 @@ export const externalApiRouter = createTRPCRouter({
}), }),
]); ]);
const tagsToCreate = Object.entries(input.tags ?? {}).map(([name, value]) => ({ await createTags(newLoggedCallId, input.tags);
loggedCallId: newLoggedCallId, return { status: "ok" };
// sanitize tags }),
name: name.replaceAll(/[^a-zA-Z0-9_]/g, "_"), localTestingOnlyGetLatestLoggedCall: openApiProtectedProc
value, .meta({
})); openapi: {
await prisma.loggedCallTag.createMany({ method: "GET",
data: tagsToCreate, path: "/local-testing-only-get-latest-logged-call",
description: "Get the latest logged call (only for local testing)",
protect: true, // Make sure to protect this endpoint
},
})
.input(z.void())
.output(
z
.object({
createdAt: z.date(),
cacheHit: z.boolean(),
tags: z.record(z.string().nullable()),
modelResponse: z
.object({
id: z.string(),
statusCode: z.number().nullable(),
errorMessage: z.string().nullable(),
reqPayload: z.unknown(),
respPayload: z.unknown(),
})
.nullable(),
})
.nullable(),
)
.mutation(async ({ ctx }) => {
if (process.env.NODE_ENV === "production") {
throw new Error("This operation is not allowed in production environment");
}
const latestLoggedCall = await prisma.loggedCall.findFirst({
where: { projectId: ctx.key.projectId },
orderBy: { requestedAt: "desc" },
select: {
createdAt: true,
cacheHit: true,
tags: true,
modelResponse: {
select: {
id: true,
statusCode: true,
errorMessage: true,
reqPayload: true,
respPayload: true,
},
},
},
}); });
return (
latestLoggedCall && {
...latestLoggedCall,
tags: Object.fromEntries(latestLoggedCall.tags.map((tag) => [tag.name, tag.value])),
}
);
}), }),
}); });
async function createTags(loggedCallId: string, tags: Record<string, string>) {
const tagsToCreate = Object.entries(tags).map(([name, value]) => ({
loggedCallId,
name: name.replaceAll(/[^a-zA-Z0-9_$]/g, "_"),
value,
}));
await prisma.loggedCallTag.createMany({
data: tagsToCreate,
});
}

View File

@@ -8,7 +8,6 @@ import { evaluationsRouter } from "./routers/evaluations.router";
import { worldChampsRouter } from "./routers/worldChamps.router"; import { worldChampsRouter } from "./routers/worldChamps.router";
import { datasetsRouter } from "./routers/datasets.router"; import { datasetsRouter } from "./routers/datasets.router";
import { datasetEntries } from "./routers/datasetEntries.router"; import { datasetEntries } from "./routers/datasetEntries.router";
import { externalApiRouter } from "./routers/externalApi.router";
import { projectsRouter } from "./routers/projects.router"; import { projectsRouter } from "./routers/projects.router";
import { dashboardRouter } from "./routers/dashboard.router"; import { dashboardRouter } from "./routers/dashboard.router";
import { loggedCallsRouter } from "./routers/loggedCalls.router"; import { loggedCallsRouter } from "./routers/loggedCalls.router";
@@ -31,7 +30,6 @@ export const appRouter = createTRPCRouter({
projects: projectsRouter, projects: projectsRouter,
dashboard: dashboardRouter, dashboard: dashboardRouter,
loggedCalls: loggedCallsRouter, loggedCalls: loggedCallsRouter,
externalApi: externalApiRouter,
}); });
// export type definition of API // export type definition of API

View File

@@ -131,6 +131,8 @@ export const promptVariantsRouter = createTRPCRouter({
const inputTokens = overallTokens._sum?.inputTokens ?? 0; const inputTokens = overallTokens._sum?.inputTokens ?? 0;
const outputTokens = overallTokens._sum?.outputTokens ?? 0; const outputTokens = overallTokens._sum?.outputTokens ?? 0;
const awaitingCompletions = outputCount < scenarioCount;
const awaitingEvals = !!evalResults.find( const awaitingEvals = !!evalResults.find(
(result) => result.totalCount < scenarioCount * evals.length, (result) => result.totalCount < scenarioCount * evals.length,
); );
@@ -142,6 +144,7 @@ export const promptVariantsRouter = createTRPCRouter({
overallCost: overallTokens._sum?.cost ?? 0, overallCost: overallTokens._sum?.cost ?? 0,
scenarioCount, scenarioCount,
outputCount, outputCount,
awaitingCompletions,
awaitingEvals, awaitingEvals,
}; };
}), }),

View File

@@ -27,7 +27,6 @@ import { capturePath } from "~/utils/analytics/serverAnalytics";
type CreateContextOptions = { type CreateContextOptions = {
session: Session | null; session: Session | null;
apiKey: string | null;
}; };
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -46,7 +45,6 @@ const noOp = () => {};
export const createInnerTRPCContext = (opts: CreateContextOptions) => { export const createInnerTRPCContext = (opts: CreateContextOptions) => {
return { return {
session: opts.session, session: opts.session,
apiKey: opts.apiKey,
prisma, prisma,
markAccessControlRun: noOp, markAccessControlRun: noOp,
}; };
@@ -64,11 +62,8 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
// Get the session from the server using the getServerSession wrapper function // Get the session from the server using the getServerSession wrapper function
const session = await getServerAuthSession({ req, res }); const session = await getServerAuthSession({ req, res });
const apiKey = req.headers.authorization?.split(" ")[1] as string | null;
return createInnerTRPCContext({ return createInnerTRPCContext({
session, session,
apiKey,
}); });
}; };

View File

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

View File

@@ -51,7 +51,7 @@ const requestUpdatedPromptFunction = async (
originalModelProvider.inputSchema, originalModelProvider.inputSchema,
null, null,
2, 2,
)}\n\nDo not add any assistant messages.`, )}`,
}, },
{ {
role: "user", role: "user",

View File

@@ -2,4 +2,4 @@ import cryptoRandomString from "crypto-random-string";
const KEY_LENGTH = 42; const KEY_LENGTH = 42;
export const generateApiKey = () => `opc_${cryptoRandomString({ length: KEY_LENGTH })}`; export const generateApiKey = () => `opk_${cryptoRandomString({ length: KEY_LENGTH })}`;

View File

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

9
app/test-docker.sh Executable file
View File

@@ -0,0 +1,9 @@
#! /bin/bash
set -e
cd "$(dirname "$0")/.."
source app/.env
docker build . --file app/Dockerfile

View File

@@ -3,17 +3,17 @@
"info": { "info": {
"title": "OpenPipe API", "title": "OpenPipe API",
"description": "The public API for reporting API calls to OpenPipe", "description": "The public API for reporting API calls to OpenPipe",
"version": "0.1.0" "version": "0.1.1"
}, },
"servers": [ "servers": [
{ {
"url": "https://app.openpipe.ai/api" "url": "https://app.openpipe.ai/api/v1"
} }
], ],
"paths": { "paths": {
"/v1/check-cache": { "/check-cache": {
"post": { "post": {
"operationId": "externalApi-checkCache", "operationId": "checkCache",
"description": "Check if a prompt is cached", "description": "Check if a prompt is cached",
"security": [ "security": [
{ {
@@ -39,7 +39,8 @@
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
}, },
"description": "Extra tags to attach to the call for filtering. Eg { \"userId\": \"123\", \"promptId\": \"populate-title\" }" "description": "Extra tags to attach to the call for filtering. Eg { \"userId\": \"123\", \"promptId\": \"populate-title\" }",
"default": {}
} }
}, },
"required": [ "required": [
@@ -74,9 +75,9 @@
} }
} }
}, },
"/v1/report": { "/report": {
"post": { "post": {
"operationId": "externalApi-report", "operationId": "report",
"description": "Report an API call", "description": "Report an API call",
"security": [ "security": [
{ {
@@ -117,7 +118,8 @@
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
}, },
"description": "Extra tags to attach to the call for filtering. Eg { \"userId\": \"123\", \"promptId\": \"populate-title\" }" "description": "Extra tags to attach to the call for filtering. Eg { \"userId\": \"123\", \"promptId\": \"populate-title\" }",
"default": {}
} }
}, },
"required": [ "required": [
@@ -135,7 +137,97 @@
"description": "Successful response", "description": "Successful response",
"content": { "content": {
"application/json": { "application/json": {
"schema": {} "schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": [
"ok"
]
}
},
"required": [
"status"
],
"additionalProperties": false
}
}
}
},
"default": {
"$ref": "#/components/responses/error"
}
}
}
},
"/local-testing-only-get-latest-logged-call": {
"get": {
"operationId": "localTestingOnlyGetLatestLoggedCall",
"description": "Get the latest logged call (only for local testing)",
"security": [
{
"Authorization": []
}
],
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"createdAt": {
"type": "string",
"format": "date-time"
},
"cacheHit": {
"type": "boolean"
},
"tags": {
"type": "object",
"additionalProperties": {
"type": "string",
"nullable": true
}
},
"modelResponse": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"statusCode": {
"type": "number",
"nullable": true
},
"errorMessage": {
"type": "string",
"nullable": true
},
"reqPayload": {},
"respPayload": {}
},
"required": [
"id",
"statusCode",
"errorMessage"
],
"additionalProperties": false,
"nullable": true
}
},
"required": [
"createdAt",
"cacheHit",
"tags",
"modelResponse"
],
"additionalProperties": false,
"nullable": true
}
} }
} }
}, },

View File

@@ -5,14 +5,14 @@ import httpx
from ... import errors from ... import errors
from ...client import AuthenticatedClient, Client from ...client import AuthenticatedClient, Client
from ...models.external_api_check_cache_json_body import ExternalApiCheckCacheJsonBody from ...models.check_cache_json_body import CheckCacheJsonBody
from ...models.external_api_check_cache_response_200 import ExternalApiCheckCacheResponse200 from ...models.check_cache_response_200 import CheckCacheResponse200
from ...types import Response from ...types import Response
def _get_kwargs( def _get_kwargs(
*, *,
json_body: ExternalApiCheckCacheJsonBody, json_body: CheckCacheJsonBody,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
pass pass
@@ -20,16 +20,16 @@ def _get_kwargs(
return { return {
"method": "post", "method": "post",
"url": "/v1/check-cache", "url": "/check-cache",
"json": json_json_body, "json": json_json_body,
} }
def _parse_response( def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response *, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[ExternalApiCheckCacheResponse200]: ) -> Optional[CheckCacheResponse200]:
if response.status_code == HTTPStatus.OK: if response.status_code == HTTPStatus.OK:
response_200 = ExternalApiCheckCacheResponse200.from_dict(response.json()) response_200 = CheckCacheResponse200.from_dict(response.json())
return response_200 return response_200
if client.raise_on_unexpected_status: if client.raise_on_unexpected_status:
@@ -40,7 +40,7 @@ def _parse_response(
def _build_response( def _build_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response *, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Response[ExternalApiCheckCacheResponse200]: ) -> Response[CheckCacheResponse200]:
return Response( return Response(
status_code=HTTPStatus(response.status_code), status_code=HTTPStatus(response.status_code),
content=response.content, content=response.content,
@@ -52,19 +52,19 @@ def _build_response(
def sync_detailed( def sync_detailed(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
json_body: ExternalApiCheckCacheJsonBody, json_body: CheckCacheJsonBody,
) -> Response[ExternalApiCheckCacheResponse200]: ) -> Response[CheckCacheResponse200]:
"""Check if a prompt is cached """Check if a prompt is cached
Args: Args:
json_body (ExternalApiCheckCacheJsonBody): json_body (CheckCacheJsonBody):
Raises: Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. 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. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
Response[ExternalApiCheckCacheResponse200] Response[CheckCacheResponse200]
""" """
kwargs = _get_kwargs( kwargs = _get_kwargs(
@@ -81,19 +81,19 @@ def sync_detailed(
def sync( def sync(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
json_body: ExternalApiCheckCacheJsonBody, json_body: CheckCacheJsonBody,
) -> Optional[ExternalApiCheckCacheResponse200]: ) -> Optional[CheckCacheResponse200]:
"""Check if a prompt is cached """Check if a prompt is cached
Args: Args:
json_body (ExternalApiCheckCacheJsonBody): json_body (CheckCacheJsonBody):
Raises: Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. 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. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
ExternalApiCheckCacheResponse200 CheckCacheResponse200
""" """
return sync_detailed( return sync_detailed(
@@ -105,19 +105,19 @@ def sync(
async def asyncio_detailed( async def asyncio_detailed(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
json_body: ExternalApiCheckCacheJsonBody, json_body: CheckCacheJsonBody,
) -> Response[ExternalApiCheckCacheResponse200]: ) -> Response[CheckCacheResponse200]:
"""Check if a prompt is cached """Check if a prompt is cached
Args: Args:
json_body (ExternalApiCheckCacheJsonBody): json_body (CheckCacheJsonBody):
Raises: Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. 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. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
Response[ExternalApiCheckCacheResponse200] Response[CheckCacheResponse200]
""" """
kwargs = _get_kwargs( kwargs = _get_kwargs(
@@ -132,19 +132,19 @@ async def asyncio_detailed(
async def asyncio( async def asyncio(
*, *,
client: AuthenticatedClient, client: AuthenticatedClient,
json_body: ExternalApiCheckCacheJsonBody, json_body: CheckCacheJsonBody,
) -> Optional[ExternalApiCheckCacheResponse200]: ) -> Optional[CheckCacheResponse200]:
"""Check if a prompt is cached """Check if a prompt is cached
Args: Args:
json_body (ExternalApiCheckCacheJsonBody): json_body (CheckCacheJsonBody):
Raises: Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. 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. httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns: Returns:
ExternalApiCheckCacheResponse200 CheckCacheResponse200
""" """
return ( return (

View File

@@ -1,98 +0,0 @@
from http import HTTPStatus
from typing import Any, Dict, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.external_api_report_json_body import ExternalApiReportJsonBody
from ...types import Response
def _get_kwargs(
*,
json_body: ExternalApiReportJsonBody,
) -> Dict[str, Any]:
pass
json_json_body = json_body.to_dict()
return {
"method": "post",
"url": "/v1/report",
"json": json_json_body,
}
def _parse_response(*, client: Union[AuthenticatedClient, Client], response: httpx.Response) -> Optional[Any]:
if response.status_code == HTTPStatus.OK:
return None
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]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
*,
client: AuthenticatedClient,
json_body: ExternalApiReportJsonBody,
) -> Response[Any]:
"""Report an API call
Args:
json_body (ExternalApiReportJsonBody):
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[Any]
"""
kwargs = _get_kwargs(
json_body=json_body,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
async def asyncio_detailed(
*,
client: AuthenticatedClient,
json_body: ExternalApiReportJsonBody,
) -> Response[Any]:
"""Report an API call
Args:
json_body (ExternalApiReportJsonBody):
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[Any]
"""
kwargs = _get_kwargs(
json_body=json_body,
)
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)

View File

@@ -0,0 +1,133 @@
from http import HTTPStatus
from typing import Any, Dict, Optional, Union
import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.local_testing_only_get_latest_logged_call_response_200 import (
LocalTestingOnlyGetLatestLoggedCallResponse200,
)
from ...types import Response
def _get_kwargs() -> Dict[str, Any]:
pass
return {
"method": "get",
"url": "/local-testing-only-get-latest-logged-call",
}
def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]]:
if response.status_code == HTTPStatus.OK:
_response_200 = response.json()
response_200: Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]
if _response_200 is None:
response_200 = None
else:
response_200 = LocalTestingOnlyGetLatestLoggedCallResponse200.from_dict(_response_200)
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[Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_detailed(
*,
client: AuthenticatedClient,
) -> Response[Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]]:
"""Get the latest logged call (only for local testing)
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[Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]]
"""
kwargs = _get_kwargs()
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
*,
client: AuthenticatedClient,
) -> Optional[Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]]:
"""Get the latest logged call (only for local testing)
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:
Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]
"""
return sync_detailed(
client=client,
).parsed
async def asyncio_detailed(
*,
client: AuthenticatedClient,
) -> Response[Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]]:
"""Get the latest logged call (only for local testing)
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[Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]]
"""
kwargs = _get_kwargs()
response = await client.get_async_httpx_client().request(**kwargs)
return _build_response(client=client, response=response)
async def asyncio(
*,
client: AuthenticatedClient,
) -> Optional[Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]]:
"""Get the latest logged call (only for local testing)
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:
Optional[LocalTestingOnlyGetLatestLoggedCallResponse200]
"""
return (
await asyncio_detailed(
client=client,
)
).parsed

View File

@@ -0,0 +1,155 @@
from http import HTTPStatus
from typing import Any, Dict, Optional, Union
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
def _get_kwargs(
*,
json_body: ReportJsonBody,
) -> Dict[str, Any]:
pass
json_json_body = json_body.to_dict()
return {
"method": "post",
"url": "/report",
"json": json_json_body,
}
def _parse_response(
*, client: Union[AuthenticatedClient, Client], response: httpx.Response
) -> Optional[ReportResponse200]:
if response.status_code == HTTPStatus.OK:
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[ReportResponse200]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)
def sync_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(
json_body=json_body,
)
response = client.get_httpx_client().request(
**kwargs,
)
return _build_response(client=client, response=response)
def sync(
*,
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 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(
json_body=json_body,
)
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

@@ -1,15 +1,29 @@
""" Contains all the data models used in inputs/outputs """ """ Contains all the data models used in inputs/outputs """
from .external_api_check_cache_json_body import ExternalApiCheckCacheJsonBody from .check_cache_json_body import CheckCacheJsonBody
from .external_api_check_cache_json_body_tags import ExternalApiCheckCacheJsonBodyTags from .check_cache_json_body_tags import CheckCacheJsonBodyTags
from .external_api_check_cache_response_200 import ExternalApiCheckCacheResponse200 from .check_cache_response_200 import CheckCacheResponse200
from .external_api_report_json_body import ExternalApiReportJsonBody from .local_testing_only_get_latest_logged_call_response_200 import LocalTestingOnlyGetLatestLoggedCallResponse200
from .external_api_report_json_body_tags import ExternalApiReportJsonBodyTags from .local_testing_only_get_latest_logged_call_response_200_model_response import (
LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse,
)
from .local_testing_only_get_latest_logged_call_response_200_tags import (
LocalTestingOnlyGetLatestLoggedCallResponse200Tags,
)
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__ = ( __all__ = (
"ExternalApiCheckCacheJsonBody", "CheckCacheJsonBody",
"ExternalApiCheckCacheJsonBodyTags", "CheckCacheJsonBodyTags",
"ExternalApiCheckCacheResponse200", "CheckCacheResponse200",
"ExternalApiReportJsonBody", "LocalTestingOnlyGetLatestLoggedCallResponse200",
"ExternalApiReportJsonBodyTags", "LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse",
"LocalTestingOnlyGetLatestLoggedCallResponse200Tags",
"ReportJsonBody",
"ReportJsonBodyTags",
"ReportResponse200",
"ReportResponse200Status",
) )

View File

@@ -5,25 +5,25 @@ from attrs import define
from ..types import UNSET, Unset from ..types import UNSET, Unset
if TYPE_CHECKING: if TYPE_CHECKING:
from ..models.external_api_check_cache_json_body_tags import ExternalApiCheckCacheJsonBodyTags from ..models.check_cache_json_body_tags import CheckCacheJsonBodyTags
T = TypeVar("T", bound="ExternalApiCheckCacheJsonBody") T = TypeVar("T", bound="CheckCacheJsonBody")
@define @define
class ExternalApiCheckCacheJsonBody: class CheckCacheJsonBody:
""" """
Attributes: Attributes:
requested_at (float): Unix timestamp in milliseconds requested_at (float): Unix timestamp in milliseconds
req_payload (Union[Unset, Any]): JSON-encoded request payload req_payload (Union[Unset, Any]): JSON-encoded request payload
tags (Union[Unset, ExternalApiCheckCacheJsonBodyTags]): Extra tags to attach to the call for filtering. Eg { tags (Union[Unset, CheckCacheJsonBodyTags]): Extra tags to attach to the call for filtering. Eg { "userId":
"userId": "123", "promptId": "populate-title" } "123", "promptId": "populate-title" }
""" """
requested_at: float requested_at: float
req_payload: Union[Unset, Any] = UNSET req_payload: Union[Unset, Any] = UNSET
tags: Union[Unset, "ExternalApiCheckCacheJsonBodyTags"] = UNSET tags: Union[Unset, "CheckCacheJsonBodyTags"] = UNSET
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
requested_at = self.requested_at requested_at = self.requested_at
@@ -47,7 +47,7 @@ class ExternalApiCheckCacheJsonBody:
@classmethod @classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
from ..models.external_api_check_cache_json_body_tags import ExternalApiCheckCacheJsonBodyTags from ..models.check_cache_json_body_tags import CheckCacheJsonBodyTags
d = src_dict.copy() d = src_dict.copy()
requested_at = d.pop("requestedAt") requested_at = d.pop("requestedAt")
@@ -55,16 +55,16 @@ class ExternalApiCheckCacheJsonBody:
req_payload = d.pop("reqPayload", UNSET) req_payload = d.pop("reqPayload", UNSET)
_tags = d.pop("tags", UNSET) _tags = d.pop("tags", UNSET)
tags: Union[Unset, ExternalApiCheckCacheJsonBodyTags] tags: Union[Unset, CheckCacheJsonBodyTags]
if isinstance(_tags, Unset): if isinstance(_tags, Unset):
tags = UNSET tags = UNSET
else: else:
tags = ExternalApiCheckCacheJsonBodyTags.from_dict(_tags) tags = CheckCacheJsonBodyTags.from_dict(_tags)
external_api_check_cache_json_body = cls( check_cache_json_body = cls(
requested_at=requested_at, requested_at=requested_at,
req_payload=req_payload, req_payload=req_payload,
tags=tags, tags=tags,
) )
return external_api_check_cache_json_body return check_cache_json_body

View File

@@ -2,11 +2,11 @@ from typing import Any, Dict, List, Type, TypeVar
from attrs import define, field from attrs import define, field
T = TypeVar("T", bound="ExternalApiReportJsonBodyTags") T = TypeVar("T", bound="CheckCacheJsonBodyTags")
@define @define
class ExternalApiReportJsonBodyTags: class CheckCacheJsonBodyTags:
"""Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }""" """Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }"""
additional_properties: Dict[str, str] = field(init=False, factory=dict) additional_properties: Dict[str, str] = field(init=False, factory=dict)
@@ -21,10 +21,10 @@ class ExternalApiReportJsonBodyTags:
@classmethod @classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
d = src_dict.copy() d = src_dict.copy()
external_api_report_json_body_tags = cls() check_cache_json_body_tags = cls()
external_api_report_json_body_tags.additional_properties = d check_cache_json_body_tags.additional_properties = d
return external_api_report_json_body_tags return check_cache_json_body_tags
@property @property
def additional_keys(self) -> List[str]: def additional_keys(self) -> List[str]:

View File

@@ -4,11 +4,11 @@ from attrs import define
from ..types import UNSET, Unset from ..types import UNSET, Unset
T = TypeVar("T", bound="ExternalApiCheckCacheResponse200") T = TypeVar("T", bound="CheckCacheResponse200")
@define @define
class ExternalApiCheckCacheResponse200: class CheckCacheResponse200:
""" """
Attributes: Attributes:
resp_payload (Union[Unset, Any]): JSON-encoded response payload resp_payload (Union[Unset, Any]): JSON-encoded response payload
@@ -31,8 +31,8 @@ class ExternalApiCheckCacheResponse200:
d = src_dict.copy() d = src_dict.copy()
resp_payload = d.pop("respPayload", UNSET) resp_payload = d.pop("respPayload", UNSET)
external_api_check_cache_response_200 = cls( check_cache_response_200 = cls(
resp_payload=resp_payload, resp_payload=resp_payload,
) )
return external_api_check_cache_response_200 return check_cache_response_200

View File

@@ -0,0 +1,84 @@
import datetime
from typing import TYPE_CHECKING, Any, Dict, Optional, Type, TypeVar
from attrs import define
from dateutil.parser import isoparse
if TYPE_CHECKING:
from ..models.local_testing_only_get_latest_logged_call_response_200_model_response import (
LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse,
)
from ..models.local_testing_only_get_latest_logged_call_response_200_tags import (
LocalTestingOnlyGetLatestLoggedCallResponse200Tags,
)
T = TypeVar("T", bound="LocalTestingOnlyGetLatestLoggedCallResponse200")
@define
class LocalTestingOnlyGetLatestLoggedCallResponse200:
"""
Attributes:
created_at (datetime.datetime):
cache_hit (bool):
tags (LocalTestingOnlyGetLatestLoggedCallResponse200Tags):
model_response (Optional[LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse]):
"""
created_at: datetime.datetime
cache_hit: bool
tags: "LocalTestingOnlyGetLatestLoggedCallResponse200Tags"
model_response: Optional["LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse"]
def to_dict(self) -> Dict[str, Any]:
created_at = self.created_at.isoformat()
cache_hit = self.cache_hit
tags = self.tags.to_dict()
model_response = self.model_response.to_dict() if self.model_response else None
field_dict: Dict[str, Any] = {}
field_dict.update(
{
"createdAt": created_at,
"cacheHit": cache_hit,
"tags": tags,
"modelResponse": model_response,
}
)
return field_dict
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
from ..models.local_testing_only_get_latest_logged_call_response_200_model_response import (
LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse,
)
from ..models.local_testing_only_get_latest_logged_call_response_200_tags import (
LocalTestingOnlyGetLatestLoggedCallResponse200Tags,
)
d = src_dict.copy()
created_at = isoparse(d.pop("createdAt"))
cache_hit = d.pop("cacheHit")
tags = LocalTestingOnlyGetLatestLoggedCallResponse200Tags.from_dict(d.pop("tags"))
_model_response = d.pop("modelResponse")
model_response: Optional[LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse]
if _model_response is None:
model_response = None
else:
model_response = LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse.from_dict(_model_response)
local_testing_only_get_latest_logged_call_response_200 = cls(
created_at=created_at,
cache_hit=cache_hit,
tags=tags,
model_response=model_response,
)
return local_testing_only_get_latest_logged_call_response_200

View File

@@ -0,0 +1,70 @@
from typing import Any, Dict, Optional, Type, TypeVar, Union
from attrs import define
from ..types import UNSET, Unset
T = TypeVar("T", bound="LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse")
@define
class LocalTestingOnlyGetLatestLoggedCallResponse200ModelResponse:
"""
Attributes:
id (str):
status_code (Optional[float]):
error_message (Optional[str]):
req_payload (Union[Unset, Any]):
resp_payload (Union[Unset, Any]):
"""
id: str
status_code: Optional[float]
error_message: Optional[str]
req_payload: Union[Unset, Any] = UNSET
resp_payload: Union[Unset, Any] = UNSET
def to_dict(self) -> Dict[str, Any]:
id = self.id
status_code = self.status_code
error_message = self.error_message
req_payload = self.req_payload
resp_payload = self.resp_payload
field_dict: Dict[str, Any] = {}
field_dict.update(
{
"id": id,
"statusCode": status_code,
"errorMessage": error_message,
}
)
if req_payload is not UNSET:
field_dict["reqPayload"] = req_payload
if resp_payload is not UNSET:
field_dict["respPayload"] = resp_payload
return field_dict
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
d = src_dict.copy()
id = d.pop("id")
status_code = d.pop("statusCode")
error_message = d.pop("errorMessage")
req_payload = d.pop("reqPayload", UNSET)
resp_payload = d.pop("respPayload", UNSET)
local_testing_only_get_latest_logged_call_response_200_model_response = cls(
id=id,
status_code=status_code,
error_message=error_message,
req_payload=req_payload,
resp_payload=resp_payload,
)
return local_testing_only_get_latest_logged_call_response_200_model_response

View File

@@ -0,0 +1,43 @@
from typing import Any, Dict, List, Optional, Type, TypeVar
from attrs import define, field
T = TypeVar("T", bound="LocalTestingOnlyGetLatestLoggedCallResponse200Tags")
@define
class LocalTestingOnlyGetLatestLoggedCallResponse200Tags:
""" """
additional_properties: Dict[str, Optional[str]] = field(init=False, factory=dict)
def to_dict(self) -> Dict[str, Any]:
field_dict: Dict[str, Any] = {}
field_dict.update(self.additional_properties)
field_dict.update({})
return field_dict
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
d = src_dict.copy()
local_testing_only_get_latest_logged_call_response_200_tags = cls()
local_testing_only_get_latest_logged_call_response_200_tags.additional_properties = d
return local_testing_only_get_latest_logged_call_response_200_tags
@property
def additional_keys(self) -> List[str]:
return list(self.additional_properties.keys())
def __getitem__(self, key: str) -> Optional[str]:
return self.additional_properties[key]
def __setitem__(self, key: str, value: Optional[str]) -> None:
self.additional_properties[key] = value
def __delitem__(self, key: str) -> None:
del self.additional_properties[key]
def __contains__(self, key: str) -> bool:
return key in self.additional_properties

View File

@@ -5,14 +5,14 @@ from attrs import define
from ..types import UNSET, Unset from ..types import UNSET, Unset
if TYPE_CHECKING: if TYPE_CHECKING:
from ..models.external_api_report_json_body_tags import ExternalApiReportJsonBodyTags from ..models.report_json_body_tags import ReportJsonBodyTags
T = TypeVar("T", bound="ExternalApiReportJsonBody") T = TypeVar("T", bound="ReportJsonBody")
@define @define
class ExternalApiReportJsonBody: class ReportJsonBody:
""" """
Attributes: Attributes:
requested_at (float): Unix timestamp in milliseconds requested_at (float): Unix timestamp in milliseconds
@@ -21,8 +21,8 @@ class ExternalApiReportJsonBody:
resp_payload (Union[Unset, Any]): JSON-encoded response payload resp_payload (Union[Unset, Any]): JSON-encoded response payload
status_code (Union[Unset, float]): HTTP status code of response status_code (Union[Unset, float]): HTTP status code of response
error_message (Union[Unset, str]): User-friendly error message error_message (Union[Unset, str]): User-friendly error message
tags (Union[Unset, ExternalApiReportJsonBodyTags]): Extra tags to attach to the call for filtering. Eg { tags (Union[Unset, ReportJsonBodyTags]): Extra tags to attach to the call for filtering. Eg { "userId": "123",
"userId": "123", "promptId": "populate-title" } "promptId": "populate-title" }
""" """
requested_at: float requested_at: float
@@ -31,7 +31,7 @@ class ExternalApiReportJsonBody:
resp_payload: Union[Unset, Any] = UNSET resp_payload: Union[Unset, Any] = UNSET
status_code: Union[Unset, float] = UNSET status_code: Union[Unset, float] = UNSET
error_message: Union[Unset, str] = UNSET error_message: Union[Unset, str] = UNSET
tags: Union[Unset, "ExternalApiReportJsonBodyTags"] = UNSET tags: Union[Unset, "ReportJsonBodyTags"] = UNSET
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
requested_at = self.requested_at requested_at = self.requested_at
@@ -66,7 +66,7 @@ class ExternalApiReportJsonBody:
@classmethod @classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
from ..models.external_api_report_json_body_tags import ExternalApiReportJsonBodyTags from ..models.report_json_body_tags import ReportJsonBodyTags
d = src_dict.copy() d = src_dict.copy()
requested_at = d.pop("requestedAt") requested_at = d.pop("requestedAt")
@@ -82,13 +82,13 @@ class ExternalApiReportJsonBody:
error_message = d.pop("errorMessage", UNSET) error_message = d.pop("errorMessage", UNSET)
_tags = d.pop("tags", UNSET) _tags = d.pop("tags", UNSET)
tags: Union[Unset, ExternalApiReportJsonBodyTags] tags: Union[Unset, ReportJsonBodyTags]
if isinstance(_tags, Unset): if isinstance(_tags, Unset):
tags = UNSET tags = UNSET
else: else:
tags = ExternalApiReportJsonBodyTags.from_dict(_tags) tags = ReportJsonBodyTags.from_dict(_tags)
external_api_report_json_body = cls( report_json_body = cls(
requested_at=requested_at, requested_at=requested_at,
received_at=received_at, received_at=received_at,
req_payload=req_payload, req_payload=req_payload,
@@ -98,4 +98,4 @@ class ExternalApiReportJsonBody:
tags=tags, tags=tags,
) )
return external_api_report_json_body return report_json_body

View File

@@ -2,11 +2,11 @@ from typing import Any, Dict, List, Type, TypeVar
from attrs import define, field from attrs import define, field
T = TypeVar("T", bound="ExternalApiCheckCacheJsonBodyTags") T = TypeVar("T", bound="ReportJsonBodyTags")
@define @define
class ExternalApiCheckCacheJsonBodyTags: class ReportJsonBodyTags:
"""Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }""" """Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }"""
additional_properties: Dict[str, str] = field(init=False, factory=dict) additional_properties: Dict[str, str] = field(init=False, factory=dict)
@@ -21,10 +21,10 @@ class ExternalApiCheckCacheJsonBodyTags:
@classmethod @classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
d = src_dict.copy() d = src_dict.copy()
external_api_check_cache_json_body_tags = cls() report_json_body_tags = cls()
external_api_check_cache_json_body_tags.additional_properties = d report_json_body_tags.additional_properties = d
return external_api_check_cache_json_body_tags return report_json_body_tags
@property @property
def additional_keys(self) -> List[str]: def additional_keys(self) -> List[str]:

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

@@ -1,9 +1,9 @@
from typing import Any, Optional from typing import Any, Optional
def merge_streamed_chunks(base: Optional[Any], chunk: Any) -> Any: def merge_openai_chunks(base: Optional[Any], chunk: Any) -> Any:
if base is None: if base is None:
return merge_streamed_chunks({**chunk, "choices": []}, chunk) return merge_openai_chunks({**chunk, "choices": []}, chunk)
choices = base["choices"].copy() choices = base["choices"].copy()
for choice in chunk["choices"]: for choice in chunk["choices"]:
@@ -34,9 +34,7 @@ def merge_streamed_chunks(base: Optional[Any], chunk: Any) -> Any:
{**new_choice, "message": {"role": "assistant", **choice["delta"]}} {**new_choice, "message": {"role": "assistant", **choice["delta"]}}
) )
merged = { return {
**base, **base,
"choices": choices, "choices": choices,
} }
return merged

View File

@@ -3,9 +3,16 @@ from openai.openai_object import OpenAIObject
import time import time
import inspect import inspect
from openpipe.merge_openai_chunks import merge_streamed_chunks from openpipe.merge_openai_chunks import merge_openai_chunks
from openpipe.openpipe_meta import OpenPipeMeta
from .shared import maybe_check_cache, maybe_check_cache_async, report_async, report from .shared import (
_should_check_cache,
maybe_check_cache,
maybe_check_cache_async,
report_async,
report,
)
class WrappedChatCompletion(original_openai.ChatCompletion): class WrappedChatCompletion(original_openai.ChatCompletion):
@@ -29,9 +36,15 @@ class WrappedChatCompletion(original_openai.ChatCompletion):
def _gen(): def _gen():
assembled_completion = None assembled_completion = None
for chunk in chat_completion: for chunk in chat_completion:
assembled_completion = merge_streamed_chunks( assembled_completion = merge_openai_chunks(
assembled_completion, chunk assembled_completion, chunk
) )
cache_status = (
"MISS" if _should_check_cache(openpipe_options) else "SKIP"
)
chunk.openpipe = OpenPipeMeta(cache_status=cache_status)
yield chunk yield chunk
received_at = int(time.time() * 1000) received_at = int(time.time() * 1000)
@@ -58,6 +71,10 @@ class WrappedChatCompletion(original_openai.ChatCompletion):
status_code=200, status_code=200,
) )
cache_status = (
"MISS" if _should_check_cache(openpipe_options) else "SKIP"
)
chat_completion["openpipe"] = OpenPipeMeta(cache_status=cache_status)
return chat_completion return chat_completion
except Exception as e: except Exception as e:
received_at = int(time.time() * 1000) received_at = int(time.time() * 1000)
@@ -96,21 +113,28 @@ class WrappedChatCompletion(original_openai.ChatCompletion):
requested_at = int(time.time() * 1000) requested_at = int(time.time() * 1000)
try: try:
chat_completion = original_openai.ChatCompletion.acreate(*args, **kwargs) chat_completion = await original_openai.ChatCompletion.acreate(
*args, **kwargs
)
if inspect.isgenerator(chat_completion): if inspect.isasyncgen(chat_completion):
def _gen(): async def _gen():
assembled_completion = None assembled_completion = None
for chunk in chat_completion: async for chunk in chat_completion:
assembled_completion = merge_streamed_chunks( assembled_completion = merge_openai_chunks(
assembled_completion, chunk assembled_completion, chunk
) )
cache_status = (
"MISS" if _should_check_cache(openpipe_options) else "SKIP"
)
chunk.openpipe = OpenPipeMeta(cache_status=cache_status)
yield chunk yield chunk
received_at = int(time.time() * 1000) received_at = int(time.time() * 1000)
report_async( await report_async(
openpipe_options=openpipe_options, openpipe_options=openpipe_options,
requested_at=requested_at, requested_at=requested_at,
received_at=received_at, received_at=received_at,
@@ -123,7 +147,7 @@ class WrappedChatCompletion(original_openai.ChatCompletion):
else: else:
received_at = int(time.time() * 1000) received_at = int(time.time() * 1000)
report_async( await report_async(
openpipe_options=openpipe_options, openpipe_options=openpipe_options,
requested_at=requested_at, requested_at=requested_at,
received_at=received_at, received_at=received_at,
@@ -132,12 +156,17 @@ class WrappedChatCompletion(original_openai.ChatCompletion):
status_code=200, status_code=200,
) )
cache_status = (
"MISS" if _should_check_cache(openpipe_options) else "SKIP"
)
chat_completion["openpipe"] = OpenPipeMeta(cache_status=cache_status)
return chat_completion return chat_completion
except Exception as e: except Exception as e:
received_at = int(time.time() * 1000) received_at = int(time.time() * 1000)
if isinstance(e, original_openai.OpenAIError): if isinstance(e, original_openai.OpenAIError):
report_async( await report_async(
openpipe_options=openpipe_options, openpipe_options=openpipe_options,
requested_at=requested_at, requested_at=requested_at,
received_at=received_at, received_at=received_at,
@@ -147,7 +176,7 @@ class WrappedChatCompletion(original_openai.ChatCompletion):
status_code=e.http_status, status_code=e.http_status,
) )
else: else:
report_async( await report_async(
openpipe_options=openpipe_options, openpipe_options=openpipe_options,
requested_at=requested_at, requested_at=requested_at,
received_at=received_at, received_at=received_at,

View File

@@ -0,0 +1,7 @@
from attr import dataclass
@dataclass
class OpenPipeMeta:
# Cache status. One of 'HIT', 'MISS', 'SKIP'
cache_status: str

View File

@@ -1,10 +1,10 @@
from openpipe.api_client.api.default import ( from openpipe.api_client.api.default import (
external_api_report, report as api_report,
external_api_check_cache, check_cache,
) )
from openpipe.api_client.client import AuthenticatedClient from openpipe.api_client.client import AuthenticatedClient
from openpipe.api_client.models.external_api_report_json_body_tags import ( from openpipe.api_client.models.report_json_body_tags import (
ExternalApiReportJsonBodyTags, ReportJsonBodyTags,
) )
import toml import toml
import time import time
@@ -19,9 +19,9 @@ configured_client = AuthenticatedClient(
def _get_tags(openpipe_options): def _get_tags(openpipe_options):
tags = openpipe_options.get("tags") or {} tags = openpipe_options.get("tags") or {}
tags["$sdk"] = "python" tags["$sdk"] = "python"
tags["$sdk_version"] = version tags["$sdk.version"] = version
return ExternalApiReportJsonBodyTags.from_dict(tags) return ReportJsonBodyTags.from_dict(tags)
def _should_check_cache(openpipe_options): def _should_check_cache(openpipe_options):
@@ -31,7 +31,7 @@ def _should_check_cache(openpipe_options):
def _process_cache_payload( def _process_cache_payload(
payload: external_api_check_cache.ExternalApiCheckCacheResponse200, payload: check_cache.CheckCacheResponse200,
): ):
if not payload or not payload.resp_payload: if not payload or not payload.resp_payload:
return None return None
@@ -47,9 +47,9 @@ def maybe_check_cache(
if not _should_check_cache(openpipe_options): if not _should_check_cache(openpipe_options):
return None return None
try: try:
payload = external_api_check_cache.sync( payload = check_cache.sync(
client=configured_client, client=configured_client,
json_body=external_api_check_cache.ExternalApiCheckCacheJsonBody( json_body=check_cache.CheckCacheJsonBody(
req_payload=req_payload, req_payload=req_payload,
requested_at=int(time.time() * 1000), requested_at=int(time.time() * 1000),
tags=_get_tags(openpipe_options), tags=_get_tags(openpipe_options),
@@ -72,9 +72,9 @@ async def maybe_check_cache_async(
return None return None
try: try:
payload = await external_api_check_cache.asyncio( payload = await check_cache.asyncio(
client=configured_client, client=configured_client,
json_body=external_api_check_cache.ExternalApiCheckCacheJsonBody( json_body=check_cache.CheckCacheJsonBody(
req_payload=req_payload, req_payload=req_payload,
requested_at=int(time.time() * 1000), requested_at=int(time.time() * 1000),
tags=_get_tags(openpipe_options), tags=_get_tags(openpipe_options),
@@ -94,9 +94,9 @@ def report(
**kwargs, **kwargs,
): ):
try: try:
external_api_report.sync_detailed( api_report.sync_detailed(
client=configured_client, client=configured_client,
json_body=external_api_report.ExternalApiReportJsonBody( json_body=api_report.ReportJsonBody(
**kwargs, **kwargs,
tags=_get_tags(openpipe_options), tags=_get_tags(openpipe_options),
), ),
@@ -112,9 +112,9 @@ async def report_async(
**kwargs, **kwargs,
): ):
try: try:
await external_api_report.asyncio_detailed( await api_report.asyncio_detailed(
client=configured_client, client=configured_client,
json_body=external_api_report.ExternalApiReportJsonBody( json_body=api_report.ReportJsonBody(
**kwargs, **kwargs,
tags=_get_tags(openpipe_options), tags=_get_tags(openpipe_options),
), ),

View File

@@ -1,55 +1,106 @@
from functools import reduce
from dotenv import load_dotenv from dotenv import load_dotenv
from . import openai, configure_openpipe
import os import os
import pytest import pytest
from . import openai, configure_openpipe, configured_client
from .api_client.api.default import local_testing_only_get_latest_logged_call
from .merge_openai_chunks import merge_openai_chunks
import random
import string
def random_string(length):
letters = string.ascii_lowercase
return "".join(random.choice(letters) for i in range(length))
load_dotenv() load_dotenv()
openai.api_key = os.getenv("OPENAI_API_KEY") openai.api_key = os.getenv("OPENAI_API_KEY")
configure_openpipe( configure_openpipe(
base_url="http://localhost:3000/api", api_key=os.getenv("OPENPIPE_API_KEY") base_url="http://localhost:3000/api/v1", api_key=os.getenv("OPENPIPE_API_KEY")
) )
def last_logged_call():
return local_testing_only_get_latest_logged_call.sync(client=configured_client)
def test_sync(): def test_sync():
completion = openai.ChatCompletion.create( completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo", model="gpt-3.5-turbo",
messages=[{"role": "system", "content": "count to 10"}], messages=[{"role": "system", "content": "count to 3"}],
) )
print(completion.choices[0].message.content) last_logged = last_logged_call()
assert (
last_logged.model_response.resp_payload["choices"][0]["message"]["content"]
== completion.choices[0].message.content
)
assert (
last_logged.model_response.req_payload["messages"][0]["content"] == "count to 3"
)
assert completion.openpipe.cache_status == "SKIP"
def test_streaming(): def test_streaming():
completion = openai.ChatCompletion.create( completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo", model="gpt-3.5-turbo",
messages=[{"role": "system", "content": "count to 10"}], messages=[{"role": "system", "content": "count to 4"}],
stream=True, stream=True,
) )
for chunk in completion: merged = reduce(merge_openai_chunks, completion, None)
print(chunk) last_logged = last_logged_call()
assert (
last_logged.model_response.resp_payload["choices"][0]["message"]["content"]
== merged["choices"][0]["message"]["content"]
)
async def test_async(): async def test_async():
acompletion = await openai.ChatCompletion.acreate( completion = await openai.ChatCompletion.acreate(
model="gpt-3.5-turbo", model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "count down from 5"}], messages=[{"role": "user", "content": "count down from 5"}],
) )
last_logged = last_logged_call()
assert (
last_logged.model_response.resp_payload["choices"][0]["message"]["content"]
== completion.choices[0].message.content
)
assert (
last_logged.model_response.req_payload["messages"][0]["content"]
== "count down from 5"
)
print(acompletion.choices[0].message.content) assert completion.openpipe.cache_status == "SKIP"
async def test_async_streaming(): async def test_async_streaming():
acompletion = await openai.ChatCompletion.acreate( completion = await openai.ChatCompletion.acreate(
model="gpt-3.5-turbo", model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "count down from 5"}], messages=[{"role": "user", "content": "count down from 5"}],
stream=True, stream=True,
) )
async for chunk in acompletion: merged = None
print(chunk) async for chunk in completion:
assert chunk.openpipe.cache_status == "SKIP"
merged = merge_openai_chunks(merged, chunk)
last_logged = last_logged_call()
assert (
last_logged.model_response.resp_payload["choices"][0]["message"]["content"]
== merged["choices"][0]["message"]["content"]
)
assert (
last_logged.model_response.req_payload["messages"][0]["content"]
== "count down from 5"
)
assert merged["openpipe"].cache_status == "SKIP"
def test_sync_with_tags(): def test_sync_with_tags():
@@ -58,31 +109,54 @@ def test_sync_with_tags():
messages=[{"role": "system", "content": "count to 10"}], messages=[{"role": "system", "content": "count to 10"}],
openpipe={"tags": {"promptId": "testprompt"}}, openpipe={"tags": {"promptId": "testprompt"}},
) )
print("finished")
print(completion.choices[0].message.content) last_logged = last_logged_call()
assert (
last_logged.model_response.resp_payload["choices"][0]["message"]["content"]
== completion.choices[0].message.content
)
print(last_logged.tags)
assert last_logged.tags["promptId"] == "testprompt"
assert last_logged.tags["$sdk"] == "python"
def test_bad_call(): def test_bad_call():
completion = openai.ChatCompletion.create( try:
model="gpt-3.5-turbo-blaster", completion = openai.ChatCompletion.create(
messages=[{"role": "system", "content": "count to 10"}], model="gpt-3.5-turbo-blaster",
stream=True, messages=[{"role": "system", "content": "count to 10"}],
stream=True,
)
assert False
except Exception as e:
pass
last_logged = last_logged_call()
print(last_logged)
assert (
last_logged.model_response.error_message
== "The model `gpt-3.5-turbo-blaster` does not exist"
) )
assert last_logged.model_response.status_code == 404
@pytest.mark.focus
async def test_caching(): async def test_caching():
messages = [{"role": "system", "content": f"repeat '{random_string(10)}'"}]
completion = openai.ChatCompletion.create( completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo", model="gpt-3.5-turbo",
messages=[{"role": "system", "content": "count to 10"}], messages=messages,
openpipe={"cache": True}, openpipe={"cache": True},
) )
assert completion.openpipe.cache_status == "MISS"
first_logged = last_logged_call()
assert (
completion.choices[0].message.content
== first_logged.model_response.resp_payload["choices"][0]["message"]["content"]
)
completion2 = await openai.ChatCompletion.acreate( completion2 = await openai.ChatCompletion.acreate(
model="gpt-3.5-turbo", model="gpt-3.5-turbo",
messages=[{"role": "system", "content": "count to 10"}], messages=messages,
openpipe={"cache": True}, openpipe={"cache": True},
) )
assert completion2.openpipe.cache_status == "HIT"
print(completion2)

View File

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

View File

@@ -4,7 +4,8 @@
"type": "module", "type": "module",
"description": "Metrics and auto-evaluation for LLM calls", "description": "Metrics and auto-evaluation for LLM calls",
"scripts": { "scripts": {
"build": "tsc" "build": "tsc",
"test": "vitest"
}, },
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@@ -12,7 +13,8 @@
"author": "", "author": "",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "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-beta": "npm:openai@4.0.0-beta.7",
"openai-legacy": "npm:openai@3.3.0" "openai-legacy": "npm:openai@3.3.0"
}, },
@@ -20,6 +22,7 @@
"@types/node": "^20.4.8", "@types/node": "^20.4.8",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"tsx": "^3.12.7", "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,327 +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.0
*
*
* 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 ExternalApiCheckCache200Response
*/
export interface ExternalApiCheckCache200Response {
/**
* JSON-encoded response payload
* @type {any}
* @memberof ExternalApiCheckCache200Response
*/
'respPayload'?: any;
}
/**
*
* @export
* @interface ExternalApiCheckCacheDefaultResponse
*/
export interface ExternalApiCheckCacheDefaultResponse {
/**
*
* @type {string}
* @memberof ExternalApiCheckCacheDefaultResponse
*/
'message': string;
/**
*
* @type {string}
* @memberof ExternalApiCheckCacheDefaultResponse
*/
'code': string;
/**
*
* @type {Array<ExternalApiCheckCacheDefaultResponseIssuesInner>}
* @memberof ExternalApiCheckCacheDefaultResponse
*/
'issues'?: Array<ExternalApiCheckCacheDefaultResponseIssuesInner>;
}
/**
*
* @export
* @interface ExternalApiCheckCacheDefaultResponseIssuesInner
*/
export interface ExternalApiCheckCacheDefaultResponseIssuesInner {
/**
*
* @type {string}
* @memberof ExternalApiCheckCacheDefaultResponseIssuesInner
*/
'message': string;
}
/**
*
* @export
* @interface ExternalApiCheckCacheRequest
*/
export interface ExternalApiCheckCacheRequest {
/**
* Unix timestamp in milliseconds
* @type {number}
* @memberof ExternalApiCheckCacheRequest
*/
'requestedAt': number;
/**
* JSON-encoded request payload
* @type {any}
* @memberof ExternalApiCheckCacheRequest
*/
'reqPayload'?: any;
/**
* Extra tags to attach to the call for filtering. Eg { \"userId\": \"123\", \"promptId\": \"populate-title\" }
* @type {{ [key: string]: string; }}
* @memberof ExternalApiCheckCacheRequest
*/
'tags'?: { [key: string]: string; };
}
/**
*
* @export
* @interface ExternalApiReportRequest
*/
export interface ExternalApiReportRequest {
/**
* Unix timestamp in milliseconds
* @type {number}
* @memberof ExternalApiReportRequest
*/
'requestedAt': number;
/**
* Unix timestamp in milliseconds
* @type {number}
* @memberof ExternalApiReportRequest
*/
'receivedAt': number;
/**
* JSON-encoded request payload
* @type {any}
* @memberof ExternalApiReportRequest
*/
'reqPayload'?: any;
/**
* JSON-encoded response payload
* @type {any}
* @memberof ExternalApiReportRequest
*/
'respPayload'?: any;
/**
* HTTP status code of response
* @type {number}
* @memberof ExternalApiReportRequest
*/
'statusCode'?: number;
/**
* User-friendly error message
* @type {string}
* @memberof ExternalApiReportRequest
*/
'errorMessage'?: string;
/**
* Extra tags to attach to the call for filtering. Eg { \"userId\": \"123\", \"promptId\": \"populate-title\" }
* @type {{ [key: string]: string; }}
* @memberof ExternalApiReportRequest
*/
'tags'?: { [key: string]: string; };
}
/**
* DefaultApi - axios parameter creator
* @export
*/
export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
* Check if a prompt is cached
* @param {ExternalApiCheckCacheRequest} externalApiCheckCacheRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
externalApiCheckCache: async (externalApiCheckCacheRequest: ExternalApiCheckCacheRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'externalApiCheckCacheRequest' is not null or undefined
assertParamExists('externalApiCheckCache', 'externalApiCheckCacheRequest', externalApiCheckCacheRequest)
const localVarPath = `/v1/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(externalApiCheckCacheRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* Report an API call
* @param {ExternalApiReportRequest} externalApiReportRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
externalApiReport: async (externalApiReportRequest: ExternalApiReportRequest, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'externalApiReportRequest' is not null or undefined
assertParamExists('externalApiReport', 'externalApiReportRequest', externalApiReportRequest)
const localVarPath = `/v1/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(externalApiReportRequest, 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 {ExternalApiCheckCacheRequest} externalApiCheckCacheRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async externalApiCheckCache(externalApiCheckCacheRequest: ExternalApiCheckCacheRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ExternalApiCheckCache200Response>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.externalApiCheckCache(externalApiCheckCacheRequest, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
* Report an API call
* @param {ExternalApiReportRequest} externalApiReportRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async externalApiReport(externalApiReportRequest: ExternalApiReportRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<any>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.externalApiReport(externalApiReportRequest, 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 {ExternalApiCheckCacheRequest} externalApiCheckCacheRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
externalApiCheckCache(externalApiCheckCacheRequest: ExternalApiCheckCacheRequest, options?: any): AxiosPromise<ExternalApiCheckCache200Response> {
return localVarFp.externalApiCheckCache(externalApiCheckCacheRequest, options).then((request) => request(axios, basePath));
},
/**
* Report an API call
* @param {ExternalApiReportRequest} externalApiReportRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
externalApiReport(externalApiReportRequest: ExternalApiReportRequest, options?: any): AxiosPromise<any> {
return localVarFp.externalApiReport(externalApiReportRequest, 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 {ExternalApiCheckCacheRequest} externalApiCheckCacheRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DefaultApi
*/
public externalApiCheckCache(externalApiCheckCacheRequest: ExternalApiCheckCacheRequest, options?: AxiosRequestConfig) {
return DefaultApiFp(this.configuration).externalApiCheckCache(externalApiCheckCacheRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
* Report an API call
* @param {ExternalApiReportRequest} externalApiReportRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DefaultApi
*/
public externalApiReport(externalApiReportRequest: ExternalApiReportRequest, options?: AxiosRequestConfig) {
return DefaultApiFp(this.configuration).externalApiReport(externalApiReportRequest, 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.0
*
*
* 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".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.0
*
*
* 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.0
*
*
* 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 */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
/** export { OPClient } from './OPClient';
* OpenPipe API
* The public API for reporting API calls to OpenPipe
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
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 { DefaultService } from './services/DefaultService';
export * from "./configuration";

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 openPipeClient from "../codegen";
// import * as openai from "openai-legacy"; import * as openai from "openai-legacy";
// import { version } from "../package.json"; import { version } from "../../package.json";
// // Anything we don't override we want to pass through to openai directly // Anything we don't override we want to pass through to openai directly
// export * as openAILegacy from "openai-legacy"; export * as openAILegacy from "openai-legacy";
// type OPConfigurationParameters = { type OPConfigurationParameters = {
// apiKey?: string; apiKey?: string;
// basePath?: string; basePath?: string;
// }; };
// export class Configuration extends openai.Configuration { export class Configuration extends openai.Configuration {
// public qkConfig?: openPipeClient.Configuration; public qkConfig?: openPipeClient.Configuration;
// constructor( constructor(
// config: openai.ConfigurationParameters & { config: openai.ConfigurationParameters & {
// opParameters?: OPConfigurationParameters; opParameters?: OPConfigurationParameters;
// } }
// ) { ) {
// super(config); super(config);
// if (config.opParameters) { if (config.opParameters) {
// this.qkConfig = new openPipeClient.Configuration(config.opParameters); this.qkConfig = new openPipeClient.Configuration(config.opParameters);
// } }
// } }
// } }
// type CreateChatCompletion = InstanceType< type CreateChatCompletion = InstanceType<typeof openai.OpenAIApi>["createChatCompletion"];
// typeof openai.OpenAIApi
// >["createChatCompletion"];
// export class OpenAIApi extends openai.OpenAIApi { export class OpenAIApi extends openai.OpenAIApi {
// public openPipeApi?: openPipeClient.DefaultApi; public openPipeApi?: openPipeClient.DefaultApi;
// constructor(config: Configuration) { constructor(config: Configuration) {
// super(config); super(config);
// if (config.qkConfig) { if (config.qkConfig) {
// this.openPipeApi = new openPipeClient.DefaultApi(config.qkConfig); this.openPipeApi = new openPipeClient.DefaultApi(config.qkConfig);
// } }
// } }
// public async createChatCompletion( public async createChatCompletion(
// createChatCompletionRequest: Parameters<CreateChatCompletion>[0], createChatCompletionRequest: Parameters<CreateChatCompletion>[0],
// options?: Parameters<CreateChatCompletion>[1] options?: Parameters<CreateChatCompletion>[1]
// ): ReturnType<CreateChatCompletion> { ): ReturnType<CreateChatCompletion> {
// const requestedAt = Date.now(); const requestedAt = Date.now();
// let resp: Awaited<ReturnType<CreateChatCompletion>> | null = null; let resp: Awaited<ReturnType<CreateChatCompletion>> | null = null;
// let respPayload: openai.CreateChatCompletionResponse | null = null; let respPayload: openai.CreateChatCompletionResponse | null = null;
// let statusCode: number | undefined = undefined; let statusCode: number | undefined = undefined;
// let errorMessage: string | undefined; let errorMessage: string | undefined;
// try { try {
// resp = await super.createChatCompletion( resp = await super.createChatCompletion(createChatCompletionRequest, options);
// createChatCompletionRequest, respPayload = resp.data;
// options statusCode = resp.status;
// ); } catch (err) {
// respPayload = resp.data; console.error("Error in createChatCompletion");
// statusCode = resp.status; if ("isAxiosError" in err && err.isAxiosError) {
// } catch (err) { errorMessage = err.response?.data?.error?.message;
// console.error("Error in createChatCompletion"); respPayload = err.response?.data;
// if ("isAxiosError" in err && err.isAxiosError) { statusCode = err.response?.status;
// errorMessage = err.response?.data?.error?.message; } else if ("message" in err) {
// respPayload = err.response?.data; errorMessage = err.message.toString();
// statusCode = err.response?.status; }
// } else if ("message" in err) { throw err;
// errorMessage = err.message.toString(); } finally {
// } this.openPipeApi
// throw err; ?.externalApiReport({
// } finally { requestedAt,
// this.openPipeApi receivedAt: Date.now(),
// ?.externalApiReport({ reqPayload: createChatCompletionRequest,
// requestedAt, respPayload: respPayload,
// receivedAt: Date.now(), statusCode: statusCode,
// reqPayload: createChatCompletionRequest, errorMessage,
// respPayload: respPayload, tags: {
// statusCode: statusCode, client: "openai-js",
// errorMessage, clientVersion: version,
// tags: { },
// client: "openai-js", })
// clientVersion: version, .catch((err) => {
// }, console.error("Error reporting to OP", err);
// }) });
// .catch((err) => { }
// console.error("Error reporting to OP", err);
// });
// }
// console.log("done"); console.log("done");
// return resp; 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 openai from "openai-beta";
import * as Core from "openai-beta/core";
import { readEnv, type RequestOptions } from "openai-beta/core"; import { readEnv, type RequestOptions } from "openai-beta/core";
import { CompletionCreateParams } from "openai-beta/resources/chat/completions"; import {
import axios from "axios"; ChatCompletion,
ChatCompletionChunk,
CompletionCreateParams,
Completions,
} from "openai-beta/resources/chat/completions";
import * as openPipeClient from "../codegen"; import { DefaultService, OPClient } from "../codegen";
import { Stream } from "openai-beta/streaming";
interface ClientOptions extends openai.ClientOptions { import { OpenPipeArgs, OpenPipeMeta, type OpenPipeConfig, getTags } from "../shared";
openPipeApiKey?: string;
openPipeBaseUrl?: string;
}
export type ClientOptions = openai.ClientOptions & { openpipe?: OpenPipeConfig };
export default class OpenAI extends openai.OpenAI { export default class OpenAI extends openai.OpenAI {
public openPipeApi?: openPipeClient.DefaultApi; public opClient?: OPClient;
constructor({ constructor({ openpipe, ...options }: ClientOptions = {}) {
openPipeApiKey = readEnv("OPENPIPE_API_KEY"), super({ ...options });
openPipeBaseUrl = readEnv("OPENPIPE_BASE_URL") ?? `https://app.openpipe.ai/v1`,
...opts const openPipeApiKey = openpipe?.apiKey ?? readEnv("OPENPIPE_API_KEY");
}: ClientOptions = {}) {
super({ ...opts });
if (openPipeApiKey) { if (openPipeApiKey) {
const axiosInstance = axios.create({ this.chat.setClient(
baseURL: openPipeBaseUrl, new OPClient({
headers: { BASE:
Authorization: `Bearer ${openPipeApiKey}`, openpipe?.baseUrl ?? readEnv("OPENPIPE_BASE_URL") ?? "https://app.openpipe.ai/api/v1",
}, TOKEN: openPipeApiKey,
}); })
this.openPipeApi = new openPipeClient.DefaultApi(
new openPipeClient.Configuration({
apiKey: openPipeApiKey,
basePath: openPipeBaseUrl,
}),
undefined,
axiosInstance
); );
} } else {
console.warn(
// Override the chat property "You're using the OpenPipe client without an API key. No completion requests will be logged."
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 })."
); );
} }
} }
chat: WrappedChat = new WrappedChat(this);
} }
class ExtendedChat extends openai.OpenAI.Chat { class WrappedChat extends openai.OpenAI.Chat {
completions: ExtendedCompletions; 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 ExtendedCompletions extends openai.OpenAI.Chat.Completions { class InstrumentedCompletions extends openai.OpenAI.Chat.Completions {
private openaiInstance: OpenAI; opClient?: OPClient;
constructor(openaiInstance: OpenAI) { constructor(client: openai.OpenAI, opClient?: OPClient) {
super(openaiInstance); super(client);
this.openaiInstance = openaiInstance; 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();
}
}
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( async create(
params: { openpipe, ...body }: CompletionCreateParams & OpenPipeArgs,
| CompletionCreateParams.CreateChatCompletionRequestNonStreaming options?: Core.RequestOptions
| CompletionCreateParams.CreateChatCompletionRequestStreaming, ): Promise<
options?: RequestOptions, Core.APIResponse<(ChatCompletion & { openpipe: OpenPipeMeta }) | Stream<ChatCompletionChunk>>
tags?: Record<string, string> > {
): Promise<any> { console.log("LALALA REPORT", this.opClient);
// // Your pre API call logic here const requestedAt = Date.now();
// console.log("Doing pre API call..."); const cacheRequested = openpipe?.cache ?? false;
// // Determine the type of request if (cacheRequested) {
// if (params.hasOwnProperty("stream") && params.stream === true) { try {
// const result = await super.create( const cached = await this.opClient?.default
// params as CompletionCreateParams.CreateChatCompletionRequestStreaming, .checkCache({
// options requestedAt,
// ); reqPayload: body,
// // Your post API call logic here tags: getTags(openpipe),
// console.log("Doing post API call for Streaming..."); })
// return result; .then((res) => res.respPayload);
// } 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,
// });
// console.log("GOT RESULT", result); if (cached) {
// return result; 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, "incremental": true,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"baseUrl": ".", "baseUrl": ".",
"outDir": "dist", "outDir": "dist"
"paths": {
"~/*": ["./src/*"]
}
}, },
"include": ["src/**/*.ts"], "include": ["src/**/*.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]

162
pnpm-lock.yaml generated
View File

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

View File

@@ -7,9 +7,8 @@ databases:
services: services:
- type: web - type: web
name: querykey-prod-web name: querykey-prod-web
rootDir: app
env: docker env: docker
dockerfilePath: Dockerfile dockerfilePath: ./app/Dockerfile
dockerContext: . dockerContext: .
plan: standard plan: standard
domains: domains:
@@ -32,9 +31,8 @@ services:
- type: web - type: web
name: querykey-prod-wss name: querykey-prod-wss
rootDir: app
env: docker env: docker
dockerfilePath: Dockerfile dockerfilePath: ./app/Dockerfile
dockerContext: . dockerContext: .
plan: free plan: free
dockerCommand: pnpm tsx src/wss-server.ts dockerCommand: pnpm tsx src/wss-server.ts