Compare commits

..

35 Commits

Author SHA1 Message Date
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
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
David Corbitt
e423ad656a Fix ExperimentCard aspect ratio 2023-08-12 23:31:25 -07:00
Kyle Corbitt
7d0d94de3a Merge pull request #149 from OpenPipe/js-client
Load the JS client using pnpm workspaces
2023-08-12 22:56:17 -07:00
Kyle Corbitt
344b257db4 Load the JS client using pnpm workspaces
This makes it so we're using our own openpipe client for all OpenAI calls from the OpenPipe app.

The client doesn't do anything at the moment beyond proxying to the OpenAI lib. But this infra work should make it easier to quickly iterate on the client and test the changes in our own app.
2023-08-12 15:24:48 -07:00
Kyle Corbitt
28b43b6e6d Merge pull request #148 from OpenPipe/js-client
Fix client bugs
2023-08-12 10:38:49 -07:00
Kyle Corbitt
8d373ec9b5 remove unused imports 2023-08-12 10:02:23 -07:00
Kyle Corbitt
537525667d don't reload monaco every render cycle
oops
2023-08-12 09:59:07 -07:00
Kyle Corbitt
519367c553 Fix client bugs
1. PostHog can only be used client-side
2. Can't nest <a> tags in the ProjectMenu
2023-08-12 09:35:52 -07:00
Kyle Corbitt
1a338ec863 Merge pull request #147 from OpenPipe/logs-ui
Style overhaul, make logged calls selectable
2023-08-12 08:48:49 -07:00
David Corbitt
01d0b8f778 Resurrect UserMenu 2023-08-12 04:28:41 -07:00
David Corbitt
d99836ec30 Add experiment button 2023-08-12 04:18:39 -07:00
David Corbitt
33751c12d2 Allow user to select logs 2023-08-12 04:07:58 -07:00
David Corbitt
89815e1f7f Add selectedLogs, rename setSelectedProjectId 2023-08-12 03:35:54 -07:00
David Corbitt
5fa5109f34 Make cache text gray 2023-08-12 03:06:19 -07:00
David Corbitt
b06ab2cbf9 Properly show model 2023-08-12 02:58:28 -07:00
David Corbitt
35fb554038 Center Add Variant button 2023-08-12 02:48:22 -07:00
David Corbitt
f238177277 Fix variant header top right border radius 2023-08-12 02:46:09 -07:00
David Corbitt
723c0f7505 Update colors throughout app 2023-08-12 02:32:09 -07:00
David Corbitt
ce6936f753 Change overall background color and menu 2023-08-12 02:31:52 -07:00
David Corbitt
2a80cbf74a Add relative time back in 2023-08-12 02:06:56 -07:00
David Corbitt
098805ef25 Create loggedCalls.router 2023-08-12 00:03:06 -07:00
David Corbitt
ed90bc5a99 Add dashboard page 2023-08-11 23:34:53 -07:00
arcticfly
de9be8c7ce Allow custom config file (#143)
* Allow custom config file

* Temporarily remove dependency on local openpipe
2023-08-11 23:07:04 -07:00
arcticfly
3e02bcf9b8 Update paginator styles (#142)
* Change paginator icons

* Remove horizontal spacing
2023-08-11 21:11:25 -07:00
Kyle Corbitt
cef2ee31fb Merge pull request #141 from OpenPipe/python-sdk
Add caching in Python
2023-08-11 19:04:18 -07:00
arcticfly
228c547839 Add logged calls pagination (#140)
* Store model on LoggedCall

* Allow mulitple page sizes

* Add logged calls pagination
2023-08-11 19:00:09 -07:00
Kyle Corbitt
e1fcc8fb38 Merge pull request #139 from OpenPipe/python-sdk
Add a python client library
2023-08-11 17:48:07 -07:00
arcticfly
3a908d51aa Store model on LoggedCall (#138) 2023-08-11 16:39:04 -07:00
105 changed files with 3280 additions and 2918 deletions

5
.dockerignore Normal file
View File

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

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
.env
.venv/
*.pyc
node_modules/
*.tsbuildinfo

View File

@@ -32,5 +32,5 @@ NEXT_PUBLIC_HOST="http://localhost:3000"
GITHUB_CLIENT_ID="your_client_id"
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"

View File

@@ -6,7 +6,7 @@ const config = {
overrides: [
{
extends: ["plugin:@typescript-eslint/recommended-requiring-type-checking"],
files: ["*.ts", "*.tsx"],
files: ["*.mts", "*.ts", "*.tsx"],
parserOptions: {
project: path.join(__dirname, "tsconfig.json"),
},

3
app/.gitignore vendored
View File

@@ -44,3 +44,6 @@ yarn-error.log*
# Sentry Auth Token
.sentryclirc
# custom openai intialization
src/server/utils/openaiCustomConfig.json

View File

@@ -12,19 +12,19 @@ declare module "nextjs-routes" {
export type Route =
| StaticRoute<"/account/signin">
| DynamicRoute<"/api/[...trpc]", { "trpc": string[] }>
| DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }>
| StaticRoute<"/api/experiments/og-image">
| StaticRoute<"/api/openapi">
| StaticRoute<"/api/sentry-example-api">
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
| DynamicRoute<"/api/v1/[...trpc]", { "trpc": string[] }>
| StaticRoute<"/api/v1/openapi">
| StaticRoute<"/dashboard">
| DynamicRoute<"/data/[id]", { "id": string }>
| StaticRoute<"/data">
| DynamicRoute<"/experiments/[id]", { "id": string }>
| StaticRoute<"/experiments">
| StaticRoute<"/">
| StaticRoute<"/logged-calls">
| StaticRoute<"/project/settings">
| StaticRoute<"/request-logs">
| StaticRoute<"/sentry-example-page">
| StaticRoute<"/world-champs">
| StaticRoute<"/world-champs/signup">;

View File

@@ -6,13 +6,13 @@ RUN yarn global add pnpm
# 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 pnpm install --frozen-lockfile
RUN cd app && pnpm install --frozen-lockfile
# BUILDER
FROM base as builder
@@ -25,22 +25,24 @@ ARG NEXT_PUBLIC_SENTRY_DSN
ARG SENTRY_AUTH_TOKEN
ARG NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
WORKDIR /code
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 . .
RUN SKIP_ENV_VALIDATION=1 pnpm build
RUN cd app && SKIP_ENV_VALIDATION=1 pnpm build
# RUNNER
FROM base as runner
WORKDIR /app
WORKDIR /code/app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
COPY --from=builder /app/ ./
COPY --from=builder /code/ /code/
EXPOSE 3000
ENV PORT 3000
# Run the "run-prod.sh" script
CMD /app/run-prod.sh
CMD /code/app/run-prod.sh

View File

@@ -36,6 +36,8 @@ let config = {
});
return config;
},
transpilePackages: ["openpipe"],
};
config = nextRoutes()(config);

View File

@@ -1,5 +1,6 @@
{
"name": "openpipe",
"name": "openpipe-app",
"private": true,
"type": "module",
"version": "0.1.0",
"license": "Apache-2.0",
@@ -16,7 +17,7 @@
"postinstall": "prisma generate",
"lint": "next lint",
"start": "next start",
"codegen": "tsx src/server/scripts/client-codegen.ts",
"codegen:clients": "tsx src/server/scripts/client-codegen.ts",
"seed": "tsx prisma/seed.ts",
"check": "concurrently 'pnpm lint' 'pnpm tsc' 'pnpm prettier . --check'",
"test": "pnpm vitest"
@@ -24,7 +25,6 @@
"dependencies": {
"@anthropic-ai/sdk": "^0.5.8",
"@apidevtools/json-schema-ref-parser": "^10.1.0",
"@babel/preset-typescript": "^7.22.5",
"@babel/standalone": "^7.22.9",
"@chakra-ui/anatomy": "^2.2.0",
"@chakra-ui/next-js": "^2.1.4",
@@ -100,7 +100,8 @@
"uuid": "^9.0.0",
"vite-tsconfig-paths": "^4.2.0",
"zod": "^3.21.4",
"zustand": "^4.3.9"
"zustand": "^4.3.9",
"openpipe": "workspace:*"
},
"devDependencies": {
"@openapi-contrib/openapi-schema-to-json-schema": "^4.0.5",

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "LoggedCall" ADD COLUMN "model" TEXT;

View File

@@ -112,17 +112,17 @@ model ScenarioVariantCell {
model ModelResponse {
id String @id @default(uuid()) @db.Uuid
cacheKey String
requestedAt DateTime?
receivedAt DateTime?
respPayload Json?
cost Float?
inputTokens Int?
outputTokens Int?
statusCode Int?
errorMessage String?
retryTime DateTime?
outdated Boolean @default(false)
cacheKey String
requestedAt DateTime?
receivedAt DateTime?
respPayload Json?
cost Float?
inputTokens Int?
outputTokens Int?
statusCode Int?
errorMessage String?
retryTime DateTime?
outdated Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -273,7 +273,8 @@ model LoggedCall {
projectId String @db.Uuid
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
tags LoggedCallTag[]
model String?
tags LoggedCallTag[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -294,7 +295,7 @@ model LoggedCallModelResponse {
errorMessage String?
requestedAt DateTime
receivedAt DateTime
receivedAt DateTime
// 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
@@ -339,8 +340,8 @@ model ApiKey {
name String
apiKey String @unique
projectId String @db.Uuid
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String @db.Uuid
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

View File

@@ -2,6 +2,7 @@ import { prisma } from "~/server/db";
import dedent from "dedent";
import { generateNewCell } from "~/server/utils/generateNewCell";
import { promptConstructorVersion } from "~/promptConstructor/version";
import { env } from "~/env.mjs";
const defaultId = "11111111-1111-1111-1111-111111111111";
@@ -16,6 +17,16 @@ const project =
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({
where: {
id: defaultId,

View File

@@ -33,25 +33,11 @@ export default function AddVariantButton() {
<Flex w="100%" justifyContent="flex-end">
<ActionButton
onClick={onClick}
py={5}
py={7}
leftIcon={<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />}
>
<Text display={{ base: "none", md: "flex" }}>Add Variant</Text>
</ActionButton>
{/* <Button
alignItems="center"
justifyContent="center"
fontWeight="normal"
bgColor="transparent"
_hover={{ bgColor: "gray.100" }}
px={cellPadding.x}
onClick={onClick}
height="unset"
minH={headerMinHeight}
>
<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />
<Text display={{ base: "none", md: "flex" }}>Add Variant</Text>
</Button> */}
</Flex>
);
}

View File

@@ -33,7 +33,7 @@ export default function OutputCell({
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(
{ scenarioId: scenario.id, variantId: variant.id },
{ refetchInterval },
@@ -64,7 +64,8 @@ export default function OutputCell({
cell.retrievalStatus === "PENDING" ||
cell.retrievalStatus === "IN_PROGRESS" ||
hardRefetching;
useEffect(() => setRefetchInterval(awaitingOutput ? 1000 : 0), [awaitingOutput]);
useEffect(() => setRefetchInterval(awaitingOutput ? 1000 : false), [awaitingOutput]);
// TODO: disconnect from socket if we're not streaming anymore
const streamedMessage = useSocket<OutputSchema>(cell?.id);
@@ -120,8 +121,13 @@ export default function OutputCell({
? response.receivedAt.getTime()
: Date.now();
if (response.requestedAt) {
numWaitingMessages = Math.floor(
(relativeWaitingTime - response.requestedAt.getTime()) / WAITING_MESSAGE_INTERVAL,
numWaitingMessages = Math.min(
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 (

View File

@@ -1,21 +1,16 @@
import { type StackProps } from "@chakra-ui/react";
import { useScenarios } from "~/utils/hooks";
import Paginator from "../Paginator";
const ScenarioPaginator = () => {
const ScenarioPaginator = (props: StackProps) => {
const { data } = useScenarios();
if (!data) return null;
const { scenarios, startIndex, lastPage, count } = data;
const { count } = data;
return (
<Paginator
numItemsLoaded={scenarios.length}
startIndex={startIndex}
lastPage={lastPage}
count={count}
/>
);
return <Paginator count={count} condense {...props} />;
};
export default ScenarioPaginator;

View File

@@ -10,6 +10,8 @@ const ScenarioRow = (props: {
variants: PromptVariant[];
canHide: boolean;
rowStart: number;
isFirst: boolean;
isLast: boolean;
}) => {
const [isHovered, setIsHovered] = useState(false);
@@ -21,10 +23,14 @@ const ScenarioRow = (props: {
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
sx={isHovered ? highlightStyle : undefined}
borderLeftWidth={1}
{...borders}
bgColor="white"
rowStart={props.rowStart}
colStart={1}
borderLeftWidth={1}
borderTopWidth={props.isFirst ? 1 : 0}
borderTopLeftRadius={props.isFirst ? 8 : 0}
borderBottomLeftRadius={props.isLast ? 8 : 0}
{...borders}
>
<ScenarioEditor scenario={props.scenario} hovered={isHovered} canHide={props.canHide} />
</GridItem>
@@ -34,8 +40,12 @@ const ScenarioRow = (props: {
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
sx={isHovered ? highlightStyle : undefined}
bgColor="white"
rowStart={props.rowStart}
colStart={i + 2}
borderTopWidth={props.isFirst ? 1 : 0}
borderTopRightRadius={props.isFirst && i === props.variants.length - 1 ? 8 : 0}
borderBottomRightRadius={props.isLast && i === props.variants.length - 1 ? 8 : 0}
{...borders}
>
<OutputCell key={variant.id} scenario={props.scenario} variant={variant} />

View File

@@ -48,7 +48,7 @@ export const ScenariosHeader = () => {
);
return (
<HStack w="100%" pb={cellPadding.y} pt={0} align="center" spacing={0}>
<HStack w="100%" py={cellPadding.y} px={cellPadding.x} align="center" spacing={0}>
<Text fontSize={16} fontWeight="bold">
Scenarios ({scenarios.data?.count})
</Text>
@@ -57,11 +57,16 @@ export const ScenariosHeader = () => {
<MenuButton
as={IconButton}
mt={1}
ml={2}
variant="ghost"
aria-label="Edit Scenarios"
icon={<Icon as={loading ? Spinner : BsGear} />}
maxW={8}
minW={8}
minH={8}
maxH={8}
/>
<MenuList fontSize="md" zIndex="dropdown" mt={-3}>
<MenuList fontSize="md" zIndex="dropdown" mt={-1}>
<MenuItem
icon={<Icon as={BsPlus} boxSize={6} mx="-5px" />}
onClick={() => onAddScenario(false)}

View File

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

View File

@@ -53,20 +53,29 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
colStart: i + 2,
borderLeftWidth: i === 0 ? 1 : 0,
marginLeft: i === 0 ? "-1px" : 0,
backgroundColor: "gray.100",
backgroundColor: "white",
};
const isFirst = i === 0;
const isLast = i === variants.data.length - 1;
return (
<Fragment key={variant.uiId}>
<VariantHeader
variant={variant}
canHide={variants.data.length > 1}
rowStart={1}
borderTopLeftRadius={isFirst ? 8 : 0}
borderTopRightRadius={isLast ? 8 : 0}
{...sharedProps}
/>
<GridItem rowStart={2} {...sharedProps}>
<VariantEditor variant={variant} />
</GridItem>
<GridItem rowStart={3} {...sharedProps}>
<GridItem
rowStart={3}
{...sharedProps}
borderBottomLeftRadius={isFirst ? 8 : 0}
borderBottomRightRadius={isLast ? 8 : 0}
>
<VariantStats variant={variant} />
</GridItem>
</Fragment>
@@ -77,7 +86,6 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
colSpan={allCols - 1}
rowStart={variantHeaderRows + 1}
colStart={1}
{...borders}
borderRightWidth={0}
>
<ScenariosHeader />
@@ -90,6 +98,8 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
scenario={scenario}
variants={variants.data}
canHide={visibleScenariosCount > 1}
isFirst={i === 0}
isLast={i === visibleScenariosCount - 1}
/>
))}
<GridItem

View File

@@ -1,77 +1,117 @@
import { Box, HStack, IconButton } from "@chakra-ui/react";
import {
BsChevronDoubleLeft,
BsChevronDoubleRight,
BsChevronLeft,
BsChevronRight,
} from "react-icons/bs";
import { usePage } from "~/utils/hooks";
import { HStack, IconButton, Text, Select, type StackProps, Icon } from "@chakra-ui/react";
import React, { useCallback } from "react";
import { FiChevronsLeft, FiChevronsRight, FiChevronLeft, FiChevronRight } from "react-icons/fi";
import { usePageParams } from "~/utils/hooks";
const pageSizeOptions = [10, 25, 50, 100];
const Paginator = ({
numItemsLoaded,
startIndex,
lastPage,
count,
}: {
numItemsLoaded: number;
startIndex: number;
lastPage: number;
count: number;
}) => {
const [page, setPage] = usePage();
condense,
...props
}: { count: number; condense?: boolean } & StackProps) => {
const { page, pageSize, setPageParams } = usePageParams();
const lastPage = Math.ceil(count / pageSize);
const updatePageSize = useCallback(
(newPageSize: number) => {
const newPage = Math.floor(((page - 1) * pageSize) / newPageSize) + 1;
setPageParams({ page: newPage, pageSize: newPageSize }, "replace");
},
[page, pageSize, setPageParams],
);
const nextPage = () => {
if (page < lastPage) {
setPage(page + 1, "replace");
setPageParams({ page: page + 1 }, "replace");
}
};
const prevPage = () => {
if (page > 1) {
setPage(page - 1, "replace");
setPageParams({ page: page - 1 }, "replace");
}
};
const goToLastPage = () => setPage(lastPage, "replace");
const goToFirstPage = () => setPage(1, "replace");
const goToLastPage = () => setPageParams({ page: lastPage }, "replace");
const goToFirstPage = () => setPageParams({ page: 1 }, "replace");
return (
<HStack pt={4}>
<IconButton
variant="ghost"
size="sm"
onClick={goToFirstPage}
isDisabled={page === 1}
aria-label="Go to first page"
icon={<BsChevronDoubleLeft />}
/>
<IconButton
variant="ghost"
size="sm"
onClick={prevPage}
isDisabled={page === 1}
aria-label="Previous page"
icon={<BsChevronLeft />}
/>
<Box>
{startIndex}-{startIndex + numItemsLoaded - 1} / {count}
</Box>
<IconButton
variant="ghost"
size="sm"
onClick={nextPage}
isDisabled={page === lastPage}
aria-label="Next page"
icon={<BsChevronRight />}
/>
<IconButton
variant="ghost"
size="sm"
onClick={goToLastPage}
isDisabled={page === lastPage}
aria-label="Go to last page"
icon={<BsChevronDoubleRight />}
/>
<HStack
pt={4}
spacing={8}
justifyContent={condense ? "flex-start" : "space-between"}
alignItems="center"
w="full"
{...props}
>
{!condense && (
<>
<HStack>
<Text>Rows</Text>
<Select
value={pageSize}
onChange={(e) => updatePageSize(parseInt(e.target.value))}
w={20}
backgroundColor="white"
>
{pageSizeOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</Select>
</HStack>
<Text>
Page {page} of {lastPage}
</Text>
</>
)}
<HStack>
<IconButton
variant="outline"
size="sm"
onClick={goToFirstPage}
isDisabled={page === 1}
aria-label="Go to first page"
icon={<Icon as={FiChevronsLeft} boxSize={5} strokeWidth={1.5} />}
bgColor="white"
/>
<IconButton
variant="outline"
size="sm"
onClick={prevPage}
isDisabled={page === 1}
aria-label="Previous page"
icon={<Icon as={FiChevronLeft} boxSize={5} strokeWidth={1.5} />}
bgColor="white"
/>
{condense && (
<Text>
Page {page} of {lastPage}
</Text>
)}
<IconButton
variant="outline"
size="sm"
onClick={nextPage}
isDisabled={page === lastPage}
aria-label="Next page"
icon={<Icon as={FiChevronRight} boxSize={5} strokeWidth={1.5} />}
bgColor="white"
/>
<IconButton
variant="outline"
size="sm"
onClick={goToLastPage}
isDisabled={page === lastPage}
aria-label="Go to last page"
icon={<Icon as={FiChevronsRight} boxSize={5} strokeWidth={1.5} />}
bgColor="white"
/>
</HStack>
</HStack>
);
};

View File

@@ -75,7 +75,7 @@ export default function VariantHeader(
padding={0}
sx={{
position: "sticky",
top: "0",
top: "-2",
// Ensure that the menu always appears above the sticky header of other variants
zIndex: menuOpen ? "dropdown" : 10,
}}
@@ -84,6 +84,7 @@ export default function VariantHeader(
>
<HStack
spacing={2}
py={2}
alignItems="flex-start"
minH={headerMinHeight}
draggable={!isInputHovered}
@@ -102,7 +103,9 @@ export default function VariantHeader(
setIsDragTarget(false);
}}
onDrop={onReorder}
backgroundColor={isDragTarget ? "gray.200" : "gray.100"}
backgroundColor={isDragTarget ? "gray.200" : "white"}
borderTopLeftRadius={gridItemProps.borderTopLeftRadius}
borderTopRightRadius={gridItemProps.borderTopRightRadius}
h="full"
>
<Icon

View File

@@ -1,209 +0,0 @@
import {
Box,
Card,
CardHeader,
Heading,
Table,
Tbody,
Td,
Th,
Thead,
Tr,
Tooltip,
Collapse,
HStack,
VStack,
IconButton,
useToast,
Icon,
Button,
ButtonGroup,
} from "@chakra-ui/react";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { ChevronUpIcon, ChevronDownIcon, CopyIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { type RouterOutputs, api } from "~/utils/api";
import SyntaxHighlighter from "react-syntax-highlighter";
import { atelierCaveLight } from "react-syntax-highlighter/dist/cjs/styles/hljs";
import stringify from "json-stringify-pretty-compact";
import Link from "next/link";
dayjs.extend(relativeTime);
type LoggedCall = RouterOutputs["dashboard"]["loggedCalls"][0];
const FormattedJson = ({ json }: { json: any }) => {
const jsonString = stringify(json, { maxLength: 40 });
const toast = useToast();
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
toast({
title: "Copied to clipboard",
status: "success",
duration: 2000,
});
} catch (err) {
toast({
title: "Failed to copy to clipboard",
status: "error",
duration: 2000,
});
}
};
return (
<Box position="relative" fontSize="sm" borderRadius="md" overflow="hidden">
<SyntaxHighlighter
customStyle={{ overflowX: "unset" }}
language="json"
style={atelierCaveLight}
lineProps={{
style: { wordBreak: "break-all", whiteSpace: "pre-wrap" },
}}
wrapLines
>
{jsonString}
</SyntaxHighlighter>
<IconButton
aria-label="Copy"
icon={<CopyIcon />}
position="absolute"
top={1}
right={1}
size="xs"
variant="ghost"
onClick={() => void copyToClipboard(jsonString)}
/>
</Box>
);
};
function TableRow({
loggedCall,
isExpanded,
onToggle,
}: {
loggedCall: LoggedCall;
isExpanded: boolean;
onToggle: () => void;
}) {
const isError = loggedCall.modelResponse?.statusCode !== 200;
const timeAgo = dayjs(loggedCall.requestedAt).fromNow();
const fullTime = dayjs(loggedCall.requestedAt).toString();
const model = useMemo(
() => loggedCall.tags.find((tag) => tag.name.startsWith("$model"))?.value,
[loggedCall.tags],
);
const durationCell = (
<Td isNumeric>
{loggedCall.cacheHit
? "Cache hit"
: ((loggedCall.modelResponse?.durationMs ?? 0) / 1000).toFixed(2) + "s"}
</Td>
);
return (
<>
<Tr
onClick={onToggle}
key={loggedCall.id}
_hover={{ bgColor: "gray.100", cursor: "pointer" }}
sx={{
"> td": { borderBottom: "none" },
}}
>
<Td>
<Icon boxSize={6} as={isExpanded ? ChevronUpIcon : ChevronDownIcon} />
</Td>
<Td>
<Tooltip label={fullTime} placement="top">
<Box whiteSpace="nowrap" minW="120px">
{timeAgo}
</Box>
</Tooltip>
</Td>
<Td width="100%">{model}</Td>
{durationCell}
<Td isNumeric>{loggedCall.modelResponse?.inputTokens}</Td>
<Td isNumeric>{loggedCall.modelResponse?.outputTokens}</Td>
<Td sx={{ color: isError ? "red.500" : "green.500", fontWeight: "semibold" }} isNumeric>
{loggedCall.modelResponse?.statusCode ?? "No response"}
</Td>
</Tr>
<Tr>
<Td colSpan={8} p={0}>
<Collapse in={isExpanded} unmountOnExit={true}>
<VStack p={4} align="stretch">
<HStack align="stretch">
<VStack flex={1} align="stretch">
<Heading size="sm">Input</Heading>
<FormattedJson json={loggedCall.modelResponse?.reqPayload} />
</VStack>
<VStack flex={1} align="stretch">
<Heading size="sm">Output</Heading>
<FormattedJson json={loggedCall.modelResponse?.respPayload} />
</VStack>
</HStack>
<ButtonGroup alignSelf="flex-end">
<Button as={Link} colorScheme="blue" href={{ pathname: "/experiments" }}>
Experiments
</Button>
</ButtonGroup>
</VStack>
</Collapse>
</Td>
</Tr>
</>
);
}
export default function LoggedCallTable() {
const [expandedRow, setExpandedRow] = useState<string | null>(null);
const loggedCalls = api.dashboard.loggedCalls.useQuery({});
return (
<Card variant="outline" width="100%" overflow="hidden">
<CardHeader>
<Heading as="h3" size="sm">
Logged Calls
</Heading>
</CardHeader>
<Table>
<Thead>
<Tr>
<Th />
<Th>Time</Th>
<Th>Model</Th>
<Th isNumeric>Duration</Th>
<Th isNumeric>Input tokens</Th>
<Th isNumeric>Output tokens</Th>
<Th isNumeric>Status</Th>
</Tr>
</Thead>
<Tbody>
{loggedCalls.data?.map((loggedCall) => {
return (
<TableRow
key={loggedCall.id}
loggedCall={loggedCall}
isExpanded={loggedCall.id === expandedRow}
onToggle={() => {
if (loggedCall.id === expandedRow) {
setExpandedRow(null);
} else {
setExpandedRow(loggedCall.id);
}
}}
/>
);
})}
</Tbody>
</Table>
</Card>
);
}

View File

@@ -0,0 +1,46 @@
import { Card, CardHeader, Heading, Table, Tbody, HStack, Button, Text } from "@chakra-ui/react";
import { useState } from "react";
import Link from "next/link";
import { useLoggedCalls } from "~/utils/hooks";
import { TableHeader, TableRow } from "../requestLogs/TableRow";
export default function LoggedCallsTable() {
const [expandedRow, setExpandedRow] = useState<string | null>(null);
const { data: loggedCalls } = useLoggedCalls();
return (
<Card width="100%" overflow="hidden">
<CardHeader>
<HStack justifyContent="space-between">
<Heading as="h3" size="sm">
Request Logs
</Heading>
<Button as={Link} href="/request-logs" variant="ghost" colorScheme="blue">
<Text>View All</Text>
</Button>
</HStack>
</CardHeader>
<Table>
<TableHeader />
<Tbody>
{loggedCalls?.calls.map((loggedCall) => {
return (
<TableRow
key={loggedCall.id}
loggedCall={loggedCall}
isExpanded={loggedCall.id === expandedRow}
onToggle={() => {
if (loggedCall.id === expandedRow) {
setExpandedRow(null);
} else {
setExpandedRow(loggedCall.id);
}
}}
/>
);
})}
</Tbody>
</Table>
</Card>
);
}

View File

@@ -1,21 +1,16 @@
import { type StackProps } from "@chakra-ui/react";
import { useDatasetEntries } from "~/utils/hooks";
import Paginator from "../Paginator";
const DatasetEntriesPaginator = () => {
const DatasetEntriesPaginator = (props: StackProps) => {
const { data } = useDatasetEntries();
if (!data) return null;
const { entries, startIndex, lastPage, count } = data;
const { count } = data;
return (
<Paginator
numItemsLoaded={entries.length}
startIndex={startIndex}
lastPage={lastPage}
count={count}
/>
);
return <Paginator count={count} {...props} />;
};
export default DatasetEntriesPaginator;

View File

@@ -7,6 +7,7 @@ import {
Spinner,
AspectRatio,
SkeletonText,
Card,
} from "@chakra-ui/react";
import { RiFlaskLine } from "react-icons/ri";
import { formatTimePast } from "~/utils/dayjs";
@@ -29,17 +30,22 @@ type ExperimentData = {
export const ExperimentCard = ({ exp }: { exp: ExperimentData }) => {
return (
<AspectRatio ratio={1.2} w="full">
<Card
w="full"
h="full"
cursor="pointer"
p={4}
bg="white"
borderRadius={4}
_hover={{ bg: "gray.100" }}
transition="background 0.2s"
aspectRatio={1.2}
>
<VStack
as={Link}
w="full"
h="full"
href={{ pathname: "/experiments/[id]", query: { id: exp.id } }}
bg="gray.50"
_hover={{ bg: "gray.100" }}
transition="background 0.2s"
cursor="pointer"
borderColor="gray.200"
borderWidth={1}
p={4}
justify="space-between"
>
<HStack w="full" color="gray.700" justify="center">
@@ -57,7 +63,7 @@ export const ExperimentCard = ({ exp }: { exp: ExperimentData }) => {
<Text flex={1}>Updated {formatTimePast(exp.updatedAt)}</Text>
</HStack>
</VStack>
</AspectRatio>
</Card>
);
};
@@ -89,30 +95,30 @@ export const NewExperimentCard = () => {
}, [createMutation, router, selectedProjectId]);
return (
<AspectRatio ratio={1.2} w="full">
<VStack
align="center"
justify="center"
_hover={{ cursor: "pointer", bg: "gray.50" }}
transition="background 0.2s"
cursor="pointer"
borderColor="gray.200"
borderWidth={1}
p={4}
onClick={createExperiment}
>
<Card
w="full"
h="full"
cursor="pointer"
p={4}
bg="white"
borderRadius={4}
_hover={{ bg: "gray.100" }}
transition="background 0.2s"
aspectRatio={1.2}
>
<VStack align="center" justify="center" w="full" h="full" p={4} onClick={createExperiment}>
<Icon as={isLoading ? Spinner : BsPlusSquare} boxSize={8} />
<Text display={{ base: "none", md: "block" }} ml={2}>
New Experiment
</Text>
</VStack>
</AspectRatio>
</Card>
);
};
export const ExperimentCardSkeleton = () => (
<AspectRatio ratio={1.2} w="full">
<VStack align="center" borderColor="gray.200" borderWidth={1} p={4} bg="gray.50">
<VStack align="center" borderColor="gray.200" borderWidth={1} p={4} bg="white">
<SkeletonText noOfLines={1} w="80%" />
<SkeletonText noOfLines={2} w="60%" />
<SkeletonText noOfLines={1} w="80%" />

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import {
Heading,
VStack,
@@ -9,14 +9,14 @@ import {
Box,
Link as ChakraLink,
Flex,
useBreakpointValue,
} from "@chakra-ui/react";
import Head from "next/head";
import Link from "next/link";
import { BsGearFill, BsGithub, BsPersonCircle } from "react-icons/bs";
import { IoStatsChartOutline } from "react-icons/io5";
import { RiDatabase2Line, RiFlaskLine } from "react-icons/ri";
import { RiHome3Line, RiDatabase2Line, RiFlaskLine } from "react-icons/ri";
import { signIn, useSession } from "next-auth/react";
import UserMenu from "./UserMenu";
import { env } from "~/env.mjs";
import ProjectMenu from "./ProjectMenu";
import NavSidebarOption from "./NavSidebarOption";
@@ -27,10 +27,16 @@ const Divider = () => <Box h="1px" bgColor="gray.300" w="full" />;
const NavSidebar = () => {
const user = useSession().data;
// Hack to get around initial flash, see https://github.com/chakra-ui/chakra-ui/issues/6452
const isMobile = useBreakpointValue({ base: true, md: false, ssr: false });
const renderCount = useRef(0);
renderCount.current++;
const displayLogo = isMobile && renderCount.current > 1;
return (
<VStack
align="stretch"
bgColor="gray.50"
py={2}
px={2}
pb={0}
@@ -40,32 +46,59 @@ const NavSidebar = () => {
borderRightWidth={1}
borderColor="gray.300"
>
<HStack
as={Link}
href="/"
_hover={{ textDecoration: "none" }}
spacing={{ base: 1, md: 0 }}
mx={2}
py={{ base: 1, md: 2 }}
>
<Image src="/logo.svg" alt="" boxSize={6} mr={4} ml={{ base: 0.5, md: 0 }} />
<Heading size="md" fontFamily="inconsolata, monospace">
OpenPipe
</Heading>
</HStack>
<Divider />
{displayLogo && (
<>
<HStack
as={Link}
href="/"
_hover={{ textDecoration: "none" }}
spacing={{ base: 1, md: 0 }}
mx={2}
py={{ base: 1, md: 2 }}
>
<Image src="/logo.svg" alt="" boxSize={6} mr={4} ml={{ base: 0.5, md: 0 }} />
<Heading size="md" fontFamily="inconsolata, monospace">
OpenPipe
</Heading>
</HStack>
<Divider />
</>
)}
<VStack align="flex-start" overflowY="auto" overflowX="hidden" flex={1}>
{user != null && (
<>
<ProjectMenu />
<Divider />
{env.NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS && (
<IconLink icon={IoStatsChartOutline} label="Logged Calls" href="/logged-calls" beta />
<>
<IconLink icon={RiHome3Line} label="Dashboard" href="/dashboard" beta />
<IconLink
icon={IoStatsChartOutline}
label="Request Logs"
href="/request-logs"
beta
/>
</>
)}
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
{env.NEXT_PUBLIC_SHOW_DATA && (
<IconLink icon={RiDatabase2Line} label="Data" href="/data" />
)}
<VStack w="full" alignItems="flex-start" spacing={0} pt={8}>
<Text
pl={2}
pb={2}
fontSize="xs"
fontWeight="bold"
color="gray.500"
display={{ base: "none", md: "flex" }}
>
CONFIGURATION
</Text>
<IconLink icon={BsGearFill} label="Project Settings" href="/project/settings" />
</VStack>
</>
)}
{user === null && (
@@ -87,20 +120,7 @@ const NavSidebar = () => {
</NavSidebarOption>
)}
</VStack>
<VStack w="full" alignItems="flex-start" spacing={0}>
<Text
pl={2}
pb={2}
fontSize="xs"
fontWeight="bold"
color="gray.500"
display={{ base: "none", md: "flex" }}
>
CONFIGURATION
</Text>
<IconLink icon={BsGearFill} label="Project Settings" href="/project/settings" />
</VStack>
{user && <UserMenu user={user} borderColor={"gray.200"} />}
<Divider />
<VStack spacing={0} align="center">
<ChakraLink
@@ -160,7 +180,7 @@ export default function AppShell({
<title>{title ? `${title} | OpenPipe` : "OpenPipe"}</title>
</Head>
<NavSidebar />
<Box h="100%" flex={1} overflowY="auto">
<Box h="100%" flex={1} overflowY="auto" bgColor="gray.50">
{children}
</Box>
</Flex>

View File

@@ -6,16 +6,18 @@ import {
PopoverTrigger,
PopoverContent,
Flex,
IconButton,
Icon,
Divider,
Button,
useDisclosure,
Spinner,
Link as ChakraLink,
Image,
Box,
} from "@chakra-ui/react";
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { BsChevronRight, BsGear, BsPlus } from "react-icons/bs";
import { BsPlus, BsPersonCircle } from "react-icons/bs";
import { type Project } from "@prisma/client";
import { useAppStore } from "~/state/store";
@@ -23,13 +25,14 @@ import { api } from "~/utils/api";
import NavSidebarOption from "./NavSidebarOption";
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
import { useRouter } from "next/router";
import { useSession, signOut } from "next-auth/react";
export default function ProjectMenu() {
const router = useRouter();
const utils = api.useContext();
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
const setselectedProjectId = useAppStore((s) => s.setselectedProjectId);
const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId);
const { data: projects } = api.projects.list.useQuery();
@@ -39,9 +42,9 @@ export default function ProjectMenu() {
projects[0] &&
(!selectedProjectId || !projects.find((proj) => proj.id === selectedProjectId))
) {
setselectedProjectId(projects[0].id);
setSelectedProjectId(projects[0].id);
}
}, [selectedProjectId, setselectedProjectId, projects]);
}, [selectedProjectId, setSelectedProjectId, projects]);
const { data: selectedProject } = useSelectedProject();
@@ -49,28 +52,32 @@ export default function ProjectMenu() {
const createMutation = api.projects.create.useMutation();
const [createProject, isLoading] = useHandledAsyncCallback(async () => {
const newProj = await createMutation.mutateAsync({ name: "New Project" });
const newProj = await createMutation.mutateAsync({ name: "Untitled Project" });
await utils.projects.list.invalidate();
setselectedProjectId(newProj.id);
setSelectedProjectId(newProj.id);
await router.push({ pathname: "/project/settings" });
}, [createMutation, router]);
const user = useSession().data;
const profileImage = user?.user.image ? (
<Image src={user.user.image} alt="profile picture" boxSize={6} borderRadius="50%" />
) : (
<Icon as={BsPersonCircle} boxSize={6} />
);
return (
<VStack w="full" alignItems="flex-start" spacing={0}>
<Text
pl={2}
pb={2}
fontSize="xs"
fontWeight="bold"
color="gray.500"
display={{ base: "none", md: "flex" }}
<VStack w="full" alignItems="flex-start" spacing={0} py={1}>
<Popover
placement="bottom"
isOpen={popover.isOpen}
onOpen={popover.onOpen}
onClose={popover.onClose}
closeOnBlur
>
PROJECT
</Text>
<Popover placement="right-end" isOpen={popover.isOpen} onClose={popover.onClose} closeOnBlur>
<PopoverTrigger>
<NavSidebarOption>
<HStack w="full" onClick={popover.onToggle}>
<HStack w="full">
<Flex
p={1}
borderRadius={4}
@@ -83,20 +90,35 @@ export default function ProjectMenu() {
>
<Text>{selectedProject?.name[0]?.toUpperCase()}</Text>
</Flex>
<Text fontSize="sm" display={{ base: "none", md: "block" }} py={1} flex={1}>
<Text
fontSize="sm"
display={{ base: "none", md: "block" }}
py={1}
flex={1}
fontWeight="bold"
>
{selectedProject?.name}
</Text>
<Icon as={BsChevronRight} boxSize={4} color="gray.500" />
<Box mr={2}>{profileImage}</Box>
</HStack>
</NavSidebarOption>
</PopoverTrigger>
<PopoverContent _focusVisible={{ outline: "unset" }} ml={-1} w="auto" minW={100} maxW={280}>
<VStack alignItems="flex-start" spacing={2} py={4} px={2}>
<Text color="gray.500" fontSize="xs" fontWeight="bold" pb={1}>
PROJECTS
<PopoverContent
_focusVisible={{ outline: "unset" }}
ml={-1}
w={224}
boxShadow="0 0 40px 4px rgba(0, 0, 0, 0.1);"
fontSize="sm"
>
<VStack alignItems="flex-start" spacing={1} py={1}>
<Text px={3} py={2}>
{user?.user.email}
</Text>
<Divider />
<VStack spacing={0} w="full">
<Text alignSelf="flex-start" fontWeight="bold" px={3} pt={2}>
Your Projects
</Text>
<VStack spacing={0} w="full" px={1}>
{projects?.map((proj) => (
<ProjectOption
key={proj.id}
@@ -105,19 +127,38 @@ export default function ProjectMenu() {
onClose={popover.onClose}
/>
))}
<HStack
as={Button}
variant="ghost"
colorScheme="blue"
color="blue.400"
fontSize="sm"
justifyContent="flex-start"
onClick={createProject}
w="full"
borderRadius={4}
spacing={0}
>
<Text>Add project</Text>
<Icon as={isLoading ? Spinner : BsPlus} boxSize={4} strokeWidth={0.5} />
</HStack>
</VStack>
<Divider />
<VStack w="full" px={1}>
<ChakraLink
onClick={() => {
signOut().catch(console.error);
}}
_hover={{ bgColor: "gray.200", textDecoration: "none" }}
w="full"
py={2}
px={2}
borderRadius={4}
>
<Text>Sign out</Text>
</ChakraLink>
</VStack>
<HStack
as={Button}
variant="ghost"
colorScheme="blue"
color="blue.400"
pr={8}
w="full"
onClick={createProject}
>
<Icon as={isLoading ? Spinner : BsPlus} boxSize={6} />
<Text>New project</Text>
</HStack>
</VStack>
</PopoverContent>
</Popover>
@@ -134,38 +175,27 @@ const ProjectOption = ({
isActive: boolean;
onClose: () => void;
}) => {
const setselectedProjectId = useAppStore((s) => s.setselectedProjectId);
const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId);
const [gearHovered, setGearHovered] = useState(false);
return (
<HStack
as={Link}
href="/experiments"
onClick={() => {
setselectedProjectId(proj.id);
setSelectedProjectId(proj.id);
onClose();
}}
w="full"
justifyContent="space-between"
bgColor={isActive ? "gray.100" : "transparent"}
_hover={gearHovered ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
p={2}
color={isActive ? "blue.400" : undefined}
py={2}
px={4}
borderRadius={4}
spacing={4}
>
<Text>{proj.name}</Text>
<IconButton
as={Link}
href="/project/settings"
aria-label={`Open ${proj.name} settings`}
icon={<Icon as={BsGear} boxSize={5} strokeWidth={0.5} color="gray.500" />}
variant="ghost"
size="xs"
p={0}
onMouseEnter={() => setGearHovered(true)}
onMouseLeave={() => setGearHovered(false)}
_hover={{ bgColor: isActive ? "gray.300" : "gray.100", transitionDelay: 0 }}
borderRadius={4}
/>
</HStack>
);
};

View File

@@ -0,0 +1,30 @@
import { Button, HStack, type ButtonProps, Icon, Text } from "@chakra-ui/react";
import { type IconType } from "react-icons";
const ActionButton = ({
icon,
label,
...buttonProps
}: { icon: IconType; label: string } & ButtonProps) => {
return (
<Button
colorScheme="blue"
color="black"
bgColor="white"
borderColor="gray.300"
borderRadius={4}
variant="outline"
size="sm"
fontSize="sm"
fontWeight="normal"
{...buttonProps}
>
<HStack spacing={1}>
{icon && <Icon as={icon} />}
<Text>{label}</Text>
</HStack>
</Button>
);
};
export default ActionButton;

View File

@@ -0,0 +1,55 @@
import { Box, IconButton, useToast } from "@chakra-ui/react";
import { CopyIcon } from "lucide-react";
import SyntaxHighlighter from "react-syntax-highlighter";
import { atelierCaveLight } from "react-syntax-highlighter/dist/cjs/styles/hljs";
import stringify from "json-stringify-pretty-compact";
const FormattedJson = ({ json }: { json: any }) => {
const jsonString = stringify(json, { maxLength: 40 });
const toast = useToast();
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
toast({
title: "Copied to clipboard",
status: "success",
duration: 2000,
});
} catch (err) {
toast({
title: "Failed to copy to clipboard",
status: "error",
duration: 2000,
});
}
};
return (
<Box position="relative" fontSize="sm" borderRadius="md" overflow="hidden">
<SyntaxHighlighter
customStyle={{ overflowX: "unset" }}
language="json"
style={atelierCaveLight}
lineProps={{
style: { wordBreak: "break-all", whiteSpace: "pre-wrap" },
}}
wrapLines
>
{jsonString}
</SyntaxHighlighter>
<IconButton
aria-label="Copy"
icon={<CopyIcon />}
position="absolute"
top={1}
right={1}
size="xs"
variant="ghost"
onClick={() => void copyToClipboard(jsonString)}
/>
</Box>
);
};
export { FormattedJson };

View File

@@ -0,0 +1,16 @@
import { type StackProps } from "@chakra-ui/react";
import { useLoggedCalls } from "~/utils/hooks";
import Paginator from "../Paginator";
const LoggedCallsPaginator = (props: StackProps) => {
const { data } = useLoggedCalls();
if (!data) return null;
const { count } = data;
return <Paginator count={count} {...props} />;
};
export default LoggedCallsPaginator;

View File

@@ -0,0 +1,36 @@
import { Card, Table, Tbody } from "@chakra-ui/react";
import { useState } from "react";
import { useLoggedCalls } from "~/utils/hooks";
import { TableHeader, TableRow } from "./TableRow";
export default function LoggedCallsTable() {
const [expandedRow, setExpandedRow] = useState<string | null>(null);
const { data: loggedCalls } = useLoggedCalls();
return (
<Card width="100%" overflow="hidden">
<Table>
<TableHeader showCheckbox />
<Tbody>
{loggedCalls?.calls.map((loggedCall) => {
return (
<TableRow
key={loggedCall.id}
loggedCall={loggedCall}
isExpanded={loggedCall.id === expandedRow}
onToggle={() => {
if (loggedCall.id === expandedRow) {
setExpandedRow(null);
} else {
setExpandedRow(loggedCall.id);
}
}}
showCheckbox
/>
);
})}
</Tbody>
</Table>
</Card>
);
}

View File

@@ -0,0 +1,164 @@
import {
Box,
Heading,
Td,
Tr,
Thead,
Th,
Tooltip,
Collapse,
HStack,
VStack,
Button,
ButtonGroup,
Text,
Checkbox,
} from "@chakra-ui/react";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import Link from "next/link";
import { type RouterOutputs } from "~/utils/api";
import { FormattedJson } from "./FormattedJson";
import { useAppStore } from "~/state/store";
import { useLoggedCalls } from "~/utils/hooks";
import { useMemo } from "react";
dayjs.extend(relativeTime);
type LoggedCall = RouterOutputs["loggedCalls"]["list"]["calls"][0];
export const TableHeader = ({ showCheckbox }: { showCheckbox?: boolean }) => {
const matchingLogIds = useLoggedCalls().data?.matchingLogIds;
const selectedLogIds = useAppStore((s) => s.selectedLogs.selectedLogIds);
const addAll = useAppStore((s) => s.selectedLogs.addSelectedLogIds);
const clearAll = useAppStore((s) => s.selectedLogs.clearSelectedLogIds);
const allSelected = useMemo(() => {
if (!matchingLogIds) return false;
return matchingLogIds.every((id) => selectedLogIds.has(id));
}, [selectedLogIds, matchingLogIds]);
return (
<Thead>
<Tr>
{showCheckbox && (
<Th>
<HStack w={8}>
<Checkbox
isChecked={allSelected}
onChange={() => {
allSelected ? clearAll() : addAll(matchingLogIds || []);
}}
/>
<Text>({selectedLogIds.size})</Text>
</HStack>
</Th>
)}
<Th>Time</Th>
<Th>Model</Th>
<Th isNumeric>Duration</Th>
<Th isNumeric>Input tokens</Th>
<Th isNumeric>Output tokens</Th>
<Th isNumeric>Status</Th>
</Tr>
</Thead>
);
};
export const TableRow = ({
loggedCall,
isExpanded,
onToggle,
showCheckbox,
}: {
loggedCall: LoggedCall;
isExpanded: boolean;
onToggle: () => void;
showCheckbox?: boolean;
}) => {
const isError = loggedCall.modelResponse?.statusCode !== 200;
const timeAgo = dayjs(loggedCall.requestedAt).fromNow();
const fullTime = dayjs(loggedCall.requestedAt).toString();
const durationCell = (
<Td isNumeric>
{loggedCall.cacheHit ? (
<Text color="gray.500">Cached</Text>
) : (
((loggedCall.modelResponse?.durationMs ?? 0) / 1000).toFixed(2) + "s"
)}
</Td>
);
const isChecked = useAppStore((s) => s.selectedLogs.selectedLogIds.has(loggedCall.id));
const toggleChecked = useAppStore((s) => s.selectedLogs.toggleSelectedLogId);
return (
<>
<Tr
onClick={onToggle}
key={loggedCall.id}
_hover={{ bgColor: "gray.50", cursor: "pointer" }}
sx={{
"> td": { borderBottom: "none" },
}}
>
{showCheckbox && (
<Td>
<Checkbox isChecked={isChecked} onChange={() => toggleChecked(loggedCall.id)} />
</Td>
)}
<Td>
<Tooltip label={fullTime} placement="top">
<Box whiteSpace="nowrap" minW="120px">
{timeAgo}
</Box>
</Tooltip>
</Td>
<Td width="100%">
<HStack justifyContent="flex-start">
<Text
colorScheme="purple"
color="purple.500"
borderColor="purple.500"
px={1}
borderRadius={4}
borderWidth={1}
fontSize="xs"
>
{loggedCall.model}
</Text>
</HStack>
</Td>
{durationCell}
<Td isNumeric>{loggedCall.modelResponse?.inputTokens}</Td>
<Td isNumeric>{loggedCall.modelResponse?.outputTokens}</Td>
<Td sx={{ color: isError ? "red.500" : "green.500", fontWeight: "semibold" }} isNumeric>
{loggedCall.modelResponse?.statusCode ?? "No response"}
</Td>
</Tr>
<Tr>
<Td colSpan={8} p={0}>
<Collapse in={isExpanded} unmountOnExit={true}>
<VStack p={4} align="stretch">
<HStack align="stretch">
<VStack flex={1} align="stretch">
<Heading size="sm">Input</Heading>
<FormattedJson json={loggedCall.modelResponse?.reqPayload} />
</VStack>
<VStack flex={1} align="stretch">
<Heading size="sm">Output</Heading>
<FormattedJson json={loggedCall.modelResponse?.respPayload} />
</VStack>
</HStack>
<ButtonGroup alignSelf="flex-end">
<Button as={Link} colorScheme="blue" href={{ pathname: "/experiments" }}>
Experiments
</Button>
</ButtonGroup>
</VStack>
</Collapse>
</Td>
</Tr>
</>
);
};

View File

@@ -120,9 +120,9 @@ export const refinementActions: Record<string, RefinementAction> = {
"Convert to function call": {
icon: TfiThought,
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", {
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", {
model: "gpt-4",
@@ -156,7 +156,7 @@ export const refinementActions: Record<string, RefinementAction> = {
],
functions: [
{
name: "extract_sentiment",
name: "log_extracted_sentiment",
parameters: {
type: "object", // parameters must always be an object with a properties key
properties: { // properties key is required
@@ -169,13 +169,13 @@ export const refinementActions: Record<string, RefinementAction> = {
},
],
function_call: {
name: "extract_sentiment",
name: "log_extracted_sentiment",
},
});
Here's another example of adding a function:
=========
Before:
Example 2 before:
definePrompt("openai/ChatCompletion", {
model: "gpt-3.5-turbo",
@@ -197,7 +197,7 @@ export const refinementActions: Record<string, RefinementAction> = {
temperature: 0,
});
After:
Example 2 after:
definePrompt("openai/ChatCompletion", {
model: "gpt-3.5-turbo",
@@ -215,7 +215,7 @@ export const refinementActions: Record<string, RefinementAction> = {
temperature: 0,
functions: [
{
name: "score_post",
name: "log_post_score",
parameters: {
type: "object",
properties: {
@@ -227,13 +227,13 @@ export const refinementActions: Record<string, RefinementAction> = {
},
],
function_call: {
name: "score_post",
name: "log_post_score",
},
});
Another example
=========
Before:
Example 3 before:
definePrompt("openai/ChatCompletion", {
model: "gpt-3.5-turbo",
@@ -246,7 +246,7 @@ export const refinementActions: Record<string, RefinementAction> = {
],
});
After:
Example 3 after:
definePrompt("openai/ChatCompletion", {
model: "gpt-3.5-turbo",
@@ -258,22 +258,25 @@ export const refinementActions: Record<string, RefinementAction> = {
],
functions: [
{
name: "write_in_language",
name: "log_translated_text",
parameters: {
type: "object",
properties: {
text: {
translated_text: {
type: "string",
description: "The text, written in the language specified in the prompt",
},
},
},
},
],
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.`,
},
};

View File

@@ -8,7 +8,7 @@ import { ChakraThemeProvider } from "~/theme/ChakraThemeProvider";
import { SyncAppStore } from "~/state/sync";
import NextAdapterApp from "next-query-params/app";
import { QueryParamProvider } from "use-query-params";
import { SessionIdentifier } from "~/utils/analytics/clientAnalytics";
import { PosthogAppProvider } from "~/utils/analytics/posthog";
const MyApp: AppType<{ session: Session | null }> = ({
Component,
@@ -34,14 +34,15 @@ const MyApp: AppType<{ session: Session | null }> = ({
<meta name="twitter:image" content="/og.png" />
</Head>
<SessionProvider session={session}>
<SyncAppStore />
<Favicon />
<SessionIdentifier />
<ChakraThemeProvider>
<QueryParamProvider adapter={NextAdapterApp}>
<Component {...pageProps} />
</QueryParamProvider>
</ChakraThemeProvider>
<PosthogAppProvider>
<SyncAppStore />
<Favicon />
<ChakraThemeProvider>
<QueryParamProvider adapter={NextAdapterApp}>
<Component {...pageProps} />
</QueryParamProvider>
</ChakraThemeProvider>
</PosthogAppProvider>
</SessionProvider>
</>
);

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 cors from "nextjs-cors";
import { createOpenApiNextHandler } from "trpc-openapi";
import { createProcedureCache } from "trpc-openapi/dist/adapters/node-http/procedures";
import { appRouter } from "~/server/api/root.router";
import { createTRPCContext } from "~/server/api/trpc";
import { v1ApiRouter } from "~/server/api/external/v1Api.router";
import { createOpenApiContext } from "~/server/api/external/openApiTrpc";
const openApiHandler = createOpenApiNextHandler({
router: appRouter,
createContext: createTRPCContext,
router: v1ApiRouter,
createContext: createOpenApiContext,
});
const cache = createProcedureCache(appRouter);
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Setup CORS
await cors(req, res);

View File

@@ -1,12 +1,12 @@
import { type NextApiRequest, type NextApiResponse } from "next";
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",
description: "The public API for reporting API calls to OpenPipe",
version: "0.1.0",
baseUrl: "https://app.openpipe.ai/api",
version: "0.1.1",
baseUrl: "https://app.openpipe.ai/api/v1",
});
// Respond with our OpenAPI schema
const hander = (req: NextApiRequest, res: NextApiResponse) => {

View File

@@ -15,20 +15,16 @@ import {
Tr,
Td,
Divider,
Breadcrumb,
BreadcrumbItem,
} from "@chakra-ui/react";
import { Ban, DollarSign, Hash } from "lucide-react";
import AppShell from "~/components/nav/AppShell";
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
import { useSelectedProject } from "~/utils/hooks";
import { api } from "~/utils/api";
import LoggedCallTable from "~/components/dashboard/LoggedCallTable";
import LoggedCallsTable from "~/components/dashboard/LoggedCallsTable";
import UsageGraph from "~/components/dashboard/UsageGraph";
export default function LoggedCalls() {
export default function Dashboard() {
const { data: selectedProject } = useSelectedProject();
const stats = api.dashboard.stats.useQuery(
@@ -37,25 +33,15 @@ export default function LoggedCalls() {
);
return (
<AppShell title="Logged Calls" requireAuth>
<PageHeaderContainer>
<Breadcrumb>
<BreadcrumbItem>
<ProjectBreadcrumbContents />
</BreadcrumbItem>
<BreadcrumbItem isCurrentPage>
<Text>Logged Calls</Text>
</BreadcrumbItem>
</Breadcrumb>
</PageHeaderContainer>
<VStack px={8} pt={4} alignItems="flex-start" spacing={4}>
<AppShell title="Dashboard" requireAuth>
<VStack px={8} py={8} alignItems="flex-start" spacing={4}>
<Text fontSize="2xl" fontWeight="bold">
{selectedProject?.name}
Dashboard
</Text>
<Divider />
<VStack margin="auto" spacing={4} align="stretch" w="full">
<HStack gap={4} align="start">
<Card variant="outline" flex={1}>
<Card flex={1}>
<CardHeader>
<Heading as="h3" size="sm">
Usage Statistics
@@ -66,7 +52,7 @@ export default function LoggedCalls() {
</CardBody>
</Card>
<VStack spacing="4" width="300px" align="stretch">
<Card variant="outline">
<Card>
<CardBody>
<Stat>
<HStack>
@@ -79,7 +65,7 @@ export default function LoggedCalls() {
</Stat>
</CardBody>
</Card>
<Card variant="outline">
<Card>
<CardBody>
<Stat>
<HStack>
@@ -94,7 +80,7 @@ export default function LoggedCalls() {
</Stat>
</CardBody>
</Card>
<Card variant="outline" overflow="hidden">
<Card overflow="hidden">
<Stat>
<CardHeader>
<HStack>
@@ -120,7 +106,7 @@ export default function LoggedCalls() {
</Card>
</VStack>
</HStack>
<LoggedCallTable />
<LoggedCallsTable />
</VStack>
</VStack>
</AppShell>

View File

@@ -62,7 +62,7 @@ export default function Experiment() {
useEffect(() => {
useAppStore.getState().sharedVariantEditor.loadMonaco().catch(console.error);
});
}, []);
const [label, setLabel] = useState(experiment.data?.label || "");
useEffect(() => {

View File

@@ -65,7 +65,7 @@ export default function Settings() {
</BreadcrumbItem>
</Breadcrumb>
</PageHeaderContainer>
<VStack px={8} pt={4} alignItems="flex-start" spacing={4}>
<VStack px={8} py={4} alignItems="flex-start" spacing={4}>
<VStack spacing={0} alignItems="flex-start">
<Text fontSize="2xl" fontWeight="bold">
Project Settings
@@ -80,6 +80,7 @@ export default function Settings() {
borderWidth={1}
borderRadius={4}
borderColor="gray.300"
bgColor="white"
p={6}
spacing={6}
>

View File

@@ -0,0 +1,34 @@
import { Text, VStack, Divider, HStack } from "@chakra-ui/react";
import AppShell from "~/components/nav/AppShell";
import LoggedCallTable from "~/components/requestLogs/LoggedCallsTable";
import LoggedCallsPaginator from "~/components/requestLogs/LoggedCallsPaginator";
import ActionButton from "~/components/requestLogs/ActionButton";
import { useAppStore } from "~/state/store";
import { RiFlaskLine } from "react-icons/ri";
export default function LoggedCalls() {
const selectedLogIds = useAppStore((s) => s.selectedLogs.selectedLogIds);
return (
<AppShell title="Request Logs" requireAuth>
<VStack px={8} py={8} alignItems="flex-start" spacing={4} w="full">
<Text fontSize="2xl" fontWeight="bold">
Request Logs
</Text>
<Divider />
<HStack w="full" justifyContent="flex-end">
<ActionButton
onClick={() => {
console.log("experimenting with these ids", selectedLogIds);
}}
label="Experiment"
icon={RiFlaskLine}
isDisabled={selectedLogIds.size === 0}
/>
</HStack>
<LoggedCallTable />
<LoggedCallsPaginator />
</VStack>
</AppShell>
);
}

View File

@@ -4,11 +4,15 @@ import parserTypescript from "prettier/plugins/typescript";
// @ts-expect-error for some reason missing from types
import parserEstree from "prettier/plugins/estree";
// This emits a warning in the browser "Critical dependency: the request of a
// dependency is an expression". Unfortunately doesn't seem to be a way to get
// around it if we want to use Babel client-side for now. One solution would be
// to just do the formatting server-side in a trpc call.
// https://github.com/babel/babel/issues/14301
import * as babel from "@babel/standalone";
export function stripTypes(tsCode: string): string {
const options = {
presets: ["typescript"],
filename: "file.ts",
};

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 { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { TRPCError } from "@trpc/server";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
import { prisma } from "~/server/db";
import { hashRequest } from "~/server/utils/hashObject";
import modelProvider from "~/modelProviders/openai-ChatCompletion";
@@ -12,6 +9,7 @@ import {
type ChatCompletion,
type CompletionCreateParams,
} from "openai/resources/chat/completions";
import { createOpenApiRouter, openApiProtectedProc } from "./openApiTrpc";
const reqValidator = z.object({
model: z.string(),
@@ -28,12 +26,12 @@ const respValidator = z.object({
),
});
export const externalApiRouter = createTRPCRouter({
checkCache: publicProcedure
export const v1ApiRouter = createOpenApiRouter({
checkCache: openApiProtectedProc
.meta({
openapi: {
method: "POST",
path: "/v1/check-cache",
path: "/check-cache",
description: "Check if a prompt is cached",
protect: true,
},
@@ -47,7 +45,8 @@ export const externalApiRouter = createTRPCRouter({
.optional()
.describe(
'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }',
),
)
.default({}),
}),
)
.output(
@@ -56,18 +55,8 @@ export const externalApiRouter = createTRPCRouter({
}),
)
.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 cacheKey = hashRequest(key.projectId, reqPayload as JsonValue);
const cacheKey = hashRequest(ctx.key.projectId, reqPayload as JsonValue);
const existingResponse = await prisma.loggedCallModelResponse.findFirst({
where: { cacheKey },
@@ -79,23 +68,24 @@ export const externalApiRouter = createTRPCRouter({
await prisma.loggedCall.create({
data: {
projectId: key.projectId,
projectId: ctx.key.projectId,
requestedAt: new Date(input.requestedAt),
cacheHit: true,
modelResponseId: existingResponse.id,
},
});
await createTags(existingResponse.originalLoggedCallId, input.tags);
return {
respPayload: existingResponse.respPayload,
};
}),
report: publicProcedure
report: openApiProtectedProc
.meta({
openapi: {
method: "POST",
path: "/v1/report",
path: "/report",
description: "Report an API call",
protect: true,
},
@@ -113,45 +103,38 @@ export const externalApiRouter = createTRPCRouter({
.optional()
.describe(
'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }',
),
)
.default({}),
}),
)
.output(z.void())
.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 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 newModelResponseId = uuidv4();
let usage;
let model;
if (reqPayload.success && respPayload.success) {
usage = modelProvider.getUsage(
input.reqPayload as CompletionCreateParams,
input.respPayload as ChatCompletion,
);
model = reqPayload.data.model;
}
await prisma.$transaction([
prisma.loggedCall.create({
data: {
id: newLoggedCallId,
projectId: key.projectId,
projectId: ctx.key.projectId,
requestedAt: new Date(input.requestedAt),
cacheHit: false,
model,
},
}),
prisma.loggedCallModelResponse.create({
@@ -182,22 +165,76 @@ export const externalApiRouter = createTRPCRouter({
}),
]);
const tagsToCreate = Object.entries(input.tags ?? {}).map(([name, value]) => ({
loggedCallId: newLoggedCallId,
// sanitize tags
name: name.replaceAll(/[^a-zA-Z0-9_]/g, "_"),
value,
}));
if (reqPayload.success) {
tagsToCreate.push({
loggedCallId: newLoggedCallId,
name: "$model",
value: reqPayload.data.model,
});
await createTags(newLoggedCallId, input.tags);
}),
localTestingOnlyGetLatestLoggedCall: openApiProtectedProc
.meta({
openapi: {
method: "GET",
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");
}
await prisma.loggedCallTag.createMany({
data: tagsToCreate,
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,9 +8,9 @@ import { evaluationsRouter } from "./routers/evaluations.router";
import { worldChampsRouter } from "./routers/worldChamps.router";
import { datasetsRouter } from "./routers/datasets.router";
import { datasetEntries } from "./routers/datasetEntries.router";
import { externalApiRouter } from "./routers/externalApi.router";
import { projectsRouter } from "./routers/projects.router";
import { dashboardRouter } from "./routers/dashboard.router";
import { loggedCallsRouter } from "./routers/loggedCalls.router";
/**
* This is the primary router for your server.
@@ -29,7 +29,7 @@ export const appRouter = createTRPCRouter({
datasetEntries: datasetEntries,
projects: projectsRouter,
dashboard: dashboardRouter,
externalApi: externalApiRouter,
loggedCalls: loggedCallsRouter,
});
// export type definition of API

View File

@@ -1,11 +1,12 @@
import { sql } from "kysely";
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
import { kysely, prisma } from "~/server/db";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import { kysely } from "~/server/db";
import { requireCanViewProject } from "~/utils/accessControl";
import dayjs from "~/utils/dayjs";
export const dashboardRouter = createTRPCRouter({
stats: publicProcedure
stats: protectedProcedure
.input(
z.object({
// TODO: actually take startDate into account
@@ -13,7 +14,8 @@ export const dashboardRouter = createTRPCRouter({
projectId: z.string(),
}),
)
.query(async ({ input }) => {
.query(async ({ input, ctx }) => {
await requireCanViewProject(input.projectId, ctx);
// Return the stats group by hour
const periods = await kysely
.selectFrom("LoggedCall")
@@ -103,16 +105,4 @@ export const dashboardRouter = createTRPCRouter({
return { periods: backfilledPeriods, totals, errors: namedErrors };
}),
// TODO useInfiniteQuery
// https://discord.com/channels/966627436387266600/1122258443886153758/1122258443886153758
loggedCalls: publicProcedure.input(z.object({})).query(async ({ input }) => {
const loggedCalls = await prisma.loggedCall.findMany({
orderBy: { requestedAt: "desc" },
include: { tags: true, modelResponse: true },
take: 20,
});
return loggedCalls;
}),
});

View File

@@ -4,23 +4,21 @@ import { prisma } from "~/server/db";
import { requireCanModifyDataset, requireCanViewDataset } from "~/utils/accessControl";
import { autogenerateDatasetEntries } from "../autogenerate/autogenerateDatasetEntries";
const PAGE_SIZE = 10;
export const datasetEntries = createTRPCRouter({
list: protectedProcedure
.input(z.object({ datasetId: z.string(), page: z.number() }))
.input(z.object({ datasetId: z.string(), page: z.number(), pageSize: z.number() }))
.query(async ({ input, ctx }) => {
await requireCanViewDataset(input.datasetId, ctx);
const { datasetId, page } = input;
const { datasetId, page, pageSize } = input;
const entries = await prisma.datasetEntry.findMany({
where: {
datasetId,
},
orderBy: { createdAt: "desc" },
skip: (page - 1) * PAGE_SIZE,
take: PAGE_SIZE,
skip: (page - 1) * pageSize,
take: pageSize,
});
const count = await prisma.datasetEntry.count({
@@ -31,8 +29,6 @@ export const datasetEntries = createTRPCRouter({
return {
entries,
startIndex: (page - 1) * PAGE_SIZE + 1,
lastPage: Math.ceil(count / PAGE_SIZE),
count,
};
}),

View File

@@ -0,0 +1,33 @@
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
import { prisma } from "~/server/db";
import { requireCanViewProject } from "~/utils/accessControl";
export const loggedCallsRouter = createTRPCRouter({
list: protectedProcedure
.input(z.object({ projectId: z.string(), page: z.number(), pageSize: z.number() }))
.query(async ({ input, ctx }) => {
const { projectId, page, pageSize } = input;
await requireCanViewProject(projectId, ctx);
const calls = await prisma.loggedCall.findMany({
where: { projectId },
orderBy: { requestedAt: "desc" },
include: { tags: true, modelResponse: true },
skip: (page - 1) * pageSize,
take: pageSize,
});
const matchingLogs = await prisma.loggedCall.findMany({
where: { projectId },
select: { id: true },
});
const count = await prisma.loggedCall.count({
where: { projectId },
});
return { calls, count, matchingLogIds: matchingLogs.map((log) => log.id) };
}),
});

View File

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

View File

@@ -7,15 +7,13 @@ import { runAllEvals } from "~/server/utils/evaluations";
import { generateNewCell } from "~/server/utils/generateNewCell";
import { requireCanModifyExperiment, requireCanViewExperiment } from "~/utils/accessControl";
const PAGE_SIZE = 10;
export const scenariosRouter = createTRPCRouter({
list: publicProcedure
.input(z.object({ experimentId: z.string(), page: z.number() }))
.input(z.object({ experimentId: z.string(), page: z.number(), pageSize: z.number() }))
.query(async ({ input, ctx }) => {
await requireCanViewExperiment(input.experimentId, ctx);
const { experimentId, page } = input;
const { experimentId, page, pageSize } = input;
const scenarios = await prisma.testScenario.findMany({
where: {
@@ -23,8 +21,8 @@ export const scenariosRouter = createTRPCRouter({
visible: true,
},
orderBy: { sortIndex: "asc" },
skip: (page - 1) * PAGE_SIZE,
take: PAGE_SIZE,
skip: (page - 1) * pageSize,
take: pageSize,
});
const count = await prisma.testScenario.count({
@@ -36,8 +34,6 @@ export const scenariosRouter = createTRPCRouter({
return {
scenarios,
startIndex: (page - 1) * PAGE_SIZE + 1,
lastPage: Math.ceil(count / PAGE_SIZE),
count,
};
}),

View File

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

View File

@@ -1,22 +1,21 @@
import "dotenv/config";
import { openApiDocument } from "~/pages/api/openapi.json";
import { openApiDocument } from "~/pages/api/v1/openapi.json";
import fs from "fs";
import path from "path";
import { execSync } from "child_process";
console.log("Exporting public OpenAPI schema to client-libs/schema.json");
const scriptPath = import.meta.url.replace("file://", "");
const clientLibsPath = path.join(path.dirname(scriptPath), "../../../../client-libs");
const schemaPath = path.join(clientLibsPath, "schema.json");
const schemaPath = path.join(clientLibsPath, "openapi.json");
console.log(`Exporting public OpenAPI schema to ${schemaPath}`);
console.log("Exporting schema");
fs.writeFileSync(schemaPath, JSON.stringify(openApiDocument, null, 2), "utf-8");
console.log("Generating Typescript client");
console.log("Generating TypeScript client");
const tsClientPath = path.join(clientLibsPath, "typescript/codegen");
const tsClientPath = path.join(clientLibsPath, "typescript/src/codegen");
fs.rmSync(tsClientPath, { recursive: true, force: true });
@@ -27,6 +26,8 @@ execSync(
},
);
console.log("Done!");
console.log("Generating Python client");
process.exit(0);
execSync(path.join(clientLibsPath, "python/codegen.sh"));
console.log("Done!");

View File

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

View File

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

View File

@@ -1,13 +1,24 @@
import { type ClientOptions } from "openai";
import fs from "fs";
import path from "path";
import OpenAI from "openpipe/src/openai";
import { env } from "~/env.mjs";
import { default as OriginalOpenAI } from "openai";
// import { OpenAI } from "openpipe";
let config: ClientOptions;
const openAIConfig = { apiKey: env.OPENAI_API_KEY ?? "dummy-key" };
try {
// Allow developers to override the config with a local file
const jsonData = fs.readFileSync(
path.join(path.dirname(import.meta.url).replace("file://", ""), "./openaiCustomConfig.json"),
"utf8",
);
config = JSON.parse(jsonData.toString());
} catch (error) {
// Set a dummy key so it doesn't fail at build time
config = { apiKey: env.OPENAI_API_KEY ?? "dummy-key" };
}
// Set a dummy key so it doesn't fail at build time
// export const openai = env.OPENPIPE_API_KEY
// ? new OpenAI.OpenAI(openAIConfig)
// : new OriginalOpenAI(openAIConfig);
// export const openai = env.OPENPIPE_API_KEY ? new OpenAI.OpenAI(config) : new OriginalOpenAI(config);
export const openai = new OriginalOpenAI(openAIConfig);
export const openai = new OpenAI(config);

View File

@@ -0,0 +1,30 @@
import { type SliceCreator } from "./store";
export const editorBackground = "#fafafa";
export type SelectedLogsSlice = {
selectedLogIds: Set<string>;
toggleSelectedLogId: (id: string) => void;
addSelectedLogIds: (ids: string[]) => void;
clearSelectedLogIds: () => void;
};
export const createSelectedLogsSlice: SliceCreator<SelectedLogsSlice> = (set, get) => ({
selectedLogIds: new Set(),
toggleSelectedLogId: (id: string) =>
set((state) => {
if (state.selectedLogs.selectedLogIds.has(id)) {
state.selectedLogs.selectedLogIds.delete(id);
} else {
state.selectedLogs.selectedLogIds.add(id);
}
}),
addSelectedLogIds: (ids: string[]) =>
set((state) => {
state.selectedLogs.selectedLogIds = new Set([...state.selectedLogs.selectedLogIds, ...ids]);
}),
clearSelectedLogIds: () =>
set((state) => {
state.selectedLogs.selectedLogIds = new Set();
}),
});

View File

@@ -81,8 +81,6 @@ export const createVariantEditorSlice: SliceCreator<SharedVariantEditorSlice> =
};
`;
console.log(modelContents);
const scenariosModel = monaco.editor.getModel(monaco.Uri.parse("file:///scenarios.ts"));
if (scenariosModel) {

View File

@@ -1,5 +1,6 @@
import { type StateCreator, create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { enableMapSet } from "immer";
import { persist } from "zustand/middleware";
import { createSelectors } from "./createSelectors";
import {
@@ -8,6 +9,9 @@ import {
} from "./sharedVariantEditor.slice";
import { type APIClient } from "~/utils/api";
import { persistOptions, type stateToPersist } from "./persist";
import { type SelectedLogsSlice, createSelectedLogsSlice } from "./selectedLogsSlice";
enableMapSet();
export type State = {
drawerOpen: boolean;
@@ -17,7 +21,8 @@ export type State = {
setApi: (api: APIClient) => void;
sharedVariantEditor: SharedVariantEditorSlice;
selectedProjectId: string | null;
setselectedProjectId: (id: string) => void;
setSelectedProjectId: (id: string) => void;
selectedLogs: SelectedLogsSlice;
};
export type SliceCreator<T> = StateCreator<State, [["zustand/immer", never]], [], T>;
@@ -48,10 +53,11 @@ const useBaseStore = create<
}),
sharedVariantEditor: createVariantEditorSlice(set, get, ...rest),
selectedProjectId: null,
setselectedProjectId: (id: string) =>
setSelectedProjectId: (id: string) =>
set((state) => {
state.selectedProjectId = id;
}),
selectedLogs: createSelectedLogsSlice(set, get, ...rest),
})),
persistOptions,
),

View File

@@ -1,31 +0,0 @@
import { type Session } from "next-auth";
import { useSession } from "next-auth/react";
import { useEffect } from "react";
import posthog from "posthog-js";
import { env } from "~/env.mjs";
// Make sure we're in the browser
const enableBrowserAnalytics = typeof window !== "undefined";
if (env.NEXT_PUBLIC_POSTHOG_KEY && enableBrowserAnalytics) {
posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: `${env.NEXT_PUBLIC_HOST}/ingest`,
});
}
export const identifySession = (session: Session) => {
if (!session.user) return;
posthog.identify(session.user.id, {
name: session.user.name,
email: session.user.email,
});
};
export const SessionIdentifier = () => {
const session = useSession().data;
useEffect(() => {
if (session && enableBrowserAnalytics) identifySession(session);
}, [session]);
return null;
};

View File

@@ -0,0 +1,41 @@
"use client";
import { useSession } from "next-auth/react";
import React, { type ReactNode, useEffect } from "react";
import { PostHogProvider } from "posthog-js/react";
import posthog from "posthog-js";
import { env } from "~/env.mjs";
import { useRouter } from "next/router";
// Make sure we're in the browser
const inBrowser = typeof window !== "undefined";
export const PosthogAppProvider = ({ children }: { children: ReactNode }) => {
const session = useSession().data;
const router = useRouter();
useEffect(() => {
// Track page views
const handleRouteChange = () => posthog?.capture("$pageview");
router.events.on("routeChangeComplete", handleRouteChange);
return () => {
router.events.off("routeChangeComplete", handleRouteChange);
};
}, [router.events]);
useEffect(() => {
if (env.NEXT_PUBLIC_POSTHOG_KEY && inBrowser && session && session.user) {
posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: `${env.NEXT_PUBLIC_HOST}/ingest`,
});
posthog.identify(session.user.id, {
name: session.user.name,
email: session.user.email,
});
}
}, [session]);
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
};

View File

@@ -1,7 +1,7 @@
import { useRouter } from "next/router";
import { type RefObject, useCallback, useEffect, useRef, useState } from "react";
import { api } from "~/utils/api";
import { NumberParam, useQueryParam, withDefault } from "use-query-params";
import { NumberParam, useQueryParams } from "use-query-params";
import { useAppStore } from "~/state/store";
export const useExperiments = () => {
@@ -46,10 +46,10 @@ export const useDataset = () => {
export const useDatasetEntries = () => {
const dataset = useDataset();
const [page] = usePage();
const { page, pageSize } = usePageParams();
return api.datasetEntries.list.useQuery(
{ datasetId: dataset.data?.id ?? "", page },
{ datasetId: dataset.data?.id ?? "", page, pageSize },
{ enabled: dataset.data?.id != null },
);
};
@@ -132,14 +132,23 @@ export const useElementDimensions = (): [RefObject<HTMLElement>, Dimensions | un
return [ref, dimensions];
};
export const usePage = () => useQueryParam("page", withDefault(NumberParam, 1));
export const usePageParams = () => {
const [pageParams, setPageParams] = useQueryParams({
page: NumberParam,
pageSize: NumberParam,
});
const { page, pageSize } = pageParams;
return { page: page || 1, pageSize: pageSize || 10, setPageParams };
};
export const useScenarios = () => {
const experiment = useExperiment();
const [page] = usePage();
const { page, pageSize } = usePageParams();
return api.scenarios.list.useQuery(
{ experimentId: experiment.data?.id ?? "", page },
{ experimentId: experiment.data?.id ?? "", page, pageSize },
{ enabled: experiment.data?.id != null },
);
};
@@ -166,3 +175,13 @@ export const useScenarioVars = () => {
{ enabled: experiment.data?.id != null },
);
};
export const useLoggedCalls = () => {
const selectedProjectId = useAppStore((state) => state.selectedProjectId);
const { page, pageSize } = usePageParams();
return api.loggedCalls.list.useQuery(
{ projectId: selectedProjectId ?? "", page, pageSize },
{ enabled: !!selectedProjectId },
);
};

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

@@ -25,11 +25,11 @@
".eslintrc.cjs",
"next-env.d.ts",
"**/*.ts",
"**/*.mts",
"**/*.tsx",
"**/*.cjs",
"**/*.mjs",
"**/*.js",
"src/pages/api/sentry-example-api.js"
"**/*.js"
],
"exclude": ["node_modules"]
}

275
client-libs/openapi.json Normal file
View File

@@ -0,0 +1,275 @@
{
"openapi": "3.0.3",
"info": {
"title": "OpenPipe API",
"description": "The public API for reporting API calls to OpenPipe",
"version": "0.1.1"
},
"servers": [
{
"url": "https://app.openpipe.ai/api/v1"
}
],
"paths": {
"/check-cache": {
"post": {
"operationId": "checkCache",
"description": "Check if a prompt is cached",
"security": [
{
"Authorization": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"requestedAt": {
"type": "number",
"description": "Unix timestamp in milliseconds"
},
"reqPayload": {
"description": "JSON-encoded request payload"
},
"tags": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Extra tags to attach to the call for filtering. Eg { \"userId\": \"123\", \"promptId\": \"populate-title\" }",
"default": {}
}
},
"required": [
"requestedAt"
],
"additionalProperties": false
}
}
}
},
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"respPayload": {
"description": "JSON-encoded response payload"
}
},
"additionalProperties": false
}
}
}
},
"default": {
"$ref": "#/components/responses/error"
}
}
}
},
"/report": {
"post": {
"operationId": "report",
"description": "Report an API call",
"security": [
{
"Authorization": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"requestedAt": {
"type": "number",
"description": "Unix timestamp in milliseconds"
},
"receivedAt": {
"type": "number",
"description": "Unix timestamp in milliseconds"
},
"reqPayload": {
"description": "JSON-encoded request payload"
},
"respPayload": {
"description": "JSON-encoded response payload"
},
"statusCode": {
"type": "number",
"description": "HTTP status code of response"
},
"errorMessage": {
"type": "string",
"description": "User-friendly error message"
},
"tags": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Extra tags to attach to the call for filtering. Eg { \"userId\": \"123\", \"promptId\": \"populate-title\" }",
"default": {}
}
},
"required": [
"requestedAt",
"receivedAt"
],
"additionalProperties": false
}
}
}
},
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {}
}
}
},
"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
}
}
}
},
"default": {
"$ref": "#/components/responses/error"
}
}
}
}
},
"components": {
"securitySchemes": {
"Authorization": {
"type": "http",
"scheme": "bearer"
}
},
"responses": {
"error": {
"description": "Error response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"code": {
"type": "string"
},
"issues": {
"type": "array",
"items": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"additionalProperties": false
}
}
},
"required": [
"message",
"code"
],
"additionalProperties": false
}
}
}
}
}
}
}

View File

@@ -4,7 +4,7 @@ set -e
cd "$(dirname "$0")"
poetry run openapi-python-client generate --url http://localhost:3000/api/openapi.json
poetry run openapi-python-client generate --path ../openapi.json
rm -rf openpipe/api_client
mv open-pipe-api-client/open_pipe_api_client openpipe/api_client

View File

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

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

@@ -5,13 +5,13 @@ import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.external_api_report_json_body import ExternalApiReportJsonBody
from ...models.report_json_body import ReportJsonBody
from ...types import Response
def _get_kwargs(
*,
json_body: ExternalApiReportJsonBody,
json_body: ReportJsonBody,
) -> Dict[str, Any]:
pass
@@ -19,7 +19,7 @@ def _get_kwargs(
return {
"method": "post",
"url": "/v1/report",
"url": "/report",
"json": json_json_body,
}
@@ -45,12 +45,12 @@ def _build_response(*, client: Union[AuthenticatedClient, Client], response: htt
def sync_detailed(
*,
client: AuthenticatedClient,
json_body: ExternalApiReportJsonBody,
json_body: ReportJsonBody,
) -> Response[Any]:
"""Report an API call
Args:
json_body (ExternalApiReportJsonBody):
json_body (ReportJsonBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
@@ -74,12 +74,12 @@ def sync_detailed(
async def asyncio_detailed(
*,
client: AuthenticatedClient,
json_body: ExternalApiReportJsonBody,
json_body: ReportJsonBody,
) -> Response[Any]:
"""Report an API call
Args:
json_body (ExternalApiReportJsonBody):
json_body (ReportJsonBody):
Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.

View File

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

View File

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

View File

@@ -4,11 +4,11 @@ from attrs import define
from ..types import UNSET, Unset
T = TypeVar("T", bound="ExternalApiCheckCacheResponse200")
T = TypeVar("T", bound="CheckCacheResponse200")
@define
class ExternalApiCheckCacheResponse200:
class CheckCacheResponse200:
"""
Attributes:
resp_payload (Union[Unset, Any]): JSON-encoded response payload
@@ -31,8 +31,8 @@ class ExternalApiCheckCacheResponse200:
d = src_dict.copy()
resp_payload = d.pop("respPayload", UNSET)
external_api_check_cache_response_200 = cls(
check_cache_response_200 = cls(
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
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
class ExternalApiReportJsonBody:
class ReportJsonBody:
"""
Attributes:
requested_at (float): Unix timestamp in milliseconds
@@ -21,8 +21,8 @@ class ExternalApiReportJsonBody:
resp_payload (Union[Unset, Any]): JSON-encoded response payload
status_code (Union[Unset, float]): HTTP status code of response
error_message (Union[Unset, str]): User-friendly error message
tags (Union[Unset, ExternalApiReportJsonBodyTags]): Extra tags to attach to the call for filtering. Eg {
"userId": "123", "promptId": "populate-title" }
tags (Union[Unset, ReportJsonBodyTags]): Extra tags to attach to the call for filtering. Eg { "userId": "123",
"promptId": "populate-title" }
"""
requested_at: float
@@ -31,7 +31,7 @@ class ExternalApiReportJsonBody:
resp_payload: Union[Unset, Any] = UNSET
status_code: Union[Unset, float] = UNSET
error_message: Union[Unset, str] = UNSET
tags: Union[Unset, "ExternalApiReportJsonBodyTags"] = UNSET
tags: Union[Unset, "ReportJsonBodyTags"] = UNSET
def to_dict(self) -> Dict[str, Any]:
requested_at = self.requested_at
@@ -66,7 +66,7 @@ class ExternalApiReportJsonBody:
@classmethod
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()
requested_at = d.pop("requestedAt")
@@ -82,13 +82,13 @@ class ExternalApiReportJsonBody:
error_message = d.pop("errorMessage", UNSET)
_tags = d.pop("tags", UNSET)
tags: Union[Unset, ExternalApiReportJsonBodyTags]
tags: Union[Unset, ReportJsonBodyTags]
if isinstance(_tags, Unset):
tags = UNSET
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,
received_at=received_at,
req_payload=req_payload,
@@ -98,4 +98,4 @@ class ExternalApiReportJsonBody:
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
T = TypeVar("T", bound="ExternalApiCheckCacheJsonBodyTags")
T = TypeVar("T", bound="ReportJsonBodyTags")
@define
class ExternalApiCheckCacheJsonBodyTags:
class ReportJsonBodyTags:
"""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)
@@ -21,10 +21,10 @@ class ExternalApiCheckCacheJsonBodyTags:
@classmethod
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
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
return external_api_check_cache_json_body_tags
report_json_body_tags.additional_properties = d
return report_json_body_tags
@property
def additional_keys(self) -> List[str]:

View File

@@ -1,9 +1,9 @@
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:
return merge_streamed_chunks({**chunk, "choices": []}, chunk)
return merge_openai_chunks({**chunk, "choices": []}, chunk)
choices = base["choices"].copy()
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"]}}
)
merged = {
return {
**base,
"choices": choices,
}
return merged

View File

@@ -3,9 +3,16 @@ from openai.openai_object import OpenAIObject
import time
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):
@@ -29,9 +36,15 @@ class WrappedChatCompletion(original_openai.ChatCompletion):
def _gen():
assembled_completion = None
for chunk in chat_completion:
assembled_completion = merge_streamed_chunks(
assembled_completion = merge_openai_chunks(
assembled_completion, chunk
)
cache_status = (
"MISS" if _should_check_cache(openpipe_options) else "SKIP"
)
chunk.openpipe = OpenPipeMeta(cache_status=cache_status)
yield chunk
received_at = int(time.time() * 1000)
@@ -58,6 +71,10 @@ class WrappedChatCompletion(original_openai.ChatCompletion):
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
except Exception as e:
received_at = int(time.time() * 1000)
@@ -96,21 +113,28 @@ class WrappedChatCompletion(original_openai.ChatCompletion):
requested_at = int(time.time() * 1000)
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
for chunk in chat_completion:
assembled_completion = merge_streamed_chunks(
async for chunk in chat_completion:
assembled_completion = merge_openai_chunks(
assembled_completion, chunk
)
cache_status = (
"MISS" if _should_check_cache(openpipe_options) else "SKIP"
)
chunk.openpipe = OpenPipeMeta(cache_status=cache_status)
yield chunk
received_at = int(time.time() * 1000)
report_async(
await report_async(
openpipe_options=openpipe_options,
requested_at=requested_at,
received_at=received_at,
@@ -123,7 +147,7 @@ class WrappedChatCompletion(original_openai.ChatCompletion):
else:
received_at = int(time.time() * 1000)
report_async(
await report_async(
openpipe_options=openpipe_options,
requested_at=requested_at,
received_at=received_at,
@@ -132,12 +156,17 @@ class WrappedChatCompletion(original_openai.ChatCompletion):
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
except Exception as e:
received_at = int(time.time() * 1000)
if isinstance(e, original_openai.OpenAIError):
report_async(
await report_async(
openpipe_options=openpipe_options,
requested_at=requested_at,
received_at=received_at,
@@ -147,7 +176,7 @@ class WrappedChatCompletion(original_openai.ChatCompletion):
status_code=e.http_status,
)
else:
report_async(
await report_async(
openpipe_options=openpipe_options,
requested_at=requested_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 (
external_api_report,
external_api_check_cache,
report as api_report,
check_cache,
)
from openpipe.api_client.client import AuthenticatedClient
from openpipe.api_client.models.external_api_report_json_body_tags import (
ExternalApiReportJsonBodyTags,
from openpipe.api_client.models.report_json_body_tags import (
ReportJsonBodyTags,
)
import toml
import time
@@ -21,7 +21,7 @@ def _get_tags(openpipe_options):
tags["$sdk"] = "python"
tags["$sdk_version"] = version
return ExternalApiReportJsonBodyTags.from_dict(tags)
return ReportJsonBodyTags.from_dict(tags)
def _should_check_cache(openpipe_options):
@@ -31,7 +31,7 @@ def _should_check_cache(openpipe_options):
def _process_cache_payload(
payload: external_api_check_cache.ExternalApiCheckCacheResponse200,
payload: check_cache.CheckCacheResponse200,
):
if not payload or not payload.resp_payload:
return None
@@ -47,9 +47,9 @@ def maybe_check_cache(
if not _should_check_cache(openpipe_options):
return None
try:
payload = external_api_check_cache.sync(
payload = check_cache.sync(
client=configured_client,
json_body=external_api_check_cache.ExternalApiCheckCacheJsonBody(
json_body=check_cache.CheckCacheJsonBody(
req_payload=req_payload,
requested_at=int(time.time() * 1000),
tags=_get_tags(openpipe_options),
@@ -72,9 +72,9 @@ async def maybe_check_cache_async(
return None
try:
payload = await external_api_check_cache.asyncio(
payload = await check_cache.asyncio(
client=configured_client,
json_body=external_api_check_cache.ExternalApiCheckCacheJsonBody(
json_body=check_cache.CheckCacheJsonBody(
req_payload=req_payload,
requested_at=int(time.time() * 1000),
tags=_get_tags(openpipe_options),
@@ -94,9 +94,9 @@ def report(
**kwargs,
):
try:
external_api_report.sync_detailed(
api_report.sync_detailed(
client=configured_client,
json_body=external_api_report.ExternalApiReportJsonBody(
json_body=api_report.ReportJsonBody(
**kwargs,
tags=_get_tags(openpipe_options),
),
@@ -112,9 +112,9 @@ async def report_async(
**kwargs,
):
try:
await external_api_report.asyncio_detailed(
await api_report.asyncio_detailed(
client=configured_client,
json_body=external_api_report.ExternalApiReportJsonBody(
json_body=api_report.ReportJsonBody(
**kwargs,
tags=_get_tags(openpipe_options),
),

View File

@@ -1,55 +1,106 @@
from functools import reduce
from dotenv import load_dotenv
from . import openai, configure_openpipe
import os
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()
openai.api_key = os.getenv("OPENAI_API_KEY")
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():
completion = openai.ChatCompletion.create(
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():
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "system", "content": "count to 10"}],
messages=[{"role": "system", "content": "count to 4"}],
stream=True,
)
for chunk in completion:
print(chunk)
merged = reduce(merge_openai_chunks, completion, None)
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():
acompletion = await openai.ChatCompletion.acreate(
completion = await openai.ChatCompletion.acreate(
model="gpt-3.5-turbo",
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():
acompletion = await openai.ChatCompletion.acreate(
completion = await openai.ChatCompletion.acreate(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": "count down from 5"}],
stream=True,
)
async for chunk in acompletion:
print(chunk)
merged = None
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():
@@ -58,31 +109,54 @@ def test_sync_with_tags():
messages=[{"role": "system", "content": "count to 10"}],
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():
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo-blaster",
messages=[{"role": "system", "content": "count to 10"}],
stream=True,
try:
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo-blaster",
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():
messages = [{"role": "system", "content": f"repeat '{random_string(10)}'"}]
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "system", "content": "count to 10"}],
messages=messages,
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(
model="gpt-3.5-turbo",
messages=[{"role": "system", "content": "count to 10"}],
openpipe={"cache": True},
)
print(completion2)
assert completion2.openpipe.cache_status == "HIT"

View File

@@ -1,133 +0,0 @@
{
"openapi": "3.0.3",
"info": {
"title": "OpenPipe API",
"description": "The public API for reporting API calls to OpenPipe",
"version": "0.1.0"
},
"servers": [{ "url": "https://app.openpipe.ai/api" }],
"paths": {
"/v1/check-cache": {
"post": {
"operationId": "externalApi-checkCache",
"description": "Check if a prompt is cached",
"security": [{ "Authorization": [] }],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"startTime": {
"type": "number",
"description": "Unix timestamp in milliseconds"
},
"reqPayload": { "description": "JSON-encoded request payload" },
"tags": {
"type": "object",
"additionalProperties": { "type": "string" },
"description": "Extra tags to attach to the call for filtering. Eg { \"userId\": \"123\", \"promptId\": \"populate-title\" }"
}
},
"required": ["startTime"],
"additionalProperties": false
}
}
}
},
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"respPayload": { "description": "JSON-encoded response payload" }
},
"additionalProperties": false
}
}
}
},
"default": { "$ref": "#/components/responses/error" }
}
}
},
"/v1/report": {
"post": {
"operationId": "externalApi-report",
"description": "Report an API call",
"security": [{ "Authorization": [] }],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"startTime": {
"type": "number",
"description": "Unix timestamp in milliseconds"
},
"endTime": { "type": "number", "description": "Unix timestamp in milliseconds" },
"reqPayload": { "description": "JSON-encoded request payload" },
"respPayload": { "description": "JSON-encoded response payload" },
"respStatus": { "type": "number", "description": "HTTP status code of response" },
"error": { "type": "string", "description": "User-friendly error message" },
"tags": {
"type": "object",
"additionalProperties": { "type": "string" },
"description": "Extra tags to attach to the call for filtering. Eg { \"userId\": \"123\", \"promptId\": \"populate-title\" }"
}
},
"required": ["startTime", "endTime"],
"additionalProperties": false
}
}
}
},
"parameters": [],
"responses": {
"200": {
"description": "Successful response",
"content": { "application/json": { "schema": {} } }
},
"default": { "$ref": "#/components/responses/error" }
}
}
}
},
"components": {
"securitySchemes": { "Authorization": { "type": "http", "scheme": "bearer" } },
"responses": {
"error": {
"description": "Error response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": { "type": "string" },
"code": { "type": "string" },
"issues": {
"type": "array",
"items": {
"type": "object",
"properties": { "message": { "type": "string" } },
"required": ["message"],
"additionalProperties": false
}
}
},
"required": ["message", "code"],
"additionalProperties": false
}
}
}
}
}
}
}

View File

@@ -1,2 +1,2 @@
node_modules
dist
node_modules/
dist/

View File

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

View File

@@ -1,15 +1,16 @@
{
"name": "openpipe",
"version": "0.1.0",
"type": "module",
"description": "Metrics and auto-evaluation for LLM calls",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build": "tsc"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"keywords": [],
"author": "",
"license": "ISC",
"license": "Apache-2.0",
"dependencies": {
"axios": "^0.26.0",
"openai-beta": "npm:openai@4.0.0-beta.7",

View File

@@ -1,548 +0,0 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
axios:
specifier: ^0.26.0
version: 0.26.0
openai-beta:
specifier: npm:openai@4.0.0-beta.7
version: /openai@4.0.0-beta.7
openai-legacy:
specifier: npm:openai@3.3.0
version: /openai@3.3.0
devDependencies:
'@types/node':
specifier: ^20.4.8
version: 20.4.8
dotenv:
specifier: ^16.3.1
version: 16.3.1
tsx:
specifier: ^3.12.7
version: 3.12.7
typescript:
specifier: ^5.0.4
version: 5.0.4
packages:
/@esbuild-kit/cjs-loader@2.4.2:
resolution: {integrity: sha512-BDXFbYOJzT/NBEtp71cvsrGPwGAMGRB/349rwKuoxNSiKjPraNNnlK6MIIabViCjqZugu6j+xeMDlEkWdHHJSg==}
dependencies:
'@esbuild-kit/core-utils': 3.1.0
get-tsconfig: 4.6.2
dev: true
/@esbuild-kit/core-utils@3.1.0:
resolution: {integrity: sha512-Uuk8RpCg/7fdHSceR1M6XbSZFSuMrxcePFuGgyvsBn+u339dk5OeL4jv2EojwTN2st/unJGsVm4qHWjWNmJ/tw==}
dependencies:
esbuild: 0.17.19
source-map-support: 0.5.21
dev: true
/@esbuild-kit/esm-loader@2.5.5:
resolution: {integrity: sha512-Qwfvj/qoPbClxCRNuac1Du01r9gvNOT+pMYtJDapfB1eoGN1YlJ1BixLyL9WVENRx5RXgNLdfYdx/CuswlGhMw==}
dependencies:
'@esbuild-kit/core-utils': 3.1.0
get-tsconfig: 4.6.2
dev: true
/@esbuild/android-arm64@0.17.19:
resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
requiresBuild: true
dev: true
optional: true
/@esbuild/android-arm@0.17.19:
resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
requiresBuild: true
dev: true
optional: true
/@esbuild/android-x64@0.17.19:
resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
requiresBuild: true
dev: true
optional: true
/@esbuild/darwin-arm64@0.17.19:
resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@esbuild/darwin-x64@0.17.19:
resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@esbuild/freebsd-arm64@0.17.19:
resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/freebsd-x64@0.17.19:
resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-arm64@0.17.19:
resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-arm@0.17.19:
resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-ia32@0.17.19:
resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-loong64@0.17.19:
resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-mips64el@0.17.19:
resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-ppc64@0.17.19:
resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-riscv64@0.17.19:
resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-s390x@0.17.19:
resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/linux-x64@0.17.19:
resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@esbuild/netbsd-x64@0.17.19:
resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/openbsd-x64@0.17.19:
resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
requiresBuild: true
dev: true
optional: true
/@esbuild/sunos-x64@0.17.19:
resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
requiresBuild: true
dev: true
optional: true
/@esbuild/win32-arm64@0.17.19:
resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@esbuild/win32-ia32@0.17.19:
resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@esbuild/win32-x64@0.17.19:
resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@types/node-fetch@2.6.4:
resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==}
dependencies:
'@types/node': 20.4.8
form-data: 3.0.1
dev: false
/@types/node@18.17.3:
resolution: {integrity: sha512-2x8HWtFk0S99zqVQABU9wTpr8wPoaDHZUcAkoTKH+nL7kPv3WUI9cRi/Kk5Mz4xdqXSqTkKP7IWNoQQYCnDsTA==}
dev: false
/@types/node@20.4.8:
resolution: {integrity: sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg==}
/abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
dependencies:
event-target-shim: 5.0.1
dev: false
/agentkeepalive@4.5.0:
resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
engines: {node: '>= 8.0.0'}
dependencies:
humanize-ms: 1.2.1
dev: false
/asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
dev: false
/axios@0.26.0:
resolution: {integrity: sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==}
dependencies:
follow-redirects: 1.15.2
transitivePeerDependencies:
- debug
dev: false
/base-64@0.1.0:
resolution: {integrity: sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==}
dev: false
/buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
dev: true
/charenc@0.0.2:
resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}
dev: false
/combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
dependencies:
delayed-stream: 1.0.0
dev: false
/crypt@0.0.2:
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
dev: false
/delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dev: false
/digest-fetch@1.3.0:
resolution: {integrity: sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==}
dependencies:
base-64: 0.1.0
md5: 2.3.0
dev: false
/dotenv@16.3.1:
resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==}
engines: {node: '>=12'}
dev: true
/esbuild@0.17.19:
resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==}
engines: {node: '>=12'}
hasBin: true
requiresBuild: true
optionalDependencies:
'@esbuild/android-arm': 0.17.19
'@esbuild/android-arm64': 0.17.19
'@esbuild/android-x64': 0.17.19
'@esbuild/darwin-arm64': 0.17.19
'@esbuild/darwin-x64': 0.17.19
'@esbuild/freebsd-arm64': 0.17.19
'@esbuild/freebsd-x64': 0.17.19
'@esbuild/linux-arm': 0.17.19
'@esbuild/linux-arm64': 0.17.19
'@esbuild/linux-ia32': 0.17.19
'@esbuild/linux-loong64': 0.17.19
'@esbuild/linux-mips64el': 0.17.19
'@esbuild/linux-ppc64': 0.17.19
'@esbuild/linux-riscv64': 0.17.19
'@esbuild/linux-s390x': 0.17.19
'@esbuild/linux-x64': 0.17.19
'@esbuild/netbsd-x64': 0.17.19
'@esbuild/openbsd-x64': 0.17.19
'@esbuild/sunos-x64': 0.17.19
'@esbuild/win32-arm64': 0.17.19
'@esbuild/win32-ia32': 0.17.19
'@esbuild/win32-x64': 0.17.19
dev: true
/event-target-shim@5.0.1:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
dev: false
/follow-redirects@1.15.2:
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
dev: false
/form-data-encoder@1.7.2:
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
dev: false
/form-data@3.0.1:
resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
engines: {node: '>= 6'}
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
dev: false
/form-data@4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
dev: false
/formdata-node@4.4.1:
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
engines: {node: '>= 12.20'}
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 4.0.0-beta.3
dev: false
/fsevents@2.3.2:
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
dev: true
optional: true
/get-tsconfig@4.6.2:
resolution: {integrity: sha512-E5XrT4CbbXcXWy+1jChlZmrmCwd5KGx502kDCXJJ7y898TtWW9FwoG5HfOLVRKmlmDGkWN2HM9Ho+/Y8F0sJDg==}
dependencies:
resolve-pkg-maps: 1.0.0
dev: true
/humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
dependencies:
ms: 2.1.3
dev: false
/is-buffer@1.1.6:
resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
dev: false
/md5@2.3.0:
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
dependencies:
charenc: 0.0.2
crypt: 0.0.2
is-buffer: 1.1.6
dev: false
/mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
dev: false
/mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
dev: false
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: false
/node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
dev: false
/node-fetch@2.6.12:
resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
dependencies:
whatwg-url: 5.0.0
dev: false
/openai@3.3.0:
resolution: {integrity: sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==}
dependencies:
axios: 0.26.0
form-data: 4.0.0
transitivePeerDependencies:
- debug
dev: false
/openai@4.0.0-beta.7:
resolution: {integrity: sha512-jHjwvpMuGkNxiQ3erwLZsOvPEhcVrMtwtfNeYmGCjhbdB+oStVw/7pIhIPkualu8rlhLwgMR7awknIaN3IQcOA==}
dependencies:
'@types/node': 18.17.3
'@types/node-fetch': 2.6.4
abort-controller: 3.0.0
agentkeepalive: 4.5.0
digest-fetch: 1.3.0
form-data-encoder: 1.7.2
formdata-node: 4.4.1
node-fetch: 2.6.12
transitivePeerDependencies:
- encoding
dev: false
/resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
dev: true
/source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
dependencies:
buffer-from: 1.1.2
source-map: 0.6.1
dev: true
/source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
dev: true
/tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: false
/tsx@3.12.7:
resolution: {integrity: sha512-C2Ip+jPmqKd1GWVQDvz/Eyc6QJbGfE7NrR3fx5BpEHMZsEHoIxHL1j+lKdGobr8ovEyqeNkPLSKp6SCSOt7gmw==}
hasBin: true
dependencies:
'@esbuild-kit/cjs-loader': 2.4.2
'@esbuild-kit/core-utils': 3.1.0
'@esbuild-kit/esm-loader': 2.5.5
optionalDependencies:
fsevents: 2.3.2
dev: true
/typescript@5.0.4:
resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==}
engines: {node: '>=12.20'}
hasBin: true
dev: true
/web-streams-polyfill@4.0.0-beta.3:
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
engines: {node: '>= 14'}
dev: false
/webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
dev: false
/whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
dev: false

View File

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

View File

@@ -4,7 +4,7 @@
* OpenPipe API
* The public API for reporting API calls to OpenPipe
*
* The version of the OpenAPI document: 0.1.0
* The version of the OpenAPI document: 0.1.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -19,7 +19,7 @@ import type { Configuration } from './configuration';
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
import globalAxios from 'axios';
export const BASE_PATH = "https://app.openpipe.ai/api".replace(/\/+$/, "");
export const BASE_PATH = "https://app.openpipe.ai/api/v1".replace(/\/+$/, "");
/**
*

View File

@@ -4,7 +4,7 @@
* OpenPipe API
* The public API for reporting API calls to OpenPipe
*
* The version of the OpenAPI document: 0.1.0
* The version of the OpenAPI document: 0.1.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* OpenPipe API
* The public API for reporting API calls to OpenPipe
*
* The version of the OpenAPI document: 0.1.0
* The version of the OpenAPI document: 0.1.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* OpenPipe API
* The public API for reporting API calls to OpenPipe
*
* The version of the OpenAPI document: 0.1.0
* The version of the OpenAPI document: 0.1.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

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

Some files were not shown because too many files have changed in this diff Show More