Compare commits

...

12 Commits

Author SHA1 Message Date
Eric Allam af57bc208a Update pnpm lock file 2023-11-10 16:54:06 +00:00
Eric Allam 7cbbb26038 Merge pull request #730 from triggerdotdev/changeset-release/main
chore: Update version for release
2023-11-10 16:49:47 +00:00
github-actions[bot] 2e5f8d8de3 chore: Update version for release 2023-11-10 16:05:39 +00:00
Eric Allam 9a7c08c26a Improvements: Fix dangling SSE issue and compression memory leak (#733)
🚀 Publish Trigger.dev Docker / typecheck (push) Failing after 1s
🚀 Publish Trigger.dev Docker / units (push) Failing after 0s
🚀 Publish Trigger.dev Docker / e2e (push) Failing after 0s
🚀 Publish Trigger.dev Docker / publish (push) Has been skipped
* Downgrade to remix-auth-email-link to remove yarn dependency

* Turn off the pg listen service for now

* Add snapshot admin route

* A couple logger fixes

* Add ability to disable compression

* Add ability to disable SSE

* Fix SSE memory leak + DB load issue
2023-11-10 16:02:53 +00:00
Eric Allam e8e7c116d1 Remove the pg listen code to see if it’s causing DB issues 2023-11-09 21:52:48 +00:00
Matt Aitken 99dd6673f9 Test page display a message if there are no environments that the logged in user can run tests with 2023-11-09 20:12:20 +00:00
Matt Aitken a41d9b3e67 Fix for first endpoint sheet React error 2023-11-09 20:11:36 +00:00
Eric Allam cb1825bfaf Add OpenAI support for 4.16.0 (#726)
* Add OpenAI support for 4.16.0

* Add support for background polling and use that in OpenAI integration to power assistants

* Much improved OpenAI docs

* Added backgroundPoll docs

* Implements waitForEvent and added docs for more built in tasks

* Add sendEvent API referenc

* Write the task libray

* Add changeset and warning for waitForEvent
2023-11-09 16:59:58 +00:00
nicktrn d02173442c Feature: io.sendEvents() (#728)
* Add io.sendEvents

* Add examples to built-ins catalog entry

* Add docs

* Add changeset

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2023-11-09 16:42:23 +00:00
Matt Aitken 9f1f59cc81 Added the redirect to the invites page back in… 2023-11-09 16:30:21 +00:00
Matt Aitken e48c9b5e69 Made it clearer in the HTTP endpoints docs that we’re using Cal.com as an example 2023-11-08 10:15:42 +00:00
Eric Allam 55a9b96c88 Remove the coming soon warning 2023-11-07 16:31:44 +00:00
156 changed files with 5664 additions and 1071 deletions
+8 -1
View File
@@ -11,7 +11,10 @@ const EnvironmentSchema = z.object({
SESSION_SECRET: z.string(),
MAGIC_LINK_SECRET: z.string(),
ENCRYPTION_KEY: z.string(),
WHITELISTED_EMAILS: z.string().refine(isValidRegex, "WHITELISTED_EMAILS must be a valid regex.").optional(),
WHITELISTED_EMAILS: z
.string()
.refine(isValidRegex, "WHITELISTED_EMAILS must be a valid regex.")
.optional(),
REMIX_APP_PORT: z.string().optional(),
LOGIN_ORIGIN: z.string().default("http://localhost:3030"),
APP_ORIGIN: z.string().default("http://localhost:3030"),
@@ -42,7 +45,11 @@ const EnvironmentSchema = z.object({
EXECUTION_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000),
WORKER_ENABLED: z.string().default("true"),
EXECUTION_WORKER_ENABLED: z.string().default("true"),
TASK_OPERATION_WORKER_ENABLED: z.string().default("true"),
TASK_OPERATION_WORKER_CONCURRENCY: z.coerce.number().int().default(10),
TASK_OPERATION_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000),
GRACEFUL_SHUTDOWN_TIMEOUT: z.coerce.number().int().default(60000),
DISABLE_SSE: z.string().optional(),
});
export type Environment = z.infer<typeof EnvironmentSchema>;
+38
View File
@@ -0,0 +1,38 @@
import { useEffect, useState } from "react";
type EventSourceOptions = {
init?: EventSourceInit;
event?: string;
};
/**
* Subscribe to an event source and return the latest event.
* @param url The URL of the event source to connect to
* @param options The options to pass to the EventSource constructor
* @returns The last event received from the server
*/
export function useEventSource(
url: string | URL,
{ event = "message", init }: EventSourceOptions = {}
) {
const [data, setData] = useState<string | null>(null);
useEffect(() => {
const eventSource = new EventSource(url, init);
eventSource.addEventListener(event ?? "message", handler);
// rest data if dependencies change
setData(null);
function handler(event: MessageEvent) {
setData(event.data || "UNKNOWN_EVENT_DATA");
}
return () => {
eventSource.removeEventListener(event ?? "message", handler);
eventSource.close();
};
}, [url, event, init]);
return data;
}
@@ -1,9 +1,9 @@
import { useEffect } from "react";
import { useEventSource } from "remix-utils/sse/react";
import { projectPath, projectStreamingPath } from "~/utils/pathBuilder";
import { useProject } from "./useProject";
import { useOrganization } from "./useOrganizations";
import { useNavigate } from "@remix-run/react";
import { useEventSource } from "./useEventSource";
export function useProjectSetupComplete() {
const project = useProject();
@@ -0,0 +1,22 @@
import { z } from "zod";
export const JobVersionDispatchableSchema = z.object({
type: z.literal("JOB_VERSION"),
id: z.string(),
});
export const DynamicTriggerDispatchableSchema = z.object({
type: z.literal("DYNAMIC_TRIGGER"),
id: z.string(),
});
export const EphemeralDispatchableSchema = z.object({
type: z.literal("EPHEMERAL"),
url: z.string(),
});
export const DispatchableSchema = z.discriminatedUnion("type", [
JobVersionDispatchableSchema,
DynamicTriggerDispatchableSchema,
EphemeralDispatchableSchema,
]);
+1
View File
@@ -16,6 +16,7 @@ export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask
description: task.description,
params: task.params as any,
output: task.output as any,
context: task.context as any,
properties: task.properties as any,
style: task.style as any,
error: task.error,
@@ -14,7 +14,6 @@ import { run as graphileRun, parseCronItems } from "graphile-worker";
import omit from "lodash.omit";
import { z } from "zod";
import { PrismaClient, PrismaClientOrTransaction } from "~/db.server";
import { PgListenService } from "~/services/db/pgListen.server";
import { workerLogger as logger } from "~/services/logger.server";
export interface MessageCatalogSchema {
@@ -167,21 +166,6 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
this.#runner?.events.on("pool:listen:success", async ({ workerPool, client }) => {
this.#logDebug("pool:listen:success");
// hijack client instance to listen and react to incoming NOTIFY events
const pgListen = new PgListenService(client, this.#name, logger);
await pgListen.on("trigger:graphile:migrate", async ({ latestMigration }) => {
this.#logDebug("Detected incoming migration", { latestMigration });
if (latestMigration > 10) {
// already migrated past v0.14 - nothing to do
return;
}
// simulate SIGTERM to trigger graceful shutdown
this._handleSignal("SIGTERM");
});
});
this.#runner?.events.on("pool:listen:error", ({ error }) => {
@@ -1,7 +1,7 @@
import { PrismaClient, prisma } from "~/db.server";
import { Project } from "~/models/project.server";
import { User } from "~/models/user.server";
import { sse } from "~/utils/sse";
import { sse } from "~/utils/sse.server";
type EnvironmentSignalsMap = {
[x: string]: {
@@ -1,6 +1,6 @@
import { JobRun } from "@trigger.dev/database";
import { PrismaClient, prisma } from "~/db.server";
import { sse } from "~/utils/sse";
import { sse } from "~/utils/sse.server";
export class RunStreamPresenter {
#prismaClient: PrismaClient;
+13 -6
View File
@@ -1,19 +1,26 @@
import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
import { getUsersInvites } from "~/models/member.server";
import { SelectBestProjectPresenter } from "~/presenters/SelectBestProjectPresenter.server";
import { requireUserId } from "~/services/session.server";
import { newOrganizationPath, projectPath } from "~/utils/pathBuilder";
import { requireUser } from "~/services/session.server";
import { invitesPath, newOrganizationPath, projectPath } from "~/utils/pathBuilder";
//this loader chooses the best project to redirect you to, ideally based on the cookie
export const loader = async ({ request }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
const user = await requireUser(request);
//if there are invites then we should redirect to the invites page
const invites = await getUsersInvites({ email: user.email });
if (invites.length > 0) {
return redirect(invitesPath());
}
const presenter = new SelectBestProjectPresenter();
try {
const { project, organization } = await presenter.call({ userId, request });
const { project, organization } = await presenter.call({ userId: user.id, request });
//redirect them to the most appropriate project
return redirect(projectPath(organization, project));
} catch (e) {
//this should only happen if the user has no projects
//this should only happen if the user has no projects, and no invites
return redirect(newOrganizationPath());
}
};
@@ -2,7 +2,7 @@ import { conform, useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { useFetcher, useRevalidator } from "@remix-run/react";
import { useEffect } from "react";
import { useEventSource } from "remix-utils/sse/react";
import { useEventSource } from "~/hooks/useEventSource";
import { InlineCode } from "~/components/code/InlineCode";
import {
EndpointIndexStatusIcon,
@@ -51,7 +51,7 @@ export function FirstEndpointSheet({ projectId, environments }: FirstEndpointShe
return (
<Sheet>
<SheetTrigger>
<Button variant="secondary/medium">Add your first endpoint</Button>
<ButtonContent variant="secondary/medium">Add your first endpoint</ButtonContent>
</SheetTrigger>
<SheetContent size="lg">
<SheetHeader>
@@ -2,7 +2,7 @@ import { useRevalidator } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { useEffect, useMemo, useState } from "react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { useEventSource } from "remix-utils/sse/react";
import { useEventSource } from "~/hooks/useEventSource";
import {
EndpointIndexStatusIcon,
EndpointIndexStatusLabel,
@@ -2,7 +2,7 @@ import { useRevalidator } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { Fragment, useEffect } from "react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { useEventSource } from "remix-utils/sse/react";
import { useEventSource } from "~/hooks/useEventSource";
import { BreadcrumbLink } from "~/components/navigation/Breadcrumb";
import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon";
import { RunOverview } from "~/components/run/RunOverview";
@@ -1,6 +1,5 @@
import { useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { ClipboardIcon } from "@heroicons/react/20/solid";
import { ClockIcon, CodeBracketIcon } from "@heroicons/react/24/outline";
import { Form, useActionData, useSubmit } from "@remix-run/react";
import { ActionFunction, LoaderFunctionArgs, json } from "@remix-run/server-runtime";
@@ -33,10 +32,14 @@ import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/mode
import { TestJobPresenter } from "~/presenters/TestJobPresenter.server";
import { TestJobService } from "~/services/jobs/testJob.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { Handle } from "~/utils/handle";
import { isValidIcon } from "~/utils/icon";
import { JobParamsSchema, jobRunDashboardPath, trimTrailingSlash } from "~/utils/pathBuilder";
import {
JobParamsSchema,
docsPath,
jobRunDashboardPath,
trimTrailingSlash,
} from "~/utils/pathBuilder";
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
@@ -139,7 +142,7 @@ export default function Page() {
setDefaultJson(code);
}, []);
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string>(environments[0].id);
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState(environments.at(0)?.id);
const selectedEnvironment = environments.find((e) => e.id === selectedEnvironmentId);
const currentJson = useRef<string>(defaultJson);
@@ -147,6 +150,10 @@ export default function Page() {
const submitForm = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
if (!selectedEnvironmentId) {
return;
}
submit(
{
payload: currentJson.current,
@@ -175,10 +182,33 @@ export default function Page() {
if (environments.length === 0) {
return (
<Callout variant="warning">
Can't run a test when there are no environments. This shouldn't happen, please contact
support.
</Callout>
<div className="flex flex-col gap-4">
<Callout variant="info">
There are no environments that you can test this job with you can't run Tests against
your teammates' Dev environments. You should run the code locally (using the CLI) so that
this Job will be associated with your Dev environment. This also means that this Job
hasn't been deployed to Staging or Prod yet.
</Callout>
<div>
<Header2 spacing>Useful guides</Header2>
<div className="flex gap-2">
<LinkButton
to={docsPath("documentation/guides/cli#dev-command")}
variant="secondary/small"
LeadingIcon="docs"
>
Using the CLI
</LinkButton>
<LinkButton
to={docsPath("documentation/guides/deployment")}
variant="secondary/small"
LeadingIcon="docs"
>
Deploying your Jobs
</LinkButton>
</div>
</div>
</div>
);
}
@@ -2,7 +2,7 @@ import { useRevalidator } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { Fragment, useEffect } from "react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { useEventSource } from "remix-utils/sse/react";
import { useEventSource } from "~/hooks/useEventSource";
import { BreadcrumbLink } from "~/components/navigation/Breadcrumb";
import { BreadcrumbIcon } from "~/components/primitives/BreadcrumbIcon";
import { RunOverview } from "~/components/run/RunOverview";
@@ -0,0 +1,59 @@
import path from "path";
import os from "os";
import fs from "fs";
import v8 from "v8";
import { PassThrough } from "stream";
import { json, type DataFunctionArgs } from "@remix-run/node";
import { prisma } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { requireUser } from "~/services/session.server";
// Format date as yyyy-MM-dd HH_mm_ss_SSS
function formatDate(date: Date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
const milliseconds = date.getMilliseconds();
return `${year}-${month.toString().padStart(2, "0")}-${day.toString().padStart(2, "0")} ${hours
.toString()
.padStart(2, "0")}_${minutes.toString().padStart(2, "0")}_${seconds
.toString()
.padStart(2, "0")}_${milliseconds.toString().padStart(3, "0")}`;
}
export async function loader({ request }: DataFunctionArgs) {
const user = await requireUser(request);
if (!user.admin) {
throw new Response("You must be an admin to perform this action", { status: 403 });
}
const host = request.headers.get("X-Forwarded-Host") ?? request.headers.get("host");
const tempDir = os.tmpdir();
const filepath = path.join(tempDir, `${host}-${formatDate(new Date())}.heapsnapshot`);
const snapshotPath = v8.writeHeapSnapshot(filepath);
if (!snapshotPath) {
throw new Response("No snapshot saved", { status: 500 });
}
const body = new PassThrough();
const stream = fs.createReadStream(snapshotPath);
stream.on("open", () => stream.pipe(body));
stream.on("error", (err) => body.end(err));
stream.on("end", () => body.end());
return new Response(body as any, {
status: 200,
headers: {
"Content-Type": "application/octet-stream",
"Content-Disposition": `attachment; filename="${path.basename(snapshotPath)}"`,
"Content-Length": (await fs.promises.stat(snapshotPath)).size.toString(),
},
});
}
@@ -0,0 +1,59 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import {
EphemeralEventDispatcherRequestBodySchema,
InvokeJobRequestBodySchema,
} from "@trigger.dev/core";
import { z } from "zod";
import { PrismaErrorSchema } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { CreateEphemeralEventDispatcherService } from "~/services/dispatchers/createEphemeralEventDispatcher.server";
import { InvokeJobService } from "~/services/jobs/invokeJob.server";
import { logger } from "~/services/logger.server";
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
}
// Authenticate the request
const authenticationResult = await authenticateApiRequest(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API Key" }, { status: 401 });
}
// Now parse the request body
const anyBody = await request.json();
logger.debug("CreateEphemeralEventDispatcherService.call() request body", {
body: anyBody,
});
const body = EphemeralEventDispatcherRequestBodySchema.safeParse(anyBody);
if (!body.success) {
return json({ error: "Invalid request body" }, { status: 400 });
}
const service = new CreateEphemeralEventDispatcherService();
try {
const dispatcher = await service.call(authenticationResult.environment, body.data);
if (!dispatcher) {
return json({ error: "Could not create Event Dispatcher" }, { status: 500 });
}
return json({ id: dispatcher.id });
} catch (error) {
const prismaError = PrismaErrorSchema.safeParse(error);
// Record not found in the database
if (prismaError.success && prismaError.data.code === "P2005") {
return json({ error: "Dispatcher not found" }, { status: 404 });
} else {
return json({ error: "Internal Server Error" }, { status: 500 });
}
}
}
@@ -0,0 +1,49 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { SendBulkEventsBodySchema } from "@trigger.dev/core";
import { generateErrorMessage } from "zod-error";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { IngestSendEvent } from "~/services/events/ingestSendEvent.server";
import { eventRecordToApiJson } from "~/api.server";
import { EventRecord } from "@trigger.dev/database";
export async function action({ request }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
}
// Next authenticate the request
const authenticationResult = await authenticateApiRequest(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}
const authenticatedEnv = authenticationResult.environment;
// Now parse the request body
const anyBody = await request.json();
const body = SendBulkEventsBodySchema.safeParse(anyBody);
if (!body.success) {
return json({ message: generateErrorMessage(body.error.issues) }, { status: 422 });
}
const service = new IngestSendEvent();
const events: EventRecord[] = [];
for (const event of body.data.events) {
const eventRecord = await service.call(authenticatedEnv, event, body.data.options);
if (!eventRecord) {
return json({ error: "Failed to create event during bulk ingest" }, { status: 500 });
}
events.push(eventRecord);
}
return json(events.map(eventRecordToApiJson));
}
@@ -1,6 +1,5 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { TaskStatus } from "@trigger.dev/database";
import {
API_VERSIONS,
RunTaskBodyOutput,
@@ -8,15 +7,16 @@ import {
RunTaskResponseWithCachedTasksBody,
ServerTask,
} from "@trigger.dev/core";
import { TaskStatus } from "@trigger.dev/database";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { env } from "~/env.server";
import { prepareTasksForCaching, taskWithAttemptsToServerTask } from "~/models/task.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { ulid } from "~/services/ulid.server";
import { workerQueue } from "~/services/worker.server";
import { generateSecret } from "~/services/sources/utils.server";
import { env } from "~/env.server";
import { ulid } from "~/services/ulid.server";
import { taskOperationWorker, workerQueue } from "~/services/worker.server";
const ParamsSchema = z.object({
runId: z.string(),
@@ -302,7 +302,7 @@ export class RunTaskService {
if (task.status === "RUNNING" && typeof taskBody.operation === "string") {
// We need to schedule the operation
await workerQueue.enqueue(
await taskOperationWorker.enqueue(
"performTaskOperation",
{
id: task.id,
@@ -2,7 +2,7 @@ import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
import { requireUserId } from "~/services/session.server";
import { sse } from "~/utils/sse";
import { sse } from "~/utils/sse.server";
export async function loader({ request, params }: LoaderFunctionArgs) {
await requireUserId(request);
@@ -2,7 +2,7 @@ import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
import { requireUserId } from "~/services/session.server";
import { sse } from "~/utils/sse";
import { sse } from "~/utils/sse.server";
export async function loader({ request, params }: LoaderFunctionArgs) {
await requireUserId(request);
+1 -1
View File
@@ -1,7 +1,7 @@
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { z } from "zod";
import { logger } from "~/services/logger.server";
import { sse } from "~/utils/sse";
import { sse } from "~/utils/sse.server";
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
+1 -1
View File
@@ -1,6 +1,6 @@
import { useLoaderData } from "@remix-run/react";
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { useEventSource } from "remix-utils/sse/react";
import { useEventSource } from "~/hooks/useEventSource";
import { z } from "zod";
export async function loader({ request }: LoaderFunctionArgs) {
@@ -0,0 +1,68 @@
import { EphemeralEventDispatcherRequestBody } from "@trigger.dev/core";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { AuthenticatedEnvironment } from "../apiAuth.server";
import { ExpireDispatcherService } from "./expireDispatcher.server";
export class CreateEphemeralEventDispatcherService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
environment: AuthenticatedEnvironment,
data: EphemeralEventDispatcherRequestBody
) {
return await $transaction(this.#prismaClient, async (tx) => {
const existingDispatcher = await tx.eventDispatcher.findUnique({
where: {
dispatchableId_environmentId: {
dispatchableId: data.url,
environmentId: environment.id,
},
},
});
if (existingDispatcher) {
return existingDispatcher;
}
const externalAccount = data.accountId
? await this.#prismaClient.externalAccount.upsert({
where: {
environmentId_identifier: {
environmentId: environment.id,
identifier: data.accountId,
},
},
create: {
environmentId: environment.id,
organizationId: environment.organizationId,
identifier: data.accountId,
},
update: {},
})
: undefined;
const dispatcher = await tx.eventDispatcher.create({
data: {
dispatchableId: data.url,
environmentId: environment.id,
source: data.source ?? "trigger.dev",
payloadFilter: data.filter,
contextFilter: data.contextFilter,
dispatchable: { url: data.url, type: "EPHEMERAL" },
enabled: true,
event: typeof data.name === "string" ? [data.name] : data.name,
manual: false,
externalAccountId: externalAccount?.id,
},
});
await ExpireDispatcherService.enqueue(dispatcher.id, data.timeoutInSeconds, tx);
return dispatcher;
});
}
}
@@ -0,0 +1,36 @@
import { PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
import { workerQueue } from "../worker.server";
export class ExpireDispatcherService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(id: string) {
await this.#prismaClient.eventDispatcher.delete({
where: {
id,
},
});
}
static async dequeue(id: string, tx?: PrismaClientOrTransaction) {
await workerQueue.dequeue(`expire:${id}`, { tx });
}
static async enqueue(id: string, timeoutInSeconds: number, tx?: PrismaClientOrTransaction) {
await workerQueue.enqueue(
"expireDispatcher",
{
id,
},
{
tx,
runAt: new Date(Date.now() + 1000 * timeoutInSeconds),
jobKey: `expire:${id}`,
}
);
}
}
@@ -0,0 +1,108 @@
import { PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
import { taskOperationWorker } from "../worker.server";
import { EphemeralDispatchableSchema } from "~/models/eventDispatcher.server";
import { fetch } from "@whatwg-node/fetch";
import { ExpireDispatcherService } from "./expireDispatcher.server";
export class InvokeEphemeralDispatcherService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(id: string, eventRecordId: string) {
const eventDispatcher = await this.#prismaClient.eventDispatcher.findUnique({
where: {
id,
},
});
if (!eventDispatcher) {
return;
}
if (!eventDispatcher.enabled) {
return;
}
const eventRecord = await this.#prismaClient.eventRecord.findUnique({
where: {
id: eventRecordId,
},
include: {
externalAccount: true,
},
});
if (!eventRecord) {
return;
}
if (eventRecord.cancelledAt) {
return;
}
const dispatchable = EphemeralDispatchableSchema.safeParse(eventDispatcher.dispatchable);
if (!dispatchable.success) {
return;
}
const url = dispatchable.data.url;
const body = {
id: eventRecord.eventId,
source: eventRecord.source,
name: eventRecord.name,
payload: eventRecord.payload,
context: eventRecord.context,
timestamp: eventRecord.timestamp,
accountId: eventRecord.externalAccount ? eventRecord.externalAccount.identifier : undefined,
};
const abortController = new AbortController();
const timeoutId = setTimeout(() => {
abortController.abort();
}, 5000);
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify(body),
signal: abortController.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(
`Failed to invoke ephemeral dispatcher: ${response.statusText} [${response.status}]`
);
}
// Run the expire dispatcher service
await ExpireDispatcherService.enqueue(id, 0);
}
static async dequeue(id: string, tx?: PrismaClientOrTransaction) {
await taskOperationWorker.dequeue(`invoke:ephemeral:${id}`, { tx });
}
static async enqueue(id: string, eventRecordId: string, tx?: PrismaClientOrTransaction) {
await taskOperationWorker.enqueue(
"invokeEphemeralDispatcher",
{
id,
eventRecordId,
},
{
tx,
jobKey: `invoke:ephemeral:${id}`,
}
);
}
}
@@ -96,6 +96,13 @@ export class DeliverEventService {
return true;
}
if (
dispatcher.externalAccountId &&
dispatcher.externalAccountId !== eventRecord.externalAccountId
) {
return false;
}
const payloadFilter = EventFilterSchema.safeParse(dispatcher.payloadFilter ?? {});
const contextFilter = EventFilterSchema.safeParse(dispatcher.contextFilter ?? {});
@@ -3,21 +3,8 @@ import type { PrismaClientOrTransaction } from "~/db.server";
import { prisma } from "~/db.server";
import { logger } from "~/services/logger.server";
import { CreateRunService } from "~/services/runs/createRun.server";
const JobVersionDispatchableSchema = z.object({
type: z.literal("JOB_VERSION"),
id: z.string(),
});
const DynamicTriggerDispatchableSchema = z.object({
type: z.literal("DYNAMIC_TRIGGER"),
id: z.string(),
});
const DispatchableSchema = z.discriminatedUnion("type", [
JobVersionDispatchableSchema,
DynamicTriggerDispatchableSchema,
]);
import { InvokeEphemeralDispatcherService } from "../dispatchers/invokeEphemeralEventDispatcher.server";
import { DispatchableSchema } from "~/models/eventDispatcher.server";
export class InvokeDispatcherService {
#prismaClient: PrismaClientOrTransaction;
@@ -142,6 +129,11 @@ export class InvokeDispatcherService {
});
}
break;
}
case "EPHEMERAL": {
await InvokeEphemeralDispatcherService.enqueue(eventDispatcher.id, eventRecord.id);
break;
}
}
+10 -18
View File
@@ -3,24 +3,16 @@ import { Logger } from "@trigger.dev/core";
import { sensitiveDataReplacer } from "./sensitiveDataReplacer";
import { singleton } from "~/utils/singleton";
export const logger = singleton(
"logger",
() =>
new Logger(
"webapp",
(process.env.APP_LOG_LEVEL ?? "debug") as LogLevel,
["examples", "output", "connectionString", "payload"],
sensitiveDataReplacer
)
export const logger = new Logger(
"webapp",
(process.env.APP_LOG_LEVEL ?? "debug") as LogLevel,
["examples", "output", "connectionString", "payload"],
sensitiveDataReplacer
);
export const workerLogger = singleton(
"worker-logger",
() =>
new Logger(
"worker",
(process.env.APP_LOG_LEVEL ?? "debug") as LogLevel,
["examples", "output", "connectionString"],
sensitiveDataReplacer
)
export const workerLogger = new Logger(
"worker",
(process.env.APP_LOG_LEVEL ?? "debug") as LogLevel,
["examples", "output", "connectionString"],
sensitiveDataReplacer
);
@@ -1,20 +1,26 @@
import {
FetchOperationSchema,
FetchPollOperationSchema,
FetchRequestInit,
FetchRetryOptions,
FetchRetryStrategy,
RedactString,
RetryOptions,
calculateResetAt,
calculateRetryAt,
eventFilterMatches,
responseFilterMatches,
} from "@trigger.dev/core";
import { type Task } from "@trigger.dev/database";
import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
import { formatUnknownError } from "~/utils/formatErrors.server";
import { safeJsonFromResponse } from "~/utils/json";
import { logger } from "../logger.server";
import { workerQueue } from "../worker.server";
import { taskOperationWorker, workerQueue } from "../worker.server";
import { ResumeTaskService } from "./resumeTask.server";
import { fetch } from "@whatwg-node/fetch";
import { fromZodError } from "zod-validation-error";
import { ulid } from "../ulid.server";
type FoundTask = Awaited<ReturnType<typeof findTask>>;
@@ -32,16 +38,166 @@ export class PerformTaskOperationService {
return;
}
if (task.status === "CANCELED") {
return;
}
if (task.status === "COMPLETED" || task.status === "ERRORED") {
return await this.#resumeRunExecution(task, this.#prismaClient);
}
if (!task.operation) {
return await this.#resumeTask(task, null, 0);
return await this.#resumeTask(task, null, null, 200, "fetch", 0);
}
switch (task.operation) {
case "fetch": {
case "fetch-poll": {
const pollOperation = FetchPollOperationSchema.safeParse(task.params);
if (!pollOperation.success) {
return await this.#resumeTaskWithError(
task,
fromZodError(pollOperation.error, {
prefix: "Invalid fetch poll params",
}).message
);
}
const { url, requestInit, timeout, interval, responseFilter, requestTimeout } =
pollOperation.data;
// check if we need to fail the task because it's timed out
const startedAt = task.startedAt;
if (!startedAt) {
return await this.#resumeTaskWithError(task, {
message: "Task has not been started",
});
}
if (Date.now() - startedAt.getTime() > timeout * 1000) {
return await this.#resumeTaskWithError(task, {
message: `Task timed out after ${timeout} seconds`,
});
}
const startTimeInMs = performance.now();
const abortController = new AbortController();
// calculate the actual timeout. If timeoutInMs is undefined, we use the default of 5s
// Also make sure the timeout is at least 1s, but not bigger than 5s
const actualTimeoutInMs = Math.min(
Math.max(requestTimeout?.durationInMs ?? 5000, 1000),
5000
);
const timeoutId = setTimeout(() => {
abortController.abort();
}, actualTimeoutInMs);
try {
logger.debug("PerformTaskOperationService.call poll request", {
task,
actualTimeoutInMs,
url,
responseFilter,
});
const startedAt = new Date();
const method = requestInit?.method ?? "GET";
const response = await fetch(url, {
method,
headers: normalizeHeaders(requestInit?.headers ?? {}),
body: requestInit?.body,
signal: abortController.signal,
});
clearTimeout(timeoutId);
const durationInMs = Math.floor(performance.now() - startTimeInMs);
const headers = Object.fromEntries(response.headers.entries());
logger.debug("PerformTaskOperationService.call poll response", {
url,
requestInit,
statusCode: response.status,
headers: Object.fromEntries(response.headers.entries()),
durationInMs,
});
const matchResult = await responseFilterMatches(response, responseFilter);
await this.#prismaClient.task.create({
data: {
id: ulid(),
idempotencyKey: ulid(),
runId: task.runId,
parentId: task.id,
name: "poll attempt",
icon: "activity",
status: "COMPLETED",
noop: true,
style: { style: "minimal", variant: "info" },
description: `${method} ${url} ${response.status}`,
params: {
status: response.status,
headers,
body: matchResult.body as any,
},
startedAt,
completedAt: new Date(),
},
});
if (matchResult.match) {
logger.debug("PerformTaskOperationService.call poll response matched", {
url,
matchResult,
});
return await this.#resumeTask(
task,
matchResult.body,
Object.fromEntries(response.headers.entries()),
response.status,
"fetch",
durationInMs
);
} else {
const retryAt = new Date(Date.now() + interval * 1000);
return await this.#retryTask(task, retryAt);
}
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
const durationInMs = Math.floor(performance.now() - startTimeInMs);
logger.debug("PerformTaskOperationService.call poll timed out", {
url,
durationInMs,
error,
});
const retryAt = this.#calculateRetryForTimeout(task, requestTimeout?.retry);
if (retryAt) {
return await this.#retryTask(task, retryAt);
}
return await this.#resumeTaskWithError(task, {
message: `Fetch timed out after ${actualTimeoutInMs.toFixed(0)}ms`,
});
}
throw error;
}
}
case "fetch":
case "fetch-response": {
const fetchOperation = FetchOperationSchema.safeParse(task.params);
if (!fetchOperation.success) {
@@ -97,7 +253,7 @@ export class PerformTaskOperationService {
});
if (!response.ok) {
const retryAt = this.#calculateRetryForResponse(task, retry, response);
const retryAt = this.#calculateRetryForResponse(task, retry, response, jsonBody);
if (retryAt) {
return await this.#retryTaskWithError(
@@ -117,7 +273,14 @@ export class PerformTaskOperationService {
}
}
return await this.#resumeTask(task, jsonBody, durationInMs);
return await this.#resumeTask(
task,
jsonBody,
Object.fromEntries(response.headers.entries()),
response.status,
task.operation,
durationInMs
);
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
const durationInMs = Math.floor(performance.now() - startTimeInMs);
@@ -157,13 +320,14 @@ export class PerformTaskOperationService {
#calculateRetryForResponse(
task: NonNullable<FoundTask>,
retry: FetchRetryOptions | undefined,
response: Response
response: Response,
body: any
): Date | undefined {
if (!retry) {
return;
}
const strategy = this.#getRetryStrategyForStatusCode(response.status, retry);
const strategy = this.#getRetryStrategyForResponse(response, body, retry);
if (!strategy) {
return;
@@ -180,11 +344,10 @@ export class PerformTaskOperationService {
return calculateRetryAt(strategy, task.attempts.length - 1);
}
case "headers": {
const remaining = response.headers.get(strategy.remainingHeader);
const resetAt = response.headers.get(strategy.resetHeader);
if (typeof remaining === "string" && typeof resetAt === "string" && remaining === "0") {
return new Date(Number(resetAt) * 1000 + addJitterInMs());
if (typeof resetAt === "string") {
return calculateResetAt(resetAt, strategy.resetFormat);
}
}
}
@@ -201,8 +364,9 @@ export class PerformTaskOperationService {
return calculateRetryAt(retry, task.attempts.length - 1);
}
#getRetryStrategyForStatusCode(
statusCode: number,
#getRetryStrategyForResponse(
response: Response,
body: any,
retry: FetchRetryOptions
): FetchRetryStrategy | undefined {
const statusCodes = Object.keys(retry);
@@ -211,7 +375,19 @@ export class PerformTaskOperationService {
const statusRange = statusCodes[i];
const strategy = retry[statusRange];
if (isStatusCodeInRange(statusCode, statusRange)) {
if (isStatusCodeInRange(response.status, statusRange)) {
if (strategy.bodyFilter) {
if (!body) {
continue;
}
if (eventFilterMatches(body, strategy.bodyFilter)) {
return strategy;
} else {
continue;
}
}
return strategy;
}
}
@@ -248,16 +424,26 @@ export class PerformTaskOperationService {
},
});
await workerQueue.enqueue(
await taskOperationWorker.enqueue(
"performTaskOperation",
{
id: task.id,
},
{ tx, runAt: retryAt }
{ tx, runAt: retryAt, jobKey: `operation:${task.id}` }
);
});
}
async #retryTask(task: Task, retryAt: Date) {
await taskOperationWorker.enqueue(
"performTaskOperation",
{
id: task.id,
},
{ runAt: retryAt, jobKey: `operation:${task.id}` }
);
}
async #resumeTaskWithError(task: NonNullable<FoundTask>, output: any) {
await $transaction(this.#prismaClient, async (tx) => {
await tx.task.update({
@@ -284,7 +470,14 @@ export class PerformTaskOperationService {
});
}
async #resumeTask(task: NonNullable<FoundTask>, output: any, durationInMs: number) {
async #resumeTask(
task: NonNullable<FoundTask>,
output: any,
context: any,
status: number,
operation: "fetch" | "fetch-response",
durationInMs: number
) {
await $transaction(this.#prismaClient, async (tx) => {
await tx.taskAttempt.updateMany({
where: {
@@ -296,12 +489,22 @@ export class PerformTaskOperationService {
},
});
const taskOutput =
operation === "fetch"
? output
: {
data: output,
headers: context,
status,
};
await tx.task.update({
where: { id: task.id },
data: {
status: "COMPLETED",
completedAt: new Date(),
output: output ? output : undefined,
output: taskOutput,
context: context ? context : undefined,
run: {
update: {
executionDuration: {
+75 -13
View File
@@ -24,6 +24,8 @@ import { ProbeEndpointService } from "./endpoints/probeEndpoint.server";
import { DeliverRunSubscriptionService } from "./runs/deliverRunSubscription.server";
import { DeliverRunSubscriptionsService } from "./runs/deliverRunSubscriptions.server";
import { ResumeTaskService } from "./tasks/resumeTask.server";
import { ExpireDispatcherService } from "./dispatchers/expireDispatcher.server";
import { InvokeEphemeralDispatcherService } from "./dispatchers/invokeEphemeralEventDispatcher.server";
const workerCatalog = {
indexEndpoint: z.object({
@@ -40,9 +42,6 @@ const workerCatalog = {
processCallbackTimeout: z.object({
id: z.string(),
}),
performTaskOperation: z.object({
id: z.string(),
}),
deliverHttpSourceRequest: z.object({ id: z.string() }),
refreshOAuthToken: z.object({
organizationId: z.string(),
@@ -93,6 +92,9 @@ const workerCatalog = {
resumeTask: z.object({
id: z.string(),
}),
expireDispatcher: z.object({
id: z.string(),
}),
};
const executionWorkerCatalog = {
@@ -108,12 +110,24 @@ const executionWorkerCatalog = {
}),
};
const taskOperationWorkerCatalog = {
performTaskOperation: z.object({
id: z.string(),
}),
invokeEphemeralDispatcher: z.object({
id: z.string(),
eventRecordId: z.string(),
}),
};
let workerQueue: ZodWorker<typeof workerCatalog>;
let executionWorker: ZodWorker<typeof executionWorkerCatalog>;
let taskOperationWorker: ZodWorker<typeof taskOperationWorkerCatalog>;
declare global {
var __worker__: ZodWorker<typeof workerCatalog>;
var __executionWorker__: ZodWorker<typeof executionWorkerCatalog>;
var __taskOperationWorker__: ZodWorker<typeof taskOperationWorkerCatalog>;
}
// this is needed because in development we don't want to restart
@@ -123,6 +137,7 @@ declare global {
if (env.NODE_ENV === "production") {
workerQueue = getWorkerQueue();
executionWorker = getExecutionWorkerQueue();
taskOperationWorker = getTaskOperationWorkerQueue();
} else {
if (!global.__worker__) {
global.__worker__ = getWorkerQueue();
@@ -134,6 +149,12 @@ if (env.NODE_ENV === "production") {
}
executionWorker = global.__executionWorker__;
if (!global.__taskOperationWorker__) {
global.__taskOperationWorker__ = getTaskOperationWorkerQueue();
}
taskOperationWorker = global.__taskOperationWorker__;
}
export async function init() {
@@ -148,6 +169,10 @@ export async function init() {
if (env.EXECUTION_WORKER_ENABLED === "true") {
await executionWorker.initialize();
}
if (env.TASK_OPERATION_WORKER_ENABLED === "true") {
await taskOperationWorker.initialize();
}
}
function getWorkerQueue() {
@@ -286,15 +311,6 @@ function getWorkerQueue() {
await service.call(payload.id);
},
},
performTaskOperation: {
priority: 0, // smaller number = higher priority
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformTaskOperationService();
await service.call(payload.id);
},
},
scheduleEmail: {
priority: 100,
maxAttempts: 3,
@@ -375,6 +391,15 @@ function getWorkerQueue() {
handler: async (payload, job) => {
const service = new ResumeTaskService();
return await service.call(payload.id);
},
},
expireDispatcher: {
priority: 10,
maxAttempts: 3,
handler: async (payload) => {
const service = new ExpireDispatcherService();
return await service.call(payload.id);
},
},
@@ -428,4 +453,41 @@ function getExecutionWorkerQueue() {
});
}
export { executionWorker, workerQueue };
function getTaskOperationWorkerQueue() {
return new ZodWorker({
name: "taskOperationWorker",
prisma,
runnerOptions: {
connectionString: env.DATABASE_URL,
concurrency: env.TASK_OPERATION_WORKER_CONCURRENCY,
pollInterval: env.TASK_OPERATION_WORKER_POLL_INTERVAL,
noPreparedStatements: env.DATABASE_URL !== env.DIRECT_URL,
schema: env.WORKER_SCHEMA,
maxPoolSize: env.TASK_OPERATION_WORKER_CONCURRENCY,
},
shutdownTimeoutInMs: env.GRACEFUL_SHUTDOWN_TIMEOUT,
schema: taskOperationWorkerCatalog,
tasks: {
performTaskOperation: {
priority: 0, // smaller number = higher priority
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformTaskOperationService();
await service.call(payload.id);
},
},
invokeEphemeralDispatcher: {
priority: 0, // smaller number = higher priority
maxAttempts: 10,
handler: async (payload, job) => {
const service = new InvokeEphemeralDispatcherService();
await service.call(payload.id, payload.eventRecordId);
},
},
},
});
}
export { executionWorker, workerQueue, taskOperationWorker };
@@ -1,4 +1,5 @@
import { eventStream } from "remix-utils/sse/server";
import { env } from "~/env.server";
import { logger } from "~/services/logger.server";
type SseProps = {
@@ -17,20 +18,11 @@ type Event = {
};
export function sse({ request, pingInterval = 1000, updateInterval = 348, run }: SseProps) {
let pinger: NodeJS.Timer | undefined = undefined;
let updater: NodeJS.Timer | undefined = undefined;
if (env.DISABLE_SSE === "1" || env.DISABLE_SSE === "true") {
return new Response("SSE disabled", { status: 200 });
}
const abort = () => {
if (pinger) {
clearInterval(pinger);
}
if (updater) {
clearInterval(updater);
}
};
return eventStream(request.signal, (send) => {
return eventStream(request.signal, (send, close) => {
const safeSend = (args: { event?: string; data: string }) => {
try {
send(args);
@@ -53,18 +45,34 @@ export function sse({ request, pingInterval = 1000, updateInterval = 348, run }:
});
}
abort();
close();
}
};
pinger = setInterval(() => {
const pinger = setInterval(() => {
if (request.signal.aborted) {
return close();
}
safeSend({ event: "ping", data: new Date().toISOString() });
}, pingInterval);
updater = setInterval(async () => {
run(safeSend, abort);
const updater = setInterval(() => {
if (request.signal.aborted) {
return close();
}
run(safeSend, close);
}, updateInterval);
return abort;
const timeout = setTimeout(() => {
close();
}, 60 * 1000); // 1 minute
return () => {
clearInterval(updater);
clearInterval(pinger);
clearTimeout(timeout);
};
});
}
+1 -1
View File
@@ -102,7 +102,7 @@
"react-use": "^17.4.0",
"recharts": "^2.8.0",
"remix-auth": "^3.6.0",
"remix-auth-email-link": "^2.1.0",
"remix-auth-email-link": "2.0.2",
"remix-auth-github": "^1.6.0",
"remix-typedjson": "0.3.1",
"remix-utils": "^7.1.0",
+3 -1
View File
@@ -20,7 +20,9 @@ app.use((req, res, next) => {
next();
});
app.use(compression());
if (process.env.DISABLE_COMPRESSION !== "1") {
app.use(compression());
}
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
app.disable("x-powered-by");
+52
View File
@@ -0,0 +1,52 @@
<ParamField body="events" type="array" required>
<Expandable title="event properties" defaultOpen>
<ParamField body="name" type="string" required>
The `name` property must exactly match any subscriptions you want to
trigger.
</ParamField>
<ParamField body="payload" type="any">
The `payload` property will be sent to any matching Jobs and will appear
as the `payload` param of the `run()` function. You can leave this
parameter out if you just want to trigger a Job without any input data.
</ParamField>
<ParamField body="context" type="any">
The optional `context` property will be sent to any matching Jobs and will
be passed through as the `context.event.context` param of the `run()`
function. This is optional but can be useful if you want to pass through
some additional context to the Job.
</ParamField>
<ParamField body="id" type="string">
The `id` property uniquely identify this particular event. If unset it
will be set automatically using `ulid`.
</ParamField>
<ParamField body="timestamp" type="Date">
This is optional, it defaults to the current timestamp. Usually you would
only set this if you have a timestamp that you wish to pass through, e.g.
you receive a timestamp from a service and you want the same timestamp to
be used in your Job.
</ParamField>
<ParamField body="source" type="string">
This is optional, it defaults to "trigger.dev". It can be useful to set
this as you can filter events using this in the `eventTrigger()`.
</ParamField>
</Expandable>
</ParamField>
<ParamField body="options" type="object">
<Expandable title="properties" defaultOpen>
<ParamField body="deliverAt" type="Date">
An optional Date when you want the event to Trigger Jobs. The event will
be sent to the platform immediately but won't be acted upon until the
specified time.
</ParamField>
<ParamField body="deliverAfter" type="number">
An optional number of seconds you want to wait for the event to Trigger
any relevant Jobs. The event will be sent to the platform immediately but
won't be acted upon until the specified time.
</ParamField>
<ParamField body="accountId" type="string">
This optional param will be used by the Trigger.dev Connect feature, which
is coming soon.
</ParamField>
</Expandable>
</ParamField>
+29
View File
@@ -0,0 +1,29 @@
<ResponseField name="events" type="array">
<Expandable title="properties" defaultOpen>
<ResponseField name="id" type="string" required>
The `id` of the event that was sent.
</ResponseField>
<ResponseField name="name" type="string" required>
The `name` of the event that was sent.
</ResponseField>
<ResponseField name="payload" type="any" required>
The `payload` of the event that was sent
</ResponseField>
<ResponseField name="timestamp" type="Date" required>
The `timestamp` of the event that was sent
</ResponseField>
<ResponseField name="context" type="any">
The `context` of the event that was sent. Is `undefined` if no context was
set when sending the event.
</ResponseField>
<ResponseField name="deliverAt" type="Date">
The timestamp when the event will be delivered to any matching Jobs. Is
`undefined` if `deliverAt` or `deliverAfter` wasn't set when sending the
event.
</ResponseField>
<ResponseField name="deliveredAt" type="Date">
The timestamp when the event was delivered. Is `undefined` if `deliverAt`
or `deliverAfter` were set when sending the event.
</ResponseField>
</Expandable>
</ResponseField>
@@ -3,13 +3,11 @@ title: HTTP endpoints
description: HTTP endpoints allow you to trigger your Jobs from any webhooks.
---
<Warning>This feature is in beta and not yet available to use on the Trigger.dev Cloud.</Warning>
Sometimes you want to subscribe to changes from an API, and we don't have [an Integration](/integrations/introduction) for it yet. That's when you can use `defineHttpEndpoint` to receive webhooks, verify them, and create an [HTTP Trigger](/documentation/concepts/triggers/http).
## Defining an HTTP endpoint
Defining an HTTP endpoint creates a URL and secret which you'll enter into Cal.com's website. It also attaches a `verify` function that is called when a webhook is received. It's compulsory to return a result from this function 90% of the time you can use our `verifyRequestSignature` helper function.
We'll use Cal.com as an example:
```ts
const caldotcom = client.defineHttpEndpoint({
@@ -35,6 +33,10 @@ const caldotcom = client.defineHttpEndpoint({
});
```
When this code runs (and you're running the CLI dev command) the HTTP endpoint will be created and be visible in the Trigger.dev dashboard.
The `verify` function is compulsory and is automatically called when a webhook is received. It's required to return a result from this function 90% of the time you can use our `verifyRequestSignature` helper function.
## Getting the URL and secret
In our dashboard, you can navigate to the HTTP endpoints page. From there you can select your endpoint and copy the URL (1) and secret (2) for the appropriate Environment.
@@ -4,8 +4,6 @@ sidebarTitle: "HTTP"
description: "HTTP Triggers allow you to trigger your Jobs from any webhooks."
---
<Warning>This feature is in beta and not yet available to use on the Trigger.dev Cloud.</Warning>
Sometimes you want to subscribe to changes from an API, and we don't have [an Integration](/integrations/introduction) for it yet. That's when you can use `defineHttpEndpoint` to receive webhooks, verify them, and create an HTTP Trigger.
You should read the [HTTP endpoint](/documentation/concepts/http-endpoints) documentation to understand how to create an HTTP endpoint.
@@ -4,8 +4,6 @@ sidebarTitle: "Manual Invoke"
description: "Invoke Jobs manually using the invoke Trigger"
---
<Warning>This feature is in beta and not yet available to use on the Trigger.dev Cloud.</Warning>
Sometimes it makes sense to be able to invoke a Job manually, without having to specify an event, especially for cases where you want to get notified when the invoked Job Run is complete.
To specify that a job is manually invokable, you can use the `invokeTrigger()` function when defining a job:
+255
View File
@@ -0,0 +1,255 @@
---
title: "Task Library"
description: "These are the built-in tasks that are available to use in your Jobs."
---
Welcome to the Trigger.dev Task Library 📚. You may be wondering, what is a Task and why are there a library of them? Well you see, Trigger.dev works by divvying up a long-running job execution into a bunch of little tasks, each one taking less time then a single serverless function execution. 💫
You can define and run your own tasks easily using [io.runTask()](/sdk/io/runtask), or you can use one of our [Integrations](/integrations/introduction) which are tasks for specific APIs, like OpenAI or Stripe.
<Note>Read more about how Tasks work [here](/documentation/concepts/tasks).</Note>
We also have a growing library of built-in tasks that you can use in your Jobs through the `io` object. These tasks are designed to be generic and reusable, and are a great way to get started with Trigger.dev.
<Info>
You may notice that I'm using emojis for all the cache keys below, which is totally 💯% fine as
long as they are unique inside a run. Read more about how cache keys work
[here](/documentation/concepts/tasks#task-cache-keys)
</Info>
## `wait`
This task allows you to resume executing your job after a certain amount of time has passed:
```ts
await io.wait("⏰", 60); // wait 60 seconds
```
Internally this task is considered a "noop", and noop tasks have no output.
[reference docs](/sdk/io/wait)
## `waitForRequest`
You supply this task with a callback to receive a URL. When a POST request is made to that URL, the JSON body of the request becomes the task output.
The example below uses `waitForRequest` to capture a Screenshot of a website using [ScreenshotOne.com](https://screenshotone.com/) and passes the callback URL to the webhook URL to get notified when the screenshot is finished:
```ts
const result = await io.waitForRequest<ScreenshotResponse>(
"📸",
async (url) => {
await fetch(`https://api.screenshotone.com/take`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
webhook_url: url, // this is the URL that will be called when the screenshot is ready
access_key: "my-access-key",
url: "https://trigger.dev",
store: "true",
storage_path: "my-screeshots",
response_type: "json",
async: "true",
storage_return_location: "true",
}),
});
},
{
timeoutInSeconds: 300, // wait up to 5 minutes for the screenshot to be ready
}
);
```
We actually originally built this task for our [Replicate integration](/integrations/apis/replicate), which accepts a callback URL to notify you when a prediction is ready. So this allows you to write very succinct code to create a prediction and wait for it's results:
```ts
const sdPrediction = await io.replicate.predictions.createAndAwait("🧑‍🎨", {
version: "ac732df83cea7fff18b8472768c88ad041fa750ff7682a21affe81863cbe77e4",
input: {
prompt: "What is the meaning of life?",
},
});
```
[reference docs](/sdk/io/wait-for-request)
## `waitForEvent`
This task allows you to wait for an event to be sent. To read about how events work, check out the [Events](/documentation/concepts/triggers/events) documentation.
```ts
const event = await io.waitForEvent(
"🥂",
{
name: "user.created",
schema: z.object({
id: z.string(),
createdAt: z.coerce.date(),
isAdmin: z.boolean(),
}),
filter: {
isAdmin: [true], // Only wait for events where isAdmin is true
},
},
{
timeoutInSeconds: 60 * 60, // Wait for up to an hour
}
);
```
The event object returned from this task is the full event object that was sent, including `id`, `name`, `payload`, `context`, and more.
[reference docs](/sdk/io/wait-for-event)
## `backgroundFetch`
This task allows you to perform a `fetch` request in the background, and then resume the execution of your job after the request has completed.
```ts
const body = io.backgroundFetch<MyResponseData>("🕸️", "https://example.com/api/endpoint", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: redactString`Bearer ${auth.apiKey}`,
},
body: JSON.stringify({ foo: "bar" }),
});
```
This is useful for when an API is slow to respond and might not finish before your serverless function times out. We created this task to power our [OpenAI integration](/integrations/apis/openai), which can sometimes take more than a minute to respond:
```ts
// This uses backgroundFetch under the hood
await io.openai.chat.completions.backgroundCreate("💬", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
```
[reference docs](/sdk/io/backgroundfetch)
## `backgroundPoll`
This task is similar to `backgroundFetch`, but instead of waiting for a single request to complete, it will poll a URL until it returns a certain value.
```ts
const result = await io.backgroundPoll<{ foo: string }>("🔃", {
url: "https://example.com/api/endpoint",
interval: 10, // every 10 seconds
timeout: 300, // stop polling after 5 minutes
responseFilter: {
// stop polling once this filter matches
status: [200],
body: {
status: ["SUCCESS"],
},
},
});
```
## `logger`
The logger object allows you to log messages to the Trigger.dev console. This is useful for debugging your jobs, or just to see what's going on inside your job.
```ts
await io.logger.info("This is an info message");
```
You can optionally pass a `context` object to the logger, which will be displayed in the console:
```ts
await io.logger.info("This is an info message", {
foo: "bar",
});
```
We support the following log levels:
- `io.logger.debug()`
- `io.logger.info()`
- `io.logger.warn()`
- `io.logger.error()`
<Note>
You may notice these tasks don't include cache keys. We automatically create a cache key for you
based on the message and the log-level
</Note>
[reference docs](/sdk/io/logger)
## `random`
Use this task to generate a random number that stays stable during run retries/resumes:
```ts
const randomNumber = await io.random("🎲", {
min: 1,
max: 100,
});
```
[reference docs](/sdk/io/random)
## `sendEvent`
This task allows you to send an event from inside your job run.
If you want to send an event from outside a run (e.g. just from your backend) you should use [client.sendEvent()](/sdk/triggerclient/instancemethods/sendevent) instead.
```ts
await io.sendEvent("🚚", {
id: "e_1234567890",
name: "new.user",
payload: {
userId: "u_1234567890",
},
});
```
[reference docs](/sdk/io/sendevent)
## `getEvent`
This task allows you to get an event by ID from inside your job run.
If you want to get an event from outside a run (e.g. just from your backend) you should use [client.getEvent()](/sdk/triggerclient/instancemethods/getevent) instead.
```ts
const event = await io.getEvent("📥", "e_1234567890");
```
[reference docs](/sdk/io/getevent)
## `cancelEvent`
If you send an event that has a delivery date in the future, you can use this task to cancel it.
```ts
await io.sendEvent(
"🚚",
{
id: "e_1234567890",
name: "new.user",
payload: {
userId: "u_1234567890",
},
},
{
deliverAt: new Date(Date.now() + 1000 * 60 * 60 * 24), // deliver in 24 hours
}
);
// Later on, if you want to cancel the event:
await io.cancelEvent("🚫", "e_1234567890");
```
## `createStatus`
Coming soon
-643
View File
@@ -1,643 +0,0 @@
---
title: OpenAI tasks
sidebarTitle: Tasks
---
Tasks are executed after the job is triggered and are the main building blocks of a job. You can string together as many tasks as you want.
---
## All tasks
### `createCompletion`
Generates text completions as per given prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat)
```ts example.ts
run: async (payload, io, ctx) => {
// This code demonstrates using OpenAI's text completion with the "davinci" model.
// It generates text based on the given prompt.
await io.openai.createCompletion("completion", {
model: "davinci",
prompt: "Once upon a time",
});
},
```
### `backgroundCreateCompletion`
Generates text completions in the background. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat)
```ts example.ts
run: async (payload, io, ctx) => {
// This code showcases background text completion using the "gpt-3.5-turbo" model.
// It generates text based on the provided programming task and logs the result.
const programmingTask = `Create a function that checks if a string is a palindrome.`;
const response = await io.openai.backgroundCreateCompletion("background-completion", {
model: "gpt-3.5-turbo",
prompt: `Coding task: ${programmingTask}\n\n`,
});
await io.logger.info("codeSnippet", response.choices[0]?.text);
},
```
You can also pass an optional third parameter to `backgroundCreateCompletion` to specify OpenAI request options:
```ts requestOptions.ts
run: async (payload, io, ctx) => {
// This code showcases background text completion using the "gpt-3.5-turbo" model.
// It generates text based on the provided programming task and logs the result.
const programmingTask = `Create a function that checks if a string is a palindrome.`;
const response = await io.openai.backgroundCreateCompletion("background-completion", {
model: "gpt-3.5-turbo",
prompt: `Coding task: ${programmingTask}\n\n`,
}, {
headers: {
"User-Agent": "my-user-agent"
}
});
await io.logger.info("codeSnippet", response.choices[0]?.text);
},
```
This task is implemented using [io.backgroundFetch()](/sdk/io/backgroundfetch) and so you can also pass a 4th parameter customizing the retry and timeout options:
```ts fetchOptions.ts
run: async (payload, io, ctx) => {
// This code showcases background text completion using the "gpt-3.5-turbo" model.
// It generates text based on the provided programming task and logs the result.
const programmingTask = `Create a function that checks if a string is a palindrome.`;
const response = await io.openai.backgroundCreateCompletion("background-completion", {
model: "gpt-3.5-turbo",
prompt: `Coding task: ${programmingTask}\n\n`,
}, {
headers: {
"User-Agent": "my-user-agent"
}
}, {
timeout: {
durationInMs: 10000,
retry: {
limit: 3,
minTimeoutInMs: 1000,
factor: 2,
}
}
});
await io.logger.info("codeSnippet", response.choices[0]?.text);
},
```
### `createChatCompletion`
Generates text completions in a conversational context. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat/create)
```ts example.ts
run: async (payload, io, ctx) => {
// This code demonstrates chat completion with the "gpt-3.5-turbo" model.
// It simulates a conversation by providing messages and receiving a chat response.
await io.openai.createChatCompletion("chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
},
```
### `backgroundCreateChatCompletion`
Generates text completions in a conversational context in the background. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat/object)
```ts example.ts
run: async (payload, io, ctx) => {
// This code showcases background chat completion using the "gpt-3.5-turbo" model.
// It simulates a conversation with a user message and logs the response choices.
const response = await io.openai.backgroundCreateChatCompletion("background-chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
await io.logger.info("choices", response.choices);
},
```
You can also use the more "fluent" pattern used by the OpenAI SDK:
```ts fluent.ts
run: async (payload, io, ctx) => {
// This code showcases background chat completion using the "gpt-3.5-turbo" model.
// It simulates a conversation with a user message and logs the response choices.
const response = await io.openai.chat.completions.backgroundCreate("background-chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
await io.logger.info("choices", response.choices);
},
```
Additionally, you can pass an optional third parameter to specify OpenAI request options:
```ts requestOptions.ts
run: async (payload, io, ctx) => {
// This code showcases background text completion using the "gpt-3.5-turbo" model.
// It generates text based on the provided programming task and logs the result.
const programmingTask = `Create a function that checks if a string is a palindrome.`;
const response = await io.openai.chat.completions.backgroundCreate("background-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
}, {
headers: {
"User-Agent": "my-user-agent"
}
});
await io.logger.info("choices", response.choices);
},
```
This task is implemented using [io.backgroundFetch()](/sdk/io/backgroundfetch) and so you can also pass a 4th parameter customizing the retry and timeout options:
```ts fetchOptions.ts
run: async (payload, io, ctx) => {
// This code showcases background text completion using the "gpt-3.5-turbo" model.
// It generates text based on the provided programming task and logs the result.
const programmingTask = `Create a function that checks if a string is a palindrome.`;
const response = await io.openai.chat.completions.backgroundCreate("background-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
}, {
headers: {
"User-Agent": "my-user-agent"
}
}, {
timeout: {
durationInMs: 10000,
retry: {
limit: 3,
minTimeoutInMs: 1000,
factor: 2,
}
}
});
await io.logger.info("choices", response.choices);
},
```
### `retrieveModel`
Retrieves a specific model by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models/retrieve)
```ts example.ts
run: async (payload, io, ctx) => {
// In this code snippet, we retrieve detailed information about a specific OpenAI model.
// Specify the ID of the model you want to retrieve. Replace 'your_model_id' with the actual model ID.
const modelIdToRetrieve = "your_model_id";
try {
// Retrieve the model information using the OpenAI API
const retrievedModel = await io.openai.retrieveModel("get-model", {
model: modelIdToRetrieve,
});
// Log the detailed model information
await io.logger.info("retrievedModel", retrievedModel);
} catch (error) {
// Handle errors, such as if the model with the provided ID does not exist.
await io.logger.error("Error retrieving model:", error.message);
}
},
```
### `listModels`
Lists the available models. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models/list)
```ts example.ts
run: async (payload, io, ctx) => {
// This code lists available models without retrieving detailed information.
const models = await io.openai.listModels("list-models");
},
```
### `createEdit`
Edits a given text prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/edits/create)
```ts example.ts
run: async (payload, io, ctx) => {
// This code snippet demonstrates using the OpenAI API to create an edit task.
// Specify the task parameters:
const editTaskParams = {
model: "text-davinci-edit-001", // Replace with the desired model
input: "Thsi is ridddled with erors", // Replace with the input text
instruction: "Fix the spelling errors", // Replace with the editing instruction
};
try {
// Create an edit task using the OpenAI API
const editResponse = await io.openai.createEdit("edit", editTaskParams);
// Log the response
await io.logger.info("editResponse", editResponse);
} catch (error) {
// Handle any potential errors that may occur during the API request.
await io.logger.error("Error creating edit task:", error.message);
}
},
```
### `createImage`
Generates images from textual descriptions. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/create)
```ts example.ts
run: async (payload, io, ctx) => {
const imageResults = await io.openai.createImage("image", {
prompt: "A hedgehog wearing a party hat",
n: 2,
size: "256x256",
response_format: "url",
});
```
### `createImageEdit`
Creates an edited or extended image given an original image and a prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/createEdit)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for the image edit
const imageEditParams = {
style: "data:image/png;base64,base64_encoded_style_image",
content: "data:image/png;base64,base64_encoded_content_image",
};
// Create the image edit using the OpenAI API
const imageEditResponse = await io.openai.createImageEdit(imageEditParams);
// Log the response
await io.logger.info("imageEditResponse", imageEditResponse);
},
```
### `createImageVariation`
Creates a variation of a given image. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/createVariation)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for creating an image variation
const imageVariationParams = {
image: "data:image/png;base64,base64_encoded_image",
variation: "brightness(1.2) contrast(0.8) rotate(45deg)",
};
// Create the image variation using the OpenAI API
const imageVariationResponse = await io.openai.createImageVariation(imageVariationParams);
// Log the response
await io.logger.info("imageVariationResponse", imageVariationResponse);
},
```
### `createEmbedding`
Generates embeddings for a given text. [Official OpenAI Docs](hhttps://platform.openai.com/docs/api-reference/embeddings/object)
```ts example.ts
run: async (payload, io, ctx) => {
// This code snippet demonstrates using the OpenAI API to create a text embedding.
// Specify the task parameters:
const embeddingTaskParams = {
model: "text-embedding-ada-002", // Replace with the desired model
input: "The food was delicious and the waiter...", // Replace with the input text
};
try {
// Create a text embedding using the OpenAI API
const embeddingResponse = await io.openai.createEmbedding("embedding", embeddingTaskParams);
// Log the response
await io.logger.info("embeddingResponse", embeddingResponse);
} catch (error) {
// Handle any potential errors that may occur during the API request.
await io.logger.error("Error creating text embedding:", error.message);
}
},
```
### `createFile`
Uploads a file to the OpenAI API. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/object)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for creating a file
const fileParams = {
name: "example.txt",
content: "This is the content of the file.",
};
// Create the file using the OpenAI API
const fileResponse = await io.openai.createFile(fileParams);
// Log the response
await io.logger.info("fileResponse", fileResponse);
},
```
### `listFiles`
Lists the uploaded files. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/list)
```ts example.ts
run: async (payload, io, ctx) => {
// List the files available in your OpenAI account
const fileListResponse = await io.openai.listFiles();
// Log the list of files
await io.logger.info("fileListResponse", fileListResponse);
},
```
### `createFineTuneFile`
Uploads a file for fine-tuning a model. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for creating a fine-tune file
const fineTuneFileParams = {
model: "text-davinci-002",
prompt: "Translate English to French: 'Hello, world.'",
language: "en",
description: "Fine-tune file for translation task",
};
// Create the fine-tune file using the OpenAI API
const fineTuneFileResponse = await io.openai.createFineTuneFile(fineTuneFileParams);
// Log the response
await io.logger.info("fineTuneFileResponse", fineTuneFileResponse);
},
```
### `createFineTune`
Fine-tunes a model on a given task. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/create)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for creating a fine-tune task
const fineTuneParams = {
model: "text-davinci-002",
dataset: "your_dataset_id",
description: "Fine-tune task for custom dataset",
};
// Create the fine-tune task using the OpenAI API
const fineTuneResponse = await io.openai.createFineTune(fineTuneParams);
// Log the response
await io.logger.info("fineTuneResponse", fineTuneResponse);
},
```
### `listFineTunes`
Lists the available fine-tunes. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list)
```ts example.ts
run: async (payload, io, ctx) => {
// List the fine-tunes available in your OpenAI account
const fineTunesListResponse = await io.openai.listFineTunes();
// Log the list of fine-tunes
await io.logger.info("fineTunesListResponse", fineTunesListResponse);
},
```
### `retrieveFineTune`
Retrieves a specific fine-tune by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/retrieve)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the ID of the fine-tune you want to retrieve
const fineTuneId = "your_fine_tune_id"; // Replace with the actual fine-tune ID
// Retrieve the fine-tune using the OpenAI API
const retrievedFineTune = await io.openai.retrieveFineTune(fineTuneId);
// Log the retrieved fine-tune
await io.logger.info("retrievedFineTune", retrievedFineTune);
},
```
### `cancelFineTune`
Cancels a specific fine-tune by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/cancel)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the ID of the fine-tune you want to cancel
const fineTuneIdToCancel = "your_fine_tune_id"; // Replace with the actual fine-tune ID
// Cancel the specified fine-tune using the OpenAI API
const cancellationResponse = await io.openai.cancelFineTune(fineTuneIdToCancel);
// Log the cancellation response
await io.logger.info("cancellationResponse", cancellationResponse);
},
```
### `createFineTuningJob`
Creates a job that fine-tunes a specified model from a given dataset. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/create)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for creating a fine-tuning job
const fineTuningJobParams = {
fineTuneId: "your_fine_tune_id", // Replace with the actual fine-tune ID
datasetId: "your_dataset_id", // Replace with the ID of your dataset
model: "text-davinci-002", // Replace with the model for fine-tuning
n_examples: 100, // Replace with the number of examples
};
// Create the fine-tuning job using the OpenAI API
const fineTuningJobResponse = await io.openai.createFineTuningJob(fineTuningJobParams);
// Log the response
await io.logger.info("fineTuningJobResponse", fineTuningJobResponse);
},
```
### `retrieveFineTuningJob`
Get info about a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/retrieve)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the ID of the fine-tuning job you want to retrieve
const fineTuningJobId = "your_fine_tuning_job_id"; // Replace with the actual job ID
// Retrieve the fine-tuning job using the OpenAI API
const retrievedJob = await io.openai.retrieveFineTuningJob(fineTuningJobId);
// Log the retrieved job
await io.logger.info("retrievedJob", retrievedJob);
},
```
### `cancelFineTuningJob`
Cancel a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/cancel)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the ID of the fine-tuning job you want to cancel
const fineTuningJobIdToCancel = "your_fine_tuning_job_id"; // Replace with the actual job ID
// Cancel the specified fine-tuning job using the OpenAI API
const cancellationResponse = await io.openai.cancelFineTuningJob(fineTuningJobIdToCancel);
// Log the cancellation response
await io.logger.info("cancellationResponse", cancellationResponse);
},
```
### `listFineTuningJobEvents`
List events for a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list-events)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the ID of the fine-tuning job for which you want to list events
const fineTuningJobId = "your_fine_tuning_job_id"; // Replace with the actual job ID
// List events for the specified fine-tuning job using the OpenAI API
const eventsListResponse = await io.openai.listFineTuningJobEvents(fineTuningJobId);
// Log the list of events
await io.logger.info("eventsListResponse", eventsListResponse);
},
```
### `listFineTuningJobs`
List fine tuning jobs. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list)
```ts example.ts
run: async (payload, io, ctx) => {
// List the fine-tuning jobs available in your OpenAI account
const jobsListResponse = await io.openai.listFineTuningJobs();
// Log the list of fine-tuning jobs
await io.logger.info("jobsListResponse", jobsListResponse);
},
```
## Example usage
In this example we'll create a task that generates a random joke using OpenAI GPT 3.5 .
```ts example.ts
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
import { OpenAI } from "@trigger.dev/openai";
import { z } from "zod";
// Initialize a TriggerClient with the ID "jobs-showcase"
const client = new TriggerClient({ id: "jobs-showcase" });
// Create an instance of the OpenAI client and provide the OpenAI API key from environment variables
const openai = new OpenAI({
id: "openai",
apiKey: process.env.OPENAI_API_KEY!, // Replace with your actual OpenAI API key
});
// Define a job that uses OpenAI GPT-3.5 Turbo to tell jokes
client.defineJob({
id: "openai-tell-me-a-joke",
name: "OpenAI: tell me a joke",
version: "1.0.0",
trigger: eventTrigger({
name: "openai.tasks", // Define the trigger event name
schema: z.object({
jokePrompt: z.string(), // Expect a joke prompt as input
}),
}),
integrations: {
openai, // Use the OpenAI integration for this job
},
run: async (payload, io, ctx) => {
// Retrieve information about the GPT-3.5 Turbo model
await io.openai.retrieveModel("get-model", {
model: "gpt-3.5-turbo",
});
// List available models (optional, for reference)
const models = await io.openai.listModels("list-models");
// Generate a joke in the background using the chat conversation format
const jokeResult = await io.openai.backgroundCreateChatCompletion(
"background-chat-completion",
{
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: payload.jokePrompt, // User-provided joke prompt
},
],
}
);
// Return the generated joke as the result
return {
joke: jokeResult.choices[0]?.message?.content,
};
},
});
// These lines are specific to the Express framework and can be removed if not needed
import { createExpressServer } from "@trigger.dev/express";
createExpressServer(client);
```
+63 -3
View File
@@ -49,11 +49,71 @@ const openai = new OpenAI({
## Tasks
Once you have set up a OpenAI client, you can use it to create tasks.
Once you have set up a OpenAI client, you can add it to your job and start using the provided tasks:
```ts
client.defineJob({
id: "openai-job",
name: "OpenAI Job",
version: "1.0.0",
trigger: invokeTrigger(),
integrations: {
openai, // Add the OpenAI client as an integration
},
run: async (payload, io, ctx) => {
// Now you can access it through the io object
const completion = await io.openai.chat.completions.create("completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
},
});
```
As you can see above, we've replicated the API of the [OpenAI TypeScript SDK](https://github.com/openai/openai-node), with a crucial difference of adding the [Task Cache Key](https://trigger.dev/docs/documentation/concepts/tasks#task-cache-keys) as the first parameter.
We've also added a few convenience methods to make it easier to work with the OpenAI API, especially in a serverless environment. For example, you can run a Chat Completion task in the background with [backgroundCreate()](/integrations/apis/openai/chat#completions-backgroundcreate):
```ts
const completion = await io.openai.chat.completions.backgroundCreate("completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
```
See our full task reference below:
<CardGroup>
<Card title="Tasks" icon="sparkles" href="/integrations/apis/openai-tasks">
Perform different AI-powered tasks using OpenAI.
<Card title="Chat Completions" icon="sparkles" href="/integrations/apis/openai/chat">
Given a list of messages comprising a conversation, the model will return a response
</Card>
<Card title="Assistants (Beta)" icon="arrows-spin" href="/integrations/apis/openai/assistants">
Build assistants that can call models and use tools to perform tasks
</Card>
<Card title="Files" icon="file" href="/integrations/apis/openai/files">
Upload files to use with assistants and fine-tuning
</Card>
<Card title="Images" icon="image" href="/integrations/apis/openai/images">
Given a prompt and/or an input image, the model will generate a new image
</Card>
<Card title="Fine Tuning Jobs" icon="vial" href="/integrations/apis/openai/fine-tunes">
Manage fine-tuning jobs to tailor a model to your specific training data
</Card>
<Card title="Models" icon="server" href="/integrations/apis/openai/models">
List and describe the various models available in the API
</Card>
<Card title="Completions (Legacy)" icon="scroll" href="/integrations/apis/openai/completions">
Given a prompt, the model will return one or more predicted completions.
</Card>
</CardGroup>
@@ -0,0 +1,287 @@
---
title: Assistant Tasks
sidebarTitle: Assitants (Beta)
---
<Note>
This feature is currently marked as a "Beta" by OpenAI. Make sure to check our their [How
Assistants Work](https://platform.openai.com/docs/assistants/how-it-works) and [Assistants
Overview](https://platform.openai.com/docs/assistants/overview) guides.
</Note>
## Assistants
Build assistants that can call models and use tools to perform tasks. [Official OpenAI docs](https://platform.openai.com/docs/api-reference/assistants)
### `create()`
Create an assistant with a model and instructions. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/assistants/createAssistant)
```ts example.ts
const file = await io.openai.files.createAndWaitForProcessing("upload-file", {
purpose: "assistants",
file: fs.createReadStream("./fixtures/mydata.csv"),
});
const assistant = await io.openai.beta.assistants.create("create-assistant", {
name: "Data visualizer",
description:
"You are great at creating beautiful data visualizations. You analyze data present in .csv files, understand trends, and come up with data visualizations relevant to those trends. You also share a brief text summary of the trends observed.",
model: "gpt-4-1106-preview",
tools: [{ type: "code_interpreter" }],
file_ids: [file.id],
});
```
## Threads
Create threads that assistants can interact with. [Official OpenAI docs](https://platform.openai.com/docs/api-reference/threads/createThread)
### `create()`
Create a thread. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/assistants/createAssistant)
```ts example.ts
const thread = await io.openai.beta.threads.create("create-thread", {
messages: [
{
role: "user",
content: "Create 3 data visualizations based on the trends in this file.",
file_ids: [fileId],
},
],
});
```
### `createAndRun()`
Create a thread and run it in one task.
```ts example.ts
const run = await io.openai.beta.threads.createAndRun("create-and-run-thread", {
assistant_id: "asst_abc123",
thread: {
messages: [
{
role: "user",
content: "Create 3 data visualizations based on the trends in this file.",
file_ids: [fileId],
},
],
},
});
```
### `createAndRunUntilCompletion()`
Create a thread and runs it in one task, and only returns when the run is completed by polling in the background using [io.backgroundPoll()](/sdk/io/background-poll).
```ts example.ts
const run = await io.openai.beta.threads.createAndRunUntilCompletion("create-thread", {
assistant_id: "asst_abc123",
thread: {
messages: [
{
role: "user",
content: "Create 3 data visualizations based on the trends in this file.",
file_ids: [fileId],
},
],
},
});
if (run.status !== "completed") {
throw new Error(`Run finished with status ${run.status}: ${JSON.stringify(run.last_error)}`);
}
// List all messages in the thread
const messages = await io.openai.beta.threads.messages.list("list-messages", run.thread_id);
```
### `retrieve()`
Retrieves a thread. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/threads/getThread)
```ts example.ts
const thread = await io.openai.beta.threads.retrieve("get-thread", "thread_abc123");
```
### `update()`
Modifies a thread. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/threads/modifyThread)
```ts example.ts
await io.openai.beta.threads.update("update-thread", "thread_abc123", {
metadata: {
foo: "bar",
},
});
```
### `del()`
Deletes a thread. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/threads/deleteThread)
```ts example.ts
const deletedThread = await io.openai.beta.threads.del("update-thread", "thread_abc123");
```
## Messages
Create messages within threads. [Official OpenAI docs](https://platform.openai.com/docs/api-reference/messages)
### `list()`
List all messages in a thread.
```ts example.ts
const messages = await io.openai.beta.threads.messages.list("list-messages", "thread_abc123");
```
### `create()`
Create a message. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/messages/createMessage)
```ts example.ts
const thread = await io.openai.beta.threads.create("get-thread");
const message = await io.openai.beta.threads.messages.create("create-message", thread.id, {
role: "user",
content: "Create 3 data visualizations based on the trends in this file.",
file_ids: [fileId],
});
```
### `retrieve()`
Retrieve a message. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/messages/getMessage)
```ts example.ts
const message = await io.openai.beta.threads.messages.retrieve(
"get-message",
"thread_abc123",
"message_abc123"
);
```
### `update()`
Update a message. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/messages/modifyMessage)
```ts example.ts
await io.openai.beta.threads.messages.update("update-message", thread.id, message.id, {
metadata: {
foo: "bar",
},
});
```
## Runs
Represents an execution run on a thread. [Official OpenAI docs](https://platform.openai.com/docs/api-reference/runs)
### `list()`
List all runs belonging to a thread.
```ts example.ts
const runs = await io.openai.beta.threads.runs.list("list-runs", "thread_abc123");
```
### `create()`
Create a run. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/runs/createRun)
```ts example.ts
const run = await io.openai.beta.threads.runs.create("create-run", "thread_abc123", {
assistant_id: payload.id,
});
```
### `createAndWaitForCompletion()`
Create a run and only return when the run is completed by polling in the background using [io.backgroundPoll()](/sdk/io/background-poll).
```ts example.ts
const run = await io.openai.beta.threads.runs.createAndWaitForCompletion(
"create-run",
"thread_abc123",
{
assistant_id: payload.id,
}
);
if (run.status !== "completed") {
throw new Error(`Run finished with status ${run.status}: ${JSON.stringify(run.last_error)}`);
}
const messages = await io.openai.beta.threads.messages.list("list-messages", run.thread_id);
```
### `waitForCompletion()`
Wait for a run to complete by polling in the background using [io.backgroundPoll()](/sdk/io/background-poll).
```ts example.ts
const run = await io.openai.beta.threads.runs.create("create-run", "thread_abc123", {
assistant_id: payload.id,
});
const completedRun = await io.openai.beta.threads.runs.waitForCompletion(
"wait-for-completion",
"thread_abc123",
run.id
);
if (completedRun.status !== "completed") {
throw new Error(
`Run finished with status ${completedRun.status}: ${JSON.stringify(completedRun.last_error)}`
);
}
const messages = await io.openai.beta.threads.messages.list("list-messages", run.thread_id);
```
### `retrieve()`
Retrieve a run. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/runs/getRun)
```ts example.ts
const run = await io.openai.beta.threads.runs.retrieve("get-run", "thread_abc123", "run_abc123");
```
### `cancel()`
Cancels a run that is `in_progress`. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/runs/cancelRun)
```ts example.ts
const run = await io.openai.beta.threads.runs.cancel("cancel-run", "thread_abc123", "run_abc123");
```
### `submitToolOutputs()`
When a run has the `status: "requires_action"` and `required_action.type` is `submit_tool_outputs`, this endpoint can be used to submit the outputs from the tool calls once they're all completed. All outputs must be submitted in a single request. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/runs/submitToolOutputs)
```ts example.ts
const run = await io.openai.beta.threads.runs.submitToolOutputs(
"submit-tool-outputs",
"thread_abc123",
"run_abc123",
{
tool_outputs: [
{
tool_call_id: "tool_run_abc123",
output: "This is the output of the tool call.",
},
],
}
);
```
### `list()`
Returns all runs belonging to a thread. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/runs/listRuns)
```ts example.ts
const runs = await io.openai.beta.threads.runs.list("list-runs", "thread_abc123");
```
+38
View File
@@ -0,0 +1,38 @@
---
title: Chat Completion Tasks
sidebarTitle: Chat Completions
---
Given a list of messages comprising a conversation, the model will return a response. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat)
### `completions.create()`
Creates a model response for the given chat conversation. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat/create)
```ts example.ts
await io.openai.chat.completions.create("chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
```
### `completions.backgroundCreate()`
Creates a model response for the given chat conversation, but runs the request in the background using [io.backgroundFetch()](/sdk/io/backgroundfetch)
```ts example.ts
await io.openai.chat.completions.create("chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
```
@@ -0,0 +1,19 @@
---
title: Completion Tasks
sidebarTitle: Completions (Legacy)
---
Given a prompt, the model will return one or more predicted completions, and can also return the probabilities of alternative tokens at each position. We recommend most users use the Chat Completions API. [Learn more](https://platform.openai.com/docs/deprecations/2023-07-06-gpt-and-embeddings)
### `create()`
<Warning>This is a legacy API</Warning>
Creates a completion for the provided prompt and parameters. [Official OpenAI docs](https://platform.openai.com/docs/api-reference/completions/create)
```ts example.ts
const completion = await io.openai.completions.create("completion", {
model: "text-davinci-003",
prompt: "Create a good programming joke about Tasks",
});
```
+62
View File
@@ -0,0 +1,62 @@
---
title: File Tasks
sidebarTitle: Files
---
Files are used to upload documents that can be used with features like [Assistants](https://platform.openai.com/docs/api-reference/assistants) and [Fine-tuning](https://platform.openai.com/docs/api-reference/fine-tuning). [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files)
### `list()`
Returns a list of files that belong to the user's organization. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/list)
```ts example.ts
await io.openai.files.list("list-files");
await io.openai.files.list("list-files", { purpose: "assistants" }); // gets only assistant files
```
### `create()`
Upload a file that can be used across various endpoints/features. The size of all the files uploaded by one organization can be up to 100 GB.
The size of individual files for can be a maximum of `512MB`. See the [Assistants Tools guide](https://platform.openai.com/docs/assistants/tools) to learn more about the types of files supported. The Fine-tuning API only supports `.jsonl` files.
[Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/create)
```ts example.ts
const file = await io.openai.files.create("upload-file", {
purpose: "assistants",
file: fs.createReadStream("./fixtures/mydata.csv"),
});
```
### `createAndWaitForProcessing()`
Upload a file and will return when the file is processed by polling in the background using [io.backgroundPoll()](/sdk/io/background-poll).
```ts example.ts
const file = await io.openai.files.createAndWaitForProcessing("upload-file", {
purpose: "assistants",
file: fs.createReadStream("./fixtures/mydata.csv"),
});
```
### `waitForProcessing()`
Will return when the file is processed by polling in the background using [io.backgroundPoll()](/sdk/io/background-poll).
```ts example.ts
const file = await io.openai.files.create("upload-file", {
purpose: "assistants",
file: fs.createReadStream("./fixtures/mydata.csv"),
});
const processedFile = await io.openai.files.waitForProcessing("wait", file.id);
```
### `retrieve()`
Returns information about a specific file. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/retrieve)
```ts example.ts
await io.openai.files.retrieve("retrieve-file", "file-id");
```
@@ -0,0 +1,59 @@
---
title: Fine Tuning Tasks
sidebarTitle: Fine Tunes
---
Manage fine-tuning jobs to tailor a model to your specific training data. See the related guide [Fine Tuning models](https://platform.openai.com/docs/guides/fine-tuning) and view the [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning).
### `jobs.create()`
Creates a job that fine-tunes a specified model from a given dataset.
Response includes details of the enqueued job including job status and the name of the fine-tuned models once complete.
You must first upload a dataset to the API before creating a fine-tuning job. See our [OpenAI File Tasks](/integrations/apis/openai/files#createandwaitforprocessing) for more information.
```ts example.ts
const file = await io.openai.files.create("upload-file", {
purpose: "fine-tune",
file: fs.createReadStream("./mydata.jsonl"),
});
const fineTuning = await io.openai.fineTuning.jobs.create("fine-tuning", {
training_file: file.id,
model: "gpt-3.5-turbo",
suffix: "my-model",
});
```
### `jobs.list()`
List your organization's fine-tuning jobs. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list)
```ts example.ts
const fts = await io.openai.fineTuning.jobs.list("list");
```
### `jobs.retrieve()`
Get info about a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/retrieve)
```ts example.ts
const fineTuning = await io.openai.fineTuning.jobs.retrieve("fine-tuning", "ft_1234");
```
### `jobs.cancel()`
Immediately cancel a fine-tune job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/cancel)
```ts example.ts
const fineTuning = await io.openai.fineTuning.jobs.cancel("fine-tuning", "ft_1234");
```
### `jobs.listEvents()`
Get status updates for a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list-events)
```ts example.ts
const events = await io.openai.fineTuning.jobs.listEvents("fine-tuning", { id: "ft_1234" });
```
+55
View File
@@ -0,0 +1,55 @@
---
title: Image Tasks
sidebarTitle: Images
---
Given a prompt and/or an input image, the model will generate a new image. See the [Image generation guide](https://platform.openai.com/docs/guides/images) and the [Official OpenAI docs](https://platform.openai.com/docs/api-reference/images).
### `create()`
Creates an image given a prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/create)
```ts example.ts
await io.openai.images.create("dalle-3", {
model: "dall-e-3",
prompt:
"I would like to generate an image of an american giraffe riding a bycicle in a suburban neighborhood, into the sunset.",
});
```
### `backgroundCreate()`
Creates a an image given a prompt, but runs the request in the background using [io.backgroundFetch()](/sdk/io/backgroundfetch)
```ts example.ts
await io.openai.images.backgroundCreate("dalle-3", {
model: "dall-e-3",
prompt:
"I would like to generate an image of an american giraffe riding a bycicle in a suburban neighborhood, into the sunset.",
});
```
### `edit()`
Creates an edited or extended image given an original image and a prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/createEdit)
```ts example.ts
await io.openai.images.edit("dalle-2", {
model: "dall-e-2",
image: fs.createReadStream("./giraffe.jpg"),
prompt: "A painting of a giraffe in a suburban neighborhood",
response_format: "url",
});
```
### `createVariation()`
Creates a variation of a given image. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/createVariation)
```ts example.ts
await io.openai.images.createVariation("dalle-3", {
model: "dall-e-2",
image: fs.createReadStream("./giraffe.jpg"),
response_format: "url",
});
```
+14
View File
@@ -0,0 +1,14 @@
---
title: Model Tasks
sidebarTitle: Models
---
List and describe the various models available in the API. You can refer to the Models documentation to understand what models are available and the differences between them. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models)
### `list`
Lists the available models. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models/list)
```ts example.ts
const models = await io.openai.models.list("list-models");
```
+51 -10
View File
@@ -1,6 +1,9 @@
{
"$schema": "https://mintlify.com/schema.json",
"name": "Trigger.dev",
"openapi": [
"/openapi.yml"
],
"logo": {
"dark": "/logo/dark.png",
"light": "/logo/light.png",
@@ -95,6 +98,7 @@
]
},
"documentation/guides/writing-jobs-step-by-step",
"documentation/guides/task-library",
"documentation/guides/video-walkthrough"
]
},
@@ -251,7 +255,10 @@
"pages": [
{
"group": "Airtable",
"pages": ["integrations/apis/airtable", "integrations/apis/airtable-tasks"]
"pages": [
"integrations/apis/airtable",
"integrations/apis/airtable-tasks"
]
},
{
"group": "GitHub",
@@ -264,18 +271,33 @@
"integrations/apis/linear",
{
"group": "OpenAI",
"pages": ["integrations/apis/openai", "integrations/apis/openai-tasks"]
"pages": [
"integrations/apis/openai",
"integrations/apis/openai/chat",
"integrations/apis/openai/assistants",
"integrations/apis/openai/files",
"integrations/apis/openai/images",
"integrations/apis/openai/fine-tunes",
"integrations/apis/openai/models",
"integrations/apis/openai/completions"
]
},
{
"group": "Plain",
"pages": ["integrations/apis/plain", "integrations/apis/plain-tasks"]
"pages": [
"integrations/apis/plain",
"integrations/apis/plain-tasks"
]
},
"integrations/apis/replicate",
"integrations/apis/resend",
"integrations/apis/sendgrid",
{
"group": "Slack",
"pages": ["integrations/apis/slack", "integrations/apis/slack-tasks"]
"pages": [
"integrations/apis/slack",
"integrations/apis/slack-tasks"
]
},
"integrations/apis/stripe",
{
@@ -302,6 +324,7 @@
"group": "Instance methods",
"pages": [
"sdk/triggerclient/instancemethods/sendevent",
"sdk/triggerclient/instancemethods/sendevents",
"sdk/triggerclient/instancemethods/getevent",
"sdk/triggerclient/instancemethods/cancel-event",
"sdk/triggerclient/instancemethods/cancel-runs-for-event",
@@ -322,11 +345,15 @@
"pages": [
"sdk/io/overview",
"sdk/io/runtask",
"sdk/io/wait",
"sdk/io/logger",
"sdk/io/sendevent",
"sdk/io/sendevents",
"sdk/io/wait",
"sdk/io/wait-for-event",
"sdk/io/wait-for-request",
"sdk/io/backgroundfetch",
"sdk/io/background-poll",
"sdk/io/random",
"sdk/io/logger",
"sdk/io/try",
"sdk/io/registerinterval",
"sdk/io/unregisterinterval",
@@ -350,7 +377,10 @@
"sdk/dynamictrigger/constructor",
{
"group": "Instance methods",
"pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"]
"pages": [
"sdk/dynamictrigger/register",
"sdk/dynamictrigger/unregister"
]
}
]
},
@@ -361,7 +391,10 @@
"sdk/dynamicschedule/constructor",
{
"group": "Instance methods",
"pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"]
"pages": [
"sdk/dynamicschedule/register",
"sdk/dynamicschedule/unregister"
]
}
]
},
@@ -371,6 +404,12 @@
"sdk/verify-request-signature"
]
},
{
"group": "HTTP Reference",
"pages": [
"sdk/api-reference/events/create-an-event"
]
},
{
"group": "React SDK",
"pages": [
@@ -383,7 +422,9 @@
},
{
"group": "Overview",
"pages": ["examples/introduction"]
"pages": [
"examples/introduction"
]
}
],
"footerSocials": {
@@ -396,4 +437,4 @@
"apiKey": "phc_hwYmedO564b3Ik8nhA4Csrb5SueY0EwFJWCbseGwWW"
}
}
}
}
+137
View File
@@ -0,0 +1,137 @@
openapi: 3.0.0
info:
title: Trigger.dev API
description: API for triggering events in Trigger.dev
version: 1.0.0
servers:
- url: https://api.trigger.dev
description: Trigger.dev API server
security:
- BearerAuth: []
paths:
/api/v1/events:
post:
operationId: sendEvent
externalDocs:
description: Find more info here
url: "https://trigger.dev/docs/api/events/send-event"
tags:
- Events
summary: Create an event
description: Send an event to Trigger.dev to trigger job runs through eventTrigger()
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/EventRequest"
responses:
"200":
description: Event successfully sent
content:
application/json:
schema:
$ref: "#/components/schemas/EventResponse"
"400":
description: Invalid request
"401":
description: Unauthorized - API key is missing or invalid
"422":
description: Invalid request body
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
schemas:
Error:
type: object
properties:
message:
type: string
EventRequest:
type: object
properties:
event:
type: object
required:
- name
properties:
name:
type: string
description: The name of the event
payload:
type: object
additionalProperties: true
description: The payload of the event
context:
type: object
additionalProperties: true
description: An optional context object
id:
type: string
description: Unique identifier for the event. Auto-generated if not provided. If you provide an ID that already exists, the event will not be redelivered.
timestamp:
type: string
format: date-time
description: Event timestamp. Defaults to current timestamp if not provided.
source:
type: string
description: Event source, default is 'trigger.dev'.
options:
type: object
properties:
deliverAt:
type: string
format: date-time
description: Optional Date to deliver the event.
deliverAfter:
type: integer
description: Optional delay in seconds before delivering the event.
accountId:
type: string
description: Optional account ID to associate with the event.
EventResponse:
type: object
properties:
id:
type: string
description: The ID of the event that was sent.
name:
type: string
description: The name of the event that was sent.
payload:
$ref: "#/components/schemas/DeserializedJson"
context:
$ref: "#/components/schemas/DeserializedJson"
nullable: true
description: The context of the event that was sent. Null if no context was set.
timestamp:
type: string
format: date-time
description: The timestamp of the event that was sent.
deliverAt:
type: string
format: date-time
nullable: true
description: The timestamp when the event will be delivered. Null if not applicable.
deliveredAt:
type: string
format: date-time
nullable: true
description: The timestamp when the event was delivered. Null if not applicable.
cancelledAt:
type: string
format: date-time
nullable: true
description: The timestamp when the event was cancelled. Null if the event wasn't cancelled.
DeserializedJson:
type: object
additionalProperties: true
description: A JSON object that represents the deserialized payload or context.
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
@@ -0,0 +1,3 @@
---
openapi: post /api/v1/events
---
-2
View File
@@ -3,8 +3,6 @@ title: "invokeTrigger()"
description: "Use `invokeTrigger()` to allow a Job to be manually triggered using `Job.invoke()`"
---
<Warning>This feature is in beta and not yet available to use on the Trigger.dev Cloud.</Warning>
Setting `invokeTrigger()` on a job allows you to manually trigger the job using `Job.invoke()`, either from your backend or from inside another job. See our [Invoke Trigger guide](/documentation/guides/invoke) for more info.
## Parameters
+137
View File
@@ -0,0 +1,137 @@
---
title: "io.backgroundPoll()"
sidebarTitle: "backgroundPoll()"
description: "`io.backgroundPoll()` allows you to fetch data from a URL on an interval."
---
## Parameters
<Snippet file="stable-key-param.mdx" />
<ResponseField name="url" type="string" required>
The url to fetch.
</ResponseField>
<ResponseField name="interval" type="number" required>
The interval in seconds to wait between requests. Minimum interval is 10 seconds and maximum is 5
minutes.
</ResponseField>
<ResponseField name="timeout" type="number" required>
The timeout in seconds before aborting the polling. Minimum timeout is 30 seconds and maximum is 1
hour.
</ResponseField>
<ResponseField name="requestInit" type="RequestInit">
Options for the fetch request
<Expandable title="options" defaultOpen>
<ResponseField name="method" type="string">
The HTTP method to use for the request.
</ResponseField>
<ResponseField name="headers" type="object">
Any headers to send with the request. Note that you can use [redactString](sdk/redactString) to
prevent sensitive information from being stored (e.g. in the logs), like API keys and tokens.
</ResponseField>
<ResponseField name="body" type="string | ArrayBuffer">
The body of the request.
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="responseFilter" type="ResponseFilter">
Allows you to filter the response to determine when to stop polling.
<Expandable title="options" defaultOpen>
<ResponseField name="status" type="string[]">
An array of status codes to match against.
</ResponseField>
<ResponseField name="headers" type="EventFilter">
An object of header key/values to match. This uses the [EventFilter matching syntax](/documentation/guides/event-filter)
```ts example.ts
filter: {
header: {
"content-type": [{ $startsWith: "application/json" }],
},
},
```
</ResponseField>
<ResponseField name="body" type="EventFilter">
An [EventFilter](/documentation/guides/event-filter) object to match against the response body. This will only be applied if the response body is JSON.
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="requestTimeout" type="object">
An optional object to specify a timeout for each individual request.
<Expandable title="options" defaultOpen>
<ResponseField name="durationInMs" type="number" required>
The timeout in milliseconds before aborting the request.
</ResponseField>
<ResponseField name="retry" type="RetryOptions">
<Expandable title="options">
{" "}
<ResponseField name="limit" type="number">
The maximum number of times to retry the request.
</ResponseField>
<ResponseField name="minTimeoutInMs" type="number">
The minimum amount of time to wait before retrying the request.
</ResponseField>
<ResponseField name="maxTimeoutInMs" type="number">
The maximum amount of time to wait before retrying the request.
</ResponseField>
<ResponseField name="factor" type="number">
The exponential factor to use when calculating the next retry time.
</ResponseField>
<ResponseField name="randomize" type="boolean">
Whether to randomize the retry time.
</ResponseField>
</Expandable>
</ResponseField>
</Expandable>
</ResponseField>
## Returns
A `Promise` that resolves with the JSON response body of the matching background fetch request. You can specify the type of the response body as a generic parameter.
<RequestExample>
```ts polling
client.defineJob({
id: "background-poll-job",
name: "Background Poll Job",
version: "0.0.1",
trigger: invokeTrigger({
schema: z.object({ url: z.string().url() }),
}),
run: async (payload, io, ctx) => {
const result = await io.backgroundPoll<{ foo: string }>("poll", {
url: payload.url,
interval: 10, // every 10 seconds
timeout: 300, // stop polling after 5 minutes
responseFilter: {
// stop polling once this filter matches
status: [200],
body: {
status: ["SUCCESS"],
},
},
});
},
});
```
</RequestExample>
+2 -2
View File
@@ -4,7 +4,7 @@ sidebarTitle: "backgroundFetch()"
description: "`io.backgroundFetch()` allows you to fetch data from a URL that can take longer that the serverless timeout. The actual `fetch` request is performed on the Trigger.dev platform, and the response is sent back to you. An example use case is fetching data from a slow API, like some AI endpoints."
---
This is used inside the OpenAI Integration for Tasks like `backgroundCreateChatCompletion` and `backgroundCreateCompletion`.
This is used inside the OpenAI Integration for Tasks like [Chat Completions Background Create](/integrations/apis/openai/chat#completions-backgroundcreate)
## Parameters
@@ -140,7 +140,7 @@ An individual retrying strategy can be one of two types:
## Returns
A `Promise` that resolves after the specified amount of time.
A `Promise` that resolves with the JSON response body of the background fetch request. You can specify the type of the response body as a generic parameter.
<RequestExample>
+22 -4
View File
@@ -26,20 +26,38 @@ Used to send log messages to the [Run log](/documentation/guides/viewing-runs).
`io.runTask()` allows you to run a [Task](/documentation/concepts/tasks) from inside a Job run. A Task is a resumable unit of a Run that can be retried, resumed and is logged. [Integrations](/integrations) use Tasks internally to perform their actions.
### [wait()](/sdk/io/wait)
Waits for a certain amount of time before continuing the Job. Delays works even if you're on a serverless platform with timeouts, or if your server goes down. They utilize [resumability](/documentation/concepts/resumability) to ensure that the Run can be resumed after the delay.
### [sendEvent()](/sdk/io/sendevent)
`io.sendEvent()` allows you to send an event from inside a Job run. The sent even will trigger any Jobs that are listening for that event (based on the name).
If you want to send an event from outside a run (e.g. just from your backend) you can use [client.sendEvent()](/sdk/triggerclient/instancemethods/sendevent).
### [wait()](/sdk/io/wait)
Waits for a certain amount of time before continuing the Job. Delays works even if you're on a serverless platform with timeouts, or if your server goes down. They utilize [resumability](/documentation/concepts/resumability) to ensure that the Run can be resumed after the delay.
### [waitForEvent()](/sdk/io/wait-for-event)
`io.waitForEvent()` allows you to pause the execution of a run until an event is received, and receive the event data.
### [waitForRequest()](/sdk/io/wait-for-request)
`io.waitForRequest()` allows you to pause the execution of a run until the provided URL is requested, and receive the request data.
### [sendEvents()](/sdk/io/sendevents)
`io.sendEvents()` allows you to send multiple events from inside a Job run. The sent events will trigger any Jobs that are listening for those events (based on the name).
If you want to send multiple events from outside a run (e.g. just from your backend) you can use [client.sendEvents()](/sdk/triggerclient/instancemethods/sendevents).
### [backgroundFetch()](/sdk/io/backgroundfetch)
`io.backgroundFetch()` allows you to fetch data from a URL that can take longer that the serverless timeout. The actual `fetch` request is performed on the Trigger.dev platform, and the response is sent back to you. An example use case is fetching data from a slow API, like some AI endpoints.
### [backgroundPoll()](/sdk/io/background-poll)
`io.backgroundPoll()` allows you to fetch data from a URL on an interval, in the background.
### [random()](/sdk/io/random)
`io.random()` is identical to `Math.random()` when called without options but ensures your random numbers are not regenerated on resume or retry. It will return a pseudo-random floating-point number between optional `min` (default: 0, inclusive) and `max` (default: 1, exclusive). Can optionally `round` to the nearest integer.
+3 -1
View File
@@ -1,13 +1,15 @@
---
title: "io.sendEvent()"
sidebarTitle: "sendEvent()"
description: "`io.sendEvent()` allows you to send an event from inside a Job run. The sent even will trigger any Jobs that are listening for that event (based on the name)."
description: "`io.sendEvent()` allows you to send an event from inside a Job run. The sent event will trigger any Jobs that are listening for that event (based on the name)."
---
If you want to send an event from outside a run (e.g. just from your backend) you should use [client.sendEvent()](/sdk/triggerclient/instancemethods/sendevent) instead.
Use [eventTrigger()](/sdk/eventtrigger) on a Job to listen for events.
For multiple events, use [io.sendEvents()](/sdk/io/sendevents) instead.
## Parameters
<Snippet file="stable-key-param.mdx" />
+71
View File
@@ -0,0 +1,71 @@
---
title: "io.sendEvents()"
sidebarTitle: "sendEvents()"
description: "`io.sendEvents()` allows you to send multiple events from inside a Job run. The sent events will trigger any Jobs that are listening for those events (based on the name)."
---
If you want to send multiple events from outside a run (e.g. just from your backend) you should use [client.sendEvents()](/sdk/triggerclient/instancemethods/sendevents) instead.
Use [eventTrigger()](/sdk/eventtrigger) on a Job to listen for events.
For single events, use [io.sendEvent()](/sdk/io/sendevent) instead.
## Parameters
<Snippet file="stable-key-param.mdx" />
<Snippet file="send-events-params.mdx" />
## Returns
<Snippet file="send-events-return.mdx" />
<RequestExample>
```ts Send multiple events
//this Job sends multiple events that triggers the second job
client.defineJob({
id: "job-1",
name: "First job",
version: "0.0.1",
trigger: cronTrigger({
cron: "0 9 * * *", // 9am every day (UTC)
}),
run: async (payload, io, ctx) => {
//sends "new.user" events with a userId in the payload
await io.sendEvents("send-events", [
{
name: "new.user",
payload: {
userId: "u_12345",
},
},
{
name: "new.user",
payload: {
userId: "u_67890",
},
},
]);
},
});
client.defineJob({
id: "job-2",
name: "Second job",
version: "0.0.1",
//subscribes to the "new.user" event
trigger: eventTrigger({
name: "new.user",
schema: z.object({
userId: z.string(),
}),
}),
run: async (payload, io, ctx) => {
await io.logger.log("New user created", { userId: payload.userId });
//do stuff with the new user
},
});
```
</RequestExample>
+105
View File
@@ -0,0 +1,105 @@
---
title: "io.waitForEvent()"
sidebarTitle: "waitForEvent()"
description: "`io.waitForEvent()` waits for the next event to be emitted, and returns the event data"
---
<Warning>This feature is in beta and has not yet been deployed to the Trigger.dev cloud</Warning>
## Parameters
<Snippet file="stable-key-param.mdx" />
<ResponseField name="event" type="object" required>
Specify the options for the event to wait for.
{" "}
<Expandable title="fields" defaultOpen>
<ResponseField name="name" type="string | string[]" required>
The name(s) of the event to wait for.
</ResponseField>
<ResponseField name="schema" type="ZodTypeAny">
An optional Zod schema to validate the event payload against. If omitted the `event.payload`
will be typed as `any`
</ResponseField>
<ResponseField name="filter" type="EventFilter">
An [EventFilter](/documentation/guides/event-filter) to match against the event payload.
</ResponseField>
<ResponseField name="source" type="string">
The source of the event to wait for. If omitted, the event can come from any source.
</ResponseField>
<ResponseField name="contextFilter" type="EventFilter">
An [EventFilter](/documentation/guides/event-filter) to match against the event context.
</ResponseField>
<ResponseField name="accountId" type="string">
The account ID of the event to wait for. If omitted, the event can come from any account.
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="options" type="object">
Specify the options for the event to wait for.
{" "}
<Expandable title="options" defaultOpen>
<ResponseField name="timeoutInSeconds" type="number">
The amount of time to wait for the event to be emitted before timing out. The default timeout is
1 hour and the maximum timeout is 1 year. If the timeout is reached, the task will fail with an
error message and the run will be exited.
</ResponseField>
</Expandable>
</ResponseField>
## Returns
<ResponseField name="id" type="string" required>
The ID of the event that was emitted.
</ResponseField>
<ResponseField name="name" type="string" required>
The name of the event that was emitted.
</ResponseField>
<ResponseField name="payload" type="any" required>
The payload of the event that was emitted.
</ResponseField>
<ResponseField name="context" type="any">
The context of the event that was emitted.
</ResponseField>
<ResponseField name="timestamp" type="Date" required>
The timestamp of the event that was emitted.
</ResponseField>
<ResponseField name="accountId" type="string">
The account ID of the event that was emitted.
</ResponseField>
<RequestExample>
```ts example.ts
const event = await io.waitForEvent(
"wait",
{
name: "my.event",
schema: z.object({
id: z.string(),
createdAt: z.coerce.date(),
isAdmin: z.boolean(),
}),
filter: {
isAdmin: [true], // Only wait for events where isAdmin is true
},
},
{
timeoutInSeconds: 60 * 60, // Wait for up to an hour
}
);
```
</RequestExample>
+75
View File
@@ -0,0 +1,75 @@
---
title: "io.waitForRequest()"
sidebarTitle: "waitForRequest()"
description: "`io.waitForRequest()` waits for a request to be made to the provided URL."
---
## Parameters
<Snippet file="stable-key-param.mdx" />
<ResponseField name="callback" type="function" required>
A callback function that is called with a single `url` parameter. When the URL is POSTed to, the
task will be completed and the POST request body will be returned.
</ResponseField>
<ResponseField name="options" type="object">
<Expandable title="fields" defaultOpen>
<ResponseField name="timeoutInSeconds" type="number">
The amount of time to wait for the request to be made before timing out. Defaults to 1 hour.
</ResponseField>
</Expandable>
</ResponseField>
## Returns
Returns a `Promise` that resolves to the request body when the request is made.
<RequestExample>
```ts example.ts
type ScreenshotResponse = {
store: {
location: string;
}
}
client.defineJob({
id: "screenshot-one-example",
name: "Screenshot One Example",
version: "1.0.0",
trigger: invokeTrigger({
schema: z.object({
url: z.string().url().default("https://trigger.dev"),
}),
}),
run: async (payload, io, ctx) => {
const result = await io.waitForRequest<ScreenshotResponse>(
"screenshot-one",
async (url) => {
await fetch(`https://api.screenshotone.com/take`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
access_key: process.env.SCREENSHOT_ONE_API_KEY,
url: payload.url,
store: "true",
storage_path: "my-screeshots",
response_type: "json",
async: "true",
webhook_url: url, // this is the URL that will be called when the screenshot is ready
storage_return_location: "true",
}),
});
},
{
timeoutInSeconds: 300,
}
);
},
});
```
</RequestExample>
@@ -8,6 +8,8 @@ You can call this function from anywhere in your backend to send an event. The o
Use [eventTrigger()](/sdk/eventtrigger) on a Job to listen for events.
For multiple events, use [client.sendEvents()](/sdk/triggerclient/instancemethods/sendevents) instead.
## Parameters
<Snippet file="send-event-params.mdx" />
@@ -0,0 +1,83 @@
---
title: "TriggerClient: sendEvents() instance method"
sidebarTitle: "sendEvents()"
description: "The `sendEvents()` instance method send multiple events that triggers any Jobs that are listening for those events (based on the name)."
---
You can call this function from anywhere in your backend to send multiple events. The other way to send multiple events is by using [io.sendEvents()](/sdk/io/sendevents) from inside a `run()` function.
Use [eventTrigger()](/sdk/eventtrigger) on a Job to listen for events.
For single events, use [client.sendEvent()](/sdk/triggerclient/instancemethods/sendevent) instead.
## Parameters
<Snippet file="send-events-params.mdx" />
## Returns
<Snippet file="send-events-return.mdx" />
<RequestExample>
```ts Simple example with payloads
const event = client.sendEvents([
{
name: "new.user",
payload: {
userId: "u_12345",
},
},
{
name: "new.user",
payload: {
userId: "u_67890",
},
},
]);
```
```ts Send multiple events with an ID
const event = client.sendEvents([
{
id: "e_12345", // You can use this to deduplicate events
name: "new.user",
payload: {
userId: "u_12345",
},
},
{
id: "e_67890", // You can use this to deduplicate events
name: "new.user",
payload: {
userId: "u_67890",
},
},
]);
```
```ts Send multiple events to be delivered later
const event = client.sendEvents(
[
{
id: "e_12345", // You can use this to deduplicate events
name: "new.user",
payload: {
userId: "u_12345",
},
},
{
id: "e_67890", // You can use this to deduplicate events
name: "new.user",
payload: {
userId: "u_67890",
},
},
],
{
deliverAt: new Date("2023-12-01T00:00:00.000Z"),
}
);
```
</RequestExample>
+6
View File
@@ -38,6 +38,12 @@ Sending an event triggers any Jobs that are listening for that event (based on t
You can call this function from anywhere in your code to send an event. The other way to send an event is by using [io.sendEvent()](/sdk/io) from inside a `run()` function.
#### [sendEvents()](/sdk/triggerclient/instancemethods/sendevents)
Sending multiple events triggers any Jobs that are listening for those events (based on the name). Use [eventTrigger()](/sdk/eventtrigger) on a Job to listen for events.
You can call this function from anywhere in your code to send multiple events. The other way to send multiple events is by using [io.sendEvents()](/sdk/io) from inside a `run()` function.
#### [getEvent()](/sdk/triggerclient/instancemethods/getevent)
The `getEvent()` method gets the event details for a given eventId.
+11
View File
@@ -1,5 +1,16 @@
# @trigger.dev/airtable
## 2.2.6
### Patch Changes
- Updated dependencies [cb1825bf]
- Updated dependencies [cb1825bf]
- Updated dependencies [d0217344]
- Updated dependencies [cb1825bf]
- @trigger.dev/integration-kit@2.2.6
- @trigger.dev/sdk@2.2.6
## 2.2.5
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/airtable",
"version": "2.2.5",
"version": "2.2.6",
"description": "Trigger.dev integration for airtable",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.2.5",
"@trigger.dev/sdk": "workspace:^2.2.5",
"@trigger.dev/integration-kit": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.6",
"airtable": "^0.12.1",
"zod": "3.22.3"
},
+11
View File
@@ -1,5 +1,16 @@
# @trigger.dev/github
## 2.2.6
### Patch Changes
- Updated dependencies [cb1825bf]
- Updated dependencies [cb1825bf]
- Updated dependencies [d0217344]
- Updated dependencies [cb1825bf]
- @trigger.dev/integration-kit@2.2.6
- @trigger.dev/sdk@2.2.6
## 2.2.5
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/github",
"version": "2.2.5",
"version": "2.2.6",
"description": "The official GitHub integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -29,8 +29,8 @@
"@octokit/request": "^6.2.5",
"@octokit/request-error": "^4.0.1",
"@octokit/webhooks": "^10.4.0",
"@trigger.dev/integration-kit": "workspace:^2.2.5",
"@trigger.dev/sdk": "workspace:^2.2.5",
"@trigger.dev/integration-kit": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.6",
"octokit": "^2.0.14",
"zod": "3.22.3"
},
+11
View File
@@ -1,5 +1,16 @@
# @trigger.dev/linear
## 2.2.6
### Patch Changes
- Updated dependencies [cb1825bf]
- Updated dependencies [cb1825bf]
- Updated dependencies [d0217344]
- Updated dependencies [cb1825bf]
- @trigger.dev/integration-kit@2.2.6
- @trigger.dev/sdk@2.2.6
## 2.2.5
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/linear",
"version": "2.2.5",
"version": "2.2.6",
"description": "Trigger.dev integration for @linear/sdk",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -27,8 +27,8 @@
},
"dependencies": {
"@linear/sdk": "^8.0.0",
"@trigger.dev/integration-kit": "workspace:^2.2.5",
"@trigger.dev/sdk": "workspace:^2.2.5",
"@trigger.dev/integration-kit": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.6",
"zod": "3.22.3"
},
"engines": {
+13
View File
@@ -1,5 +1,18 @@
# @trigger.dev/slack
## 2.2.6
### Patch Changes
- cb1825bf: OpenAI support for 4.16.0
- cb1825bf: Add support for background polling and use that in OpenAI integration to power assistants
- Updated dependencies [cb1825bf]
- Updated dependencies [cb1825bf]
- Updated dependencies [d0217344]
- Updated dependencies [cb1825bf]
- @trigger.dev/integration-kit@2.2.6
- @trigger.dev/sdk@2.2.6
## 2.2.5
### Patch Changes
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
moduleFileExtensions: ["ts", "tsx", "js"],
transform: {
"^.+\\.(ts|tsx)$": "ts-jest",
},
testMatch: ["<rootDir>/test/**/*.ts?(x)", "<rootDir>/test/**/?(*.)+(spec|test).ts?(x)"],
testEnvironment: "node",
};
+10 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/openai",
"version": "2.2.5",
"version": "2.2.6",
"description": "The official OpenAI integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -17,18 +17,22 @@
"@types/node": "18",
"rimraf": "^3.0.2",
"tsup": "^6.5.0",
"typescript": "^4.9.4"
"typescript": "^4.9.4",
"@types/jest": "^29.5.3",
"jest": "^29.6.2",
"ts-jest": "^29.1.1"
},
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && npm run build:tsup",
"build:tsup": "tsup",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"test": "jest"
},
"dependencies": {
"openai": "^4.13.0",
"@trigger.dev/sdk": "workspace:^2.2.5",
"@trigger.dev/integration-kit": "workspace:^2.2.5"
"openai": "^4.16.1",
"@trigger.dev/sdk": "workspace:^2.2.6",
"@trigger.dev/integration-kit": "workspace:^2.2.6"
},
"engines": {
"node": ">=18.0.0"
+57
View File
@@ -0,0 +1,57 @@
import { IntegrationTaskKey, Prettify } from "@trigger.dev/sdk";
import { OpenAIRunTask } from "./index";
import { OpenAIIntegrationOptions, OpenAIRequestOptions } from "./types";
import OpenAI from "openai";
import { createTaskOutputProperties, handleOpenAIError } from "./taskUtils";
export class Assistants {
constructor(
private runTask: OpenAIRunTask,
private options: OpenAIIntegrationOptions
) {}
async create(
key: IntegrationTaskKey,
params: Prettify<OpenAI.Beta.AssistantCreateParams>,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Assistant> {
return this.runTask(
key,
async (client, task) => {
const { data, response } = await client.beta.assistants
.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
const outputProperties = createTaskOutputProperties(undefined, response.headers);
task.outputProperties = [
...(outputProperties ?? []),
{
label: "assistantId",
text: data.id,
},
];
return data;
},
{
name: "Create Assistant",
params,
properties: [
{
label: "model",
text: params.model,
},
...(params.name ? [{ label: "name", text: params.name }] : []),
...(params.file_ids && params.file_ids.length > 0
? [{ label: "files", text: params.file_ids.join(", ") }]
: []),
],
},
handleOpenAIError
);
}
}
+19
View File
@@ -0,0 +1,19 @@
import { Assistants } from "./assistants";
import { OpenAIRunTask } from "./index";
import { Threads } from "./threads";
import { OpenAIIntegrationOptions } from "./types";
export class Beta {
constructor(
private runTask: OpenAIRunTask,
private options: OpenAIIntegrationOptions
) {}
get assistants() {
return new Assistants(this.runTask.bind(this), this.options);
}
get threads() {
return new Threads(this.runTask.bind(this), this.options);
}
}
+24 -13
View File
@@ -5,7 +5,8 @@ import {
backgroundTaskRetries,
createBackgroundFetchHeaders,
createBackgroundFetchUrl,
createTaskUsageProperties,
createTaskOutputProperties,
handleOpenAIError,
} from "./taskUtils";
import { OpenAIIntegrationOptions, OpenAIRequestOptions } from "./types";
import { FetchRetryOptions, FetchTimeoutOptions } from "@trigger.dev/integration-kit";
@@ -25,12 +26,16 @@ export class Chat {
return this.runTask(
key,
async (client, task) => {
const response = await client.chat.completions.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
});
task.outputProperties = createTaskUsageProperties(response.usage);
return response;
const { data, response } = await client.chat.completions
.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(data.usage, response.headers);
return data;
},
{
name: "Chat Completion",
@@ -41,7 +46,8 @@ export class Chat {
text: params.model,
},
],
}
},
handleOpenAIError
);
},
@@ -61,7 +67,7 @@ export class Chat {
options
);
const response = await io.backgroundFetch<OpenAI.Chat.ChatCompletion>(
const response = await io.backgroundFetchResponse<OpenAI.Chat.ChatCompletion>(
"background",
url,
{
@@ -74,13 +80,18 @@ export class Chat {
),
body: JSON.stringify(params),
},
fetchOptions.retries ?? backgroundTaskRetries,
fetchOptions.timeout
{
retry: fetchOptions?.retries ?? backgroundTaskRetries,
timeout: fetchOptions?.timeout,
}
);
task.outputProperties = createTaskUsageProperties(response.usage);
task.outputProperties = createTaskOutputProperties(
response.data.usage,
new Headers(response.headers)
);
return response;
return response.data;
},
{
name: "Background Chat Completion",
+24 -13
View File
@@ -5,7 +5,8 @@ import {
backgroundTaskRetries,
createBackgroundFetchHeaders,
createBackgroundFetchUrl,
createTaskUsageProperties,
createTaskOutputProperties,
handleOpenAIError,
} from "./taskUtils";
import { OpenAIIntegrationOptions, OpenAIRequestOptions } from "./types";
import { FetchRetryOptions, FetchTimeoutOptions } from "@trigger.dev/integration-kit";
@@ -24,12 +25,16 @@ export class Completions {
return this.runTask(
key,
async (client, task) => {
const response = await client.completions.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
});
task.outputProperties = createTaskUsageProperties(response.usage);
return response;
const { data, response } = await client.completions
.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(data.usage, response.headers);
return data;
},
{
name: "Completion",
@@ -40,7 +45,8 @@ export class Completions {
text: params.model,
},
],
}
},
handleOpenAIError
);
}
@@ -60,7 +66,7 @@ export class Completions {
options
);
const response = await io.backgroundFetch<OpenAI.Completion>(
const response = await io.backgroundFetchResponse<OpenAI.Completion>(
"background",
url,
{
@@ -73,13 +79,18 @@ export class Completions {
),
body: JSON.stringify(params),
},
fetchOptions.retries ?? backgroundTaskRetries,
fetchOptions.timeout
{
retry: fetchOptions?.retries ?? backgroundTaskRetries,
timeout: fetchOptions?.timeout,
}
);
task.outputProperties = createTaskUsageProperties(response.usage);
task.outputProperties = createTaskOutputProperties(
response.data.usage,
new Headers(response.headers)
);
return response;
return response.data;
},
{
name: "Background Completion",
+11 -8
View File
@@ -2,7 +2,7 @@ import { truncate } from "@trigger.dev/integration-kit";
import { IntegrationTaskKey, Prettify } from "@trigger.dev/sdk";
import OpenAI from "openai";
import { OpenAIRunTask } from "./index";
import { createTaskUsageProperties } from "./taskUtils";
import { createTaskOutputProperties, handleOpenAIError } from "./taskUtils";
import { OpenAIRequestOptions } from "./types";
export class Edits {
@@ -42,18 +42,21 @@ export class Edits {
return this.runTask(
key,
async (client, task) => {
const response = await client.edits.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
});
task.outputProperties = createTaskUsageProperties(response.usage);
return response;
const { data, response } = await client.edits
.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(data.usage, response.headers);
return data;
},
{
name: "Create edit",
params,
properties,
}
},
handleOpenAIError
);
}
}
+11 -8
View File
@@ -1,7 +1,7 @@
import { IntegrationTaskKey } from "@trigger.dev/sdk";
import OpenAI from "openai";
import { OpenAIRunTask } from "./index";
import { createTaskUsageProperties } from "./taskUtils";
import { createTaskOutputProperties, handleOpenAIError } from "./taskUtils";
import { OpenAIRequestOptions } from "./types";
export class Embeddings {
@@ -19,12 +19,14 @@ export class Embeddings {
return this.runTask(
key,
async (client, task) => {
const response = await client.embeddings.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
});
task.outputProperties = createTaskUsageProperties(response.usage);
return response;
const { data, response } = await client.embeddings
.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(data.usage, response.headers);
return data;
},
{
name: "Create embedding",
@@ -35,7 +37,8 @@ export class Embeddings {
text: params.model,
},
],
}
},
handleOpenAIError
);
}
}
+168 -13
View File
@@ -2,12 +2,19 @@ import { fileFromString } from "@trigger.dev/integration-kit";
import { IntegrationTaskKey } from "@trigger.dev/sdk";
import OpenAI from "openai";
import { OpenAIRunTask } from "./index";
import { OpenAIRequestOptions } from "./types";
import { OpenAIIntegrationOptions, OpenAIRequestOptions } from "./types";
import {
createBackgroundFetchHeaders,
createBackgroundFetchUrl,
createTaskOutputProperties,
handleOpenAIError,
} from "./taskUtils";
import { Uploadable } from "openai/uploads";
type CreateFileRequest = {
file: string | File;
file: string | File | Uploadable;
fileName?: string;
purpose: string;
purpose: "fine-tune" | "assistants";
};
type CreateFineTuneFileRequest = {
@@ -19,11 +26,10 @@ type CreateFineTuneFileRequest = {
};
export class Files {
runTask: OpenAIRunTask;
constructor(runTask: OpenAIRunTask) {
this.runTask = runTask;
}
constructor(
private runTask: OpenAIRunTask,
private options: OpenAIIntegrationOptions
) {}
create(
key: IntegrationTaskKey,
@@ -33,7 +39,7 @@ export class Files {
return this.runTask(
key,
async (client, task) => {
let file: File;
let file: Uploadable;
if (typeof params.file === "string") {
file = await fileFromString(params.file, params.fileName ?? "file.txt");
@@ -59,24 +65,172 @@ export class Files {
text: typeof params.file === "string" ? "string" : "File",
},
],
}
},
handleOpenAIError
);
}
async createAndWaitForProcessing(
key: IntegrationTaskKey,
params: CreateFileRequest,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Files.FileObject> {
return this.runTask(
key,
async (client, task, io) => {
let file: Uploadable;
if (typeof params.file === "string") {
file = await fileFromString(params.file, params.fileName ?? "file.txt");
} else {
file = params.file;
}
const { data, response } = await client.files
.create(
{ file, purpose: params.purpose },
{ idempotencyKey: task.idempotencyKey, ...options }
)
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
if (["processed", "error", "deleted"].includes(data.status)) {
return data;
}
const url = createBackgroundFetchUrl(
client,
`/files/${data.id}`,
this.options.defaultQuery,
options
);
const headers = this.options.defaultHeaders ?? {};
const processedFile = await io.backgroundPoll<OpenAI.Files.FileObject>("poll", {
url,
requestInit: {
headers: createBackgroundFetchHeaders(client, task.idempotencyKey, headers, options),
},
interval: 10,
timeout: 600,
responseFilter: {
status: [200],
body: {
status: ["processed", "error", "deleted"],
},
},
});
return processedFile;
},
{
name: "Create file and wait for processing",
params,
properties: [
{
label: "Purpose",
text: params.purpose,
},
{
label: "Input type",
text: typeof params.file === "string" ? "string" : "File",
},
],
},
handleOpenAIError
);
}
async waitForProcessing(
key: IntegrationTaskKey,
id: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Files.FileObject> {
return this.runTask(
key,
async (client, task, io) => {
const url = createBackgroundFetchUrl(
client,
`/files/${id}`,
this.options.defaultQuery,
options
);
const headers = this.options.defaultHeaders ?? {};
const processedFile = await io.backgroundPoll<OpenAI.Files.FileObject>("poll", {
url,
requestInit: {
headers: createBackgroundFetchHeaders(client, task.idempotencyKey, headers, options),
},
interval: 10,
timeout: 600,
responseFilter: {
status: [200],
body: {
status: ["processed", "error", "deleted"],
},
},
});
return processedFile;
},
{
name: "Wait for processing",
properties: [
{
label: "fileId",
text: id,
},
],
},
handleOpenAIError
);
}
retrieve(
key: IntegrationTaskKey,
id: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Files.FileObject> {
return this.runTask(
key,
async (client, task) => {
const response = await client.files.retrieve(id, options);
return response;
},
{
name: "Retrieve file",
properties: [
{
label: "fileId",
text: id,
},
],
},
handleOpenAIError
);
}
list(
key: IntegrationTaskKey,
query?: OpenAI.Files.FileListParams,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Files.FileObject[]> {
return this.runTask(
key,
async (client, task) => {
const response = await client.files.list(options);
const response = await client.files.list(query, options);
return response.data;
},
{
name: "List files",
properties: [],
}
},
handleOpenAIError
);
}
@@ -107,7 +261,8 @@ export class Files {
text: params.examples.length.toString(),
},
],
}
},
handleOpenAIError
);
}
}
+21 -10
View File
@@ -2,6 +2,7 @@ import { IntegrationTaskKey } from "@trigger.dev/sdk";
import OpenAI from "openai";
import { OpenAIRunTask } from "./index";
import { OpenAIRequestOptions } from "./types";
import { handleOpenAIError } from "./taskUtils";
type SpecificFineTuneRequest = {
fineTuneId: string;
@@ -49,7 +50,8 @@ export class FineTunes {
name: "Create fine tune",
params,
properties,
}
},
handleOpenAIError
);
}
@@ -66,7 +68,8 @@ export class FineTunes {
{
name: "List fine tunes",
properties: [],
}
},
handleOpenAIError
);
}
@@ -89,7 +92,8 @@ export class FineTunes {
text: params.fineTuneId,
},
],
}
},
handleOpenAIError
);
}
@@ -115,7 +119,8 @@ export class FineTunes {
text: params.fineTuneId,
},
],
}
},
handleOpenAIError
);
}
@@ -138,7 +143,8 @@ export class FineTunes {
text: params.fineTuneId,
},
],
}
},
handleOpenAIError
);
}
@@ -189,7 +195,8 @@ export class FineTunes {
name: "Create Fine Tuning Job",
params,
properties,
}
},
handleOpenAIError
);
},
@@ -212,7 +219,8 @@ export class FineTunes {
text: params.id,
},
],
}
},
handleOpenAIError
);
},
@@ -238,7 +246,8 @@ export class FineTunes {
text: params.id,
},
],
}
},
handleOpenAIError
);
},
@@ -262,7 +271,8 @@ export class FineTunes {
text: params.id,
},
],
}
},
handleOpenAIError
);
},
@@ -281,7 +291,8 @@ export class FineTunes {
{
name: "List Fine Tuning Jobs",
params,
}
},
handleOpenAIError
);
},
};
+144 -36
View File
@@ -1,33 +1,43 @@
import { fileFromUrl } from "@trigger.dev/integration-kit";
import { FetchRetryOptions, FetchTimeoutOptions, fileFromUrl } from "@trigger.dev/integration-kit";
import { IntegrationTaskKey, Prettify } from "@trigger.dev/sdk";
import OpenAI from "openai";
import { OpenAIRunTask } from "./index";
import { OpenAIRequestOptions } from "./types";
import { OpenAIIntegrationOptions, OpenAIRequestOptions } from "./types";
import {
backgroundTaskRetries,
createBackgroundFetchHeaders,
createBackgroundFetchUrl,
createImageTaskOutputProperties,
createTaskOutputProperties,
handleOpenAIError,
} from "./taskUtils";
import { Uploadable } from "openai/uploads";
export type CreateImageEditRequest = {
image: string | File;
image: string | File | Uploadable;
prompt: string;
mask?: string | File;
mask?: string | File | Uploadable;
n?: number;
size?: "256x256" | "512x512" | "1024x1024";
response_format?: "url" | "b64_json";
user?: string;
model?: (string & {}) | "dall-e-2" | null;
};
export type CreateImageVariationRequest = {
image: string | File;
image: string | File | Uploadable;
n?: number;
size?: "256x256" | "512x512" | "1024x1024";
response_format?: "url" | "b64_json";
user?: string;
model?: (string & {}) | "dall-e-2" | null;
};
export class Images {
runTask: OpenAIRunTask;
constructor(runTask: OpenAIRunTask) {
this.runTask = runTask;
}
constructor(
private runTask: OpenAIRunTask,
private options: OpenAIIntegrationOptions
) {}
generate(
key: IntegrationTaskKey,
@@ -65,16 +75,89 @@ export class Images {
return this.runTask(
key,
async (client, task) => {
return client.images.generate(params, { idempotencyKey: task.idempotencyKey, ...options });
const { data, response } = await client.images
.generate(params, { idempotencyKey: task.idempotencyKey, ...options })
.withResponse();
task.outputProperties = createImageTaskOutputProperties(data, response.headers);
return data;
},
{
name: "Create image",
params,
properties,
},
handleOpenAIError
);
}
backgroundGenerate(
key: IntegrationTaskKey,
params: Prettify<OpenAI.Images.ImageGenerateParams>,
options: OpenAIRequestOptions = {},
fetchOptions: { retries?: FetchRetryOptions; timeout?: FetchTimeoutOptions } = {}
): Promise<OpenAI.Images.ImagesResponse> {
return this.runTask(
key,
async (client, task, io) => {
const url = createBackgroundFetchUrl(
client,
"/images/generations",
this.options.defaultQuery,
options
);
const response = await io.backgroundFetchResponse<OpenAI.Images.ImagesResponse>(
"background",
url,
{
method: options.method ?? "POST",
headers: createBackgroundFetchHeaders(
client,
task.idempotencyKey,
this.options.defaultHeaders,
options
),
body: JSON.stringify(params),
},
{
retry: fetchOptions?.retries ?? backgroundTaskRetries,
timeout: fetchOptions?.timeout,
}
);
task.outputProperties = createImageTaskOutputProperties(
response.data,
new Headers(response.headers)
);
return response.data;
},
{
name: "Background Image Generate",
params,
properties: [
{
label: "model",
text: params.model ?? "unknown",
},
],
retry: {
limit: 0,
},
}
);
}
create(...args: Parameters<Images["generate"]>) {
return this.generate(...args);
}
backgroundCreate(...args: Parameters<Images["backgroundGenerate"]>) {
return this.backgroundGenerate(...args);
}
edit(
key: IntegrationTaskKey,
params: CreateImageEditRequest,
@@ -87,6 +170,13 @@ export class Images {
text: params.prompt,
});
if (typeof params.model === "string") {
properties.push({
label: "model",
text: params.model,
});
}
if (params.n) {
properties.push({
label: "Number of images",
@@ -123,26 +213,32 @@ export class Images {
typeof params.image === "string" ? await fileFromUrl(params.image) : params.image;
const mask = typeof params.mask === "string" ? await fileFromUrl(params.mask) : params.mask;
const response = await client.images.edit(
{
image: file,
prompt: params.prompt,
mask: mask,
n: params.n,
size: params.size,
response_format: params.response_format,
user: params.user,
},
{ idempotencyKey: task.idempotencyKey, ...options }
);
const { data, response } = await client.images
.edit(
{
image: file,
prompt: params.prompt,
mask: mask,
n: params.n,
size: params.size,
response_format: params.response_format,
user: params.user,
model: params.model,
},
{ idempotencyKey: task.idempotencyKey, ...options }
)
.withResponse();
return response;
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Create image edit",
params,
properties,
}
},
handleOpenAIError
);
}
@@ -153,6 +249,13 @@ export class Images {
): Promise<OpenAI.Images.ImagesResponse> {
let properties = [];
if (typeof params.model === "string") {
properties.push({
label: "model",
text: params.model,
});
}
if (params.n) {
properties.push({
label: "Number of images",
@@ -188,18 +291,23 @@ export class Images {
const file =
typeof params.image === "string" ? await fileFromUrl(params.image) : params.image;
const response = await client.images.createVariation(
{
image: file,
n: params.n,
size: params.size,
response_format: params.response_format,
user: params.user,
},
{ idempotencyKey: task.idempotencyKey, ...options }
);
const { data, response } = await client.images
.createVariation(
{
image: file,
n: params.n,
size: params.size,
response_format: params.response_format,
user: params.user,
model: params.model,
},
{ idempotencyKey: task.idempotencyKey, ...options }
)
.withResponse();
return response;
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Create image variation",
+124 -8
View File
@@ -19,6 +19,7 @@ import { FineTunes } from "./fineTunes";
import { Images } from "./images";
import { Models } from "./models";
import { OpenAIIntegrationOptions } from "./types";
import { Beta } from "./beta";
export type OpenAIRunTask = InstanceType<typeof OpenAI>["runTask"];
@@ -103,7 +104,10 @@ export class OpenAI implements TriggerIntegration {
options?: RunTaskOptions,
errorCallback?: RunTaskErrorCallback
): Promise<TResult> {
if (!this._io) throw new Error("No IO");
if (!this._io)
throw new Error(
"Issue with running task: IO not found. It's possible that you forgot to prefix openai with io. inside a run"
);
if (!this._connectionKey) throw new Error("No connection key");
return this._io.runTask(
key,
@@ -113,7 +117,7 @@ export class OpenAI implements TriggerIntegration {
},
{
icon: this._options.icon ?? "openai",
retry: retry.standardBackoff,
retry: retry.exponentialBackoff,
...(options ?? {}),
connectionKey: this._connectionKey,
},
@@ -129,6 +133,10 @@ export class OpenAI implements TriggerIntegration {
return new Completions(this.runTask.bind(this), this._options);
}
get beta() {
return new Beta(this.runTask.bind(this), this._options);
}
get chat() {
return new Chat(this.runTask.bind(this), this._options);
}
@@ -138,7 +146,7 @@ export class OpenAI implements TriggerIntegration {
}
get images() {
return new Images(this.runTask.bind(this));
return new Images(this.runTask.bind(this), this._options);
}
get embeddings() {
@@ -146,18 +154,45 @@ export class OpenAI implements TriggerIntegration {
}
get files() {
return new Files(this.runTask.bind(this));
return new Files(this.runTask.bind(this), this._options);
}
get fineTunes() {
return this.fineTuning;
}
get fineTuning() {
return new FineTunes(this.runTask.bind(this));
}
/**
* @deprecated Please use openai.models.retrieve instead
*/
retrieveModel = this.models.retrieve;
/**
* @deprecated Please use openai.models.list instead
*/
listModels = this.models.list;
/**
* @deprecated Please use openai.models.delete instead
*/
deleteModel = this.models.delete;
/**
* @deprecated Please use openai.models.delete instead
*/
deleteFineTune = this.models.delete;
/**
* @deprecated Please use openai.completions.create instead
*/
createCompletion = this.completions.create;
/**
* @deprecated Please use openai.chat.completions.create instead
*/
createChatCompletion = this.chat.completions.create;
/**
@@ -176,19 +211,82 @@ export class OpenAI implements TriggerIntegration {
return this.chat.completions.backgroundCreate(...args);
}
/**
* @deprecated Please use openai.edits.create instead
*/
createEdit = this.edits.create;
generateImage = this.images.generate;
createImage = this.images.generate;
createImageEdit = this.images.edit;
createImageVariation = this.images.createVariation;
/**
* @deprecated Please use openai.images.generate instead
*/
async generateImage(...args: Parameters<typeof this.images.generate>) {
return this.images.generate(...args);
}
/**
* @deprecated Please use openai.images.create instead
*/
async createImage(...args: Parameters<typeof this.images.generate>) {
return this.images.generate(...args);
}
/**
* @deprecated Please use openai.images.edit instead
*/
async createImageEdit(...args: Parameters<typeof this.images.edit>) {
return this.images.edit(...args);
}
/**
* @deprecated Please use openai.images.createVariation instead
*/
async createImageVariation(...args: Parameters<typeof this.images.createVariation>) {
return this.images.createVariation(...args);
}
/**
* @deprecated Please use openai.embeddings.create instead
*/
createEmbedding = this.embeddings.create;
/**
* @deprecated Please use openai.files.create instead
*/
createFile = this.files.create;
/**
* @deprecated Please use openai.files.list instead
*/
listFiles = this.files.list;
/**
* @deprecated Please use openai.files.create instead
*/
createFineTuneFile = this.files.createFineTune;
/**
* @deprecated Please use openai.fineTuning.create instead
*/
createFineTune = this.fineTunes.create;
/**
* @deprecated Please use openai.fineTuning.list instead
*/
listFineTunes = this.fineTunes.list;
/**
* @deprecated Please use openai.fineTuning.retrieve instead
*/
retrieveFineTune = this.fineTunes.retrieve;
/**
* @deprecated Please use openai.fineTuning.cancel instead
*/
cancelFineTune = this.fineTunes.cancel;
/**
* @deprecated Please use openai.fineTuning.listEvents instead
*/
listFineTuneEvents = this.fineTunes.listEvents;
/**
@@ -198,10 +296,28 @@ export class OpenAI implements TriggerIntegration {
* of the fine-tuned models once complete.
*
* [Learn more about fine-tuning](https://platform.openai.com/docs/guides/fine-tuning)
*
* @deprecated Please use openai.fineTuning.jobs.create instead
*/
createFineTuningJob = this.fineTunes.jobs.create;
/**
* @deprecated Please use openai.fineTuning.jobs.retrieve instead
*/
retrieveFineTuningJob = this.fineTunes.jobs.retrieve;
/**
* @deprecated Please use openai.fineTuning.jobs.cancel instead
*/
cancelFineTuningJob = this.fineTunes.jobs.cancel;
/**
* @deprecated Please use openai.fineTuning.jobs.listEvents instead
*/
listFineTuningJobEvents = this.fineTunes.jobs.listEvents;
/**
* @deprecated Please use openai.fineTuning.jobs.list instead
*/
listFineTuningJobs = this.fineTunes.jobs.list;
}
+7 -3
View File
@@ -3,6 +3,7 @@ import { Model } from "openai/resources";
import { OpenAIRunTask } from "./index";
import OpenAI from "openai";
import { OpenAIRequestOptions } from "./types";
import { handleOpenAIError } from "./taskUtils";
type DeleteFineTunedModelRequest = {
fineTunedModelId: string;
@@ -33,7 +34,8 @@ export class Models {
text: params.model,
},
],
}
},
handleOpenAIError
);
}
@@ -47,7 +49,8 @@ export class Models {
{
name: "List models",
properties: [],
}
},
handleOpenAIError
);
}
@@ -70,7 +73,8 @@ export class Models {
text: params.fineTunedModelId,
},
],
}
},
handleOpenAIError
);
}
}
+191 -18
View File
@@ -1,12 +1,56 @@
import OpenAI from "openai";
import OpenAI, { APIError } from "openai";
import { OpenAIRequestOptions } from "./types";
import { redactString } from "@trigger.dev/sdk";
import { calculateResetAtUtil } from "@trigger.dev/integration-kit";
import { FetchRetryOptions } from "@trigger.dev/integration-kit";
export function createTaskUsageProperties(
export function createImageTaskOutputProperties(
response: OpenAI.ImagesResponse | undefined,
headers?: Headers | undefined
) {
if (!response && !headers) {
return;
}
return [...createTaskImageProperties(response), ...createTaskRateLimitProperties(headers)];
}
function createTaskImageProperties(response: OpenAI.ImagesResponse | undefined) {
if (!response) {
return [];
}
const imageUrls = response.data.map((image) => image.url).filter(Boolean) as string[];
if (imageUrls.length === 0) {
return [];
}
return [
{
label: "Images",
text: imageUrls[0],
imageUrl: imageUrls,
},
];
}
export function createTaskOutputProperties(
usage: OpenAI.Completions.CompletionUsage | OpenAI.CreateEmbeddingResponse.Usage | undefined,
headers?: Headers | undefined
) {
if (!usage && !headers) {
return;
}
return [...createTaskUsageProperties(usage), ...createTaskRateLimitProperties(headers)];
}
function createTaskUsageProperties(
usage: OpenAI.Completions.CompletionUsage | OpenAI.CreateEmbeddingResponse.Usage | undefined
) {
if (!usage) {
return;
return [];
}
return [
@@ -22,15 +66,117 @@ export function createTaskUsageProperties(
},
]
: []),
{
label: "Total Usage",
text: String(usage.total_tokens),
},
];
}
export function onTaskError(error: unknown) {
return;
function createTaskRateLimitProperties(headers: Headers | undefined) {
if (!headers) {
return [];
}
const remainingRequests = headers.get("x-ratelimit-remaining-requests");
const remainingTokens = headers.get("x-ratelimit-remaining-tokens");
const resetRequests = headers.get("x-ratelimit-reset-requests");
const resetTokens = headers.get("x-ratelimit-reset-tokens");
return [
...(remainingRequests
? [
{
label: "Remaining Requests",
text: remainingRequests ?? "Unknown",
},
]
: []),
...(resetRequests
? [
{
label: "Reset Requests",
text: resetRequests ?? "Unknown",
},
]
: []),
...(remainingTokens
? [
{
label: "Remaining Tokens",
text: remainingTokens ?? "Unknown",
},
]
: []),
...(resetTokens
? [
{
label: "Reset Tokens",
text: resetTokens ?? "Unknown",
},
]
: []),
];
}
export function handleOpenAIError(error: unknown) {
if (error instanceof APIError) {
const isErrorRetryable = () => {
if (typeof error.status !== "number") {
return false;
}
if (error.status === 429 && error.type === "insufficient_quota") {
return false;
}
return (
error.status === 429 ||
error.status === 408 ||
error.status === 409 ||
(error.status >= 500 && error.status <= 599)
);
};
const calculateRetryAt = () => {
if (error.status !== 429) {
return;
}
if (!error.headers) {
return;
}
const remainingRequests = error.headers["x-ratelimit-remaining-requests"];
const requestResets = error.headers["x-ratelimit-reset-requests"];
if (typeof remainingRequests === "string" && Number(remainingRequests) === 0) {
return calculateResetAt(requestResets);
}
const remainingTokens = error.headers["x-ratelimit-remaining-tokens"];
const tokensResets = error.headers["x-ratelimit-reset-tokens"];
if (typeof remainingTokens === "string" && Number(remainingTokens) === 0) {
return calculateResetAt(tokensResets);
}
};
return {
error,
skipRetrying: !isErrorRetryable(),
retryAt: calculateRetryAt(),
};
}
return error as Error;
}
// This takes a string in the format of 1s or 6m59s, 1h6m18s and calculates the date
// If the string is invalid, it returns undefined
// If the string is null or undefined, it returns undefined
export function calculateResetAt(
resets: string | null | undefined,
now: Date = new Date()
): Date | undefined {
return calculateResetAtUtil(resets, "iso_8601_duration_openai_variant", now);
}
export function createBackgroundFetchUrl(
@@ -83,29 +229,56 @@ export function createBackgroundFetchHeaders(
};
}
export const backgroundTaskRetries = {
export const backgroundTaskRetries: FetchRetryOptions = {
"500-599": {
strategy: "backoff",
limit: 5,
minTimeoutInMs: 1000,
maxTimeoutInMs: 30000,
factor: 1.8,
factor: 2,
randomize: true,
},
"429": {
strategy: "backoff",
limit: 10,
minTimeoutInMs: 1000,
maxTimeoutInMs: 60000,
factor: 2,
randomize: true,
limit: 0,
bodyFilter: {
error: {
code: ["insufficient_quota"],
},
},
},
"429,429": {
strategy: "headers",
limitHeader: "x-ratelimit-limit-requests",
remainingHeader: "x-ratelimit-remaining-requests",
resetHeader: "x-ratelimit-reset-requests",
resetFormat: "iso_8601_duration_openai_variant",
bodyFilter: {
error: {
code: ["rate_limit_exceeded"],
type: ["requests"],
},
},
},
"429,429,429": {
strategy: "headers",
limitHeader: "x-ratelimit-limit-tokens",
remainingHeader: "x-ratelimit-remaining-tokens",
resetHeader: "x-ratelimit-reset-tokens",
resetFormat: "iso_8601_duration_openai_variant",
bodyFilter: {
error: {
code: ["rate_limit_exceeded"],
type: ["tokens"],
},
},
},
"408-409": {
strategy: "backoff",
limit: 3,
limit: 5,
minTimeoutInMs: 1000,
maxTimeoutInMs: 60000,
factor: 2,
randomize: true,
},
} as const;
};
+691
View File
@@ -0,0 +1,691 @@
import { IntegrationTaskKey, Prettify } from "@trigger.dev/sdk";
import { OpenAIRunTask } from "./index";
import { OpenAIIntegrationOptions, OpenAIRequestOptions } from "./types";
import OpenAI from "openai";
import {
createBackgroundFetchHeaders,
createBackgroundFetchUrl,
createTaskOutputProperties,
handleOpenAIError,
} from "./taskUtils";
import { RunSubmitToolOutputsParams } from "openai/resources/beta/threads/runs/runs";
import { ThreadUpdateParams } from "openai/resources/beta/threads/threads";
export class Threads {
constructor(
private runTask: OpenAIRunTask,
private options: OpenAIIntegrationOptions
) {}
/**
* Create a thread and run it in one task.
*/
async createAndRun(
key: IntegrationTaskKey,
params: Prettify<OpenAI.Beta.ThreadCreateAndRunParams>,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run> {
return this.runTask(
key,
async (client, task) => {
const { data, response } = await client.beta.threads
.createAndRun(params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
const outputProperties = [
...(createTaskOutputProperties(undefined, response.headers) ?? []),
{
label: "threadId",
text: data.thread_id,
},
{
label: "runId",
text: data.id,
},
];
task.outputProperties = outputProperties;
return data;
},
{
name: "Create Thread and Run",
params,
},
handleOpenAIError
);
}
/**
* Create a thread and runs it in one task, and only returns when the run is completed by polling in the background.
*/
async createAndRunUntilCompletion(
key: IntegrationTaskKey,
params: Prettify<OpenAI.Beta.ThreadCreateAndRunParams>,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads
.createAndRun(params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
const outputProperties = [
...(createTaskOutputProperties(undefined, response.headers) ?? []),
{
label: "threadId",
text: data.thread_id,
},
{
label: "runId",
text: data.id,
},
];
task.outputProperties = outputProperties;
const url = createBackgroundFetchUrl(
client,
`/threads/${data.thread_id}/runs/${data.id}`,
this.options.defaultQuery,
options
);
const headers = this.options.defaultHeaders ?? {};
headers["OpenAI-Beta"] = "assistants=v1";
const completedRun = await io.backgroundPoll<OpenAI.Beta.Threads.Run>("poll", {
url,
requestInit: {
headers: createBackgroundFetchHeaders(client, task.idempotencyKey, headers, options),
},
interval: 10,
timeout: 600,
responseFilter: {
status: [200],
body: {
status: ["completed", "expired", "cancelled", "failed"],
},
},
});
return completedRun;
},
{
name: "Run Created Thread and Wait for Completion",
params,
},
handleOpenAIError
);
}
/**
* Create a thread.
*/
async create(
key: IntegrationTaskKey,
params: Prettify<OpenAI.Beta.ThreadCreateParams> = {},
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Thread> {
return this.runTask(
key,
async (client, task) => {
const { data, response } = await client.beta.threads
.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Create Thread",
params,
},
handleOpenAIError
);
}
/**
* Retrieves a thread.
*/
async retrieve(
key: IntegrationTaskKey,
threadId: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Thread> {
return this.runTask(
key,
async (client, task) => {
const { data, response } = await client.beta.threads
.retrieve(threadId, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Retrieve Thread",
properties: [{ label: "threadId", text: threadId }],
},
handleOpenAIError
);
}
/**
* Modifies a thread.
*/
async update(
key: IntegrationTaskKey,
threadId: string,
body: ThreadUpdateParams,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Thread> {
return this.runTask(
key,
async (client, task) => {
const { data, response } = await client.beta.threads
.update(threadId, body, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Update Thread",
properties: [{ label: "threadId", text: threadId }],
},
handleOpenAIError
);
}
/**
* Delete a thread.
*/
async del(
key: IntegrationTaskKey,
threadId: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.ThreadDeleted> {
return this.runTask(
key,
async (client, task) => {
const { data, response } = await client.beta.threads
.del(threadId, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Delete Thread",
properties: [{ label: "threadId", text: threadId }],
},
handleOpenAIError
);
}
get runs() {
return new Runs(this.runTask.bind(this), this.options);
}
get messages() {
return new Messages(this.runTask.bind(this), this.options);
}
}
class Runs {
constructor(
private runTask: OpenAIRunTask,
private options: OpenAIIntegrationOptions
) {}
/**
* Creates a run and waits for it to complete by polling in the background.
*/
async createAndWaitForCompletion(
key: IntegrationTaskKey,
threadId: string,
params: Prettify<OpenAI.Beta.Threads.RunCreateParams>,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads.runs
.create(threadId, params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
const url = createBackgroundFetchUrl(
client,
`/threads/${threadId}/runs/${data.id}`,
this.options.defaultQuery,
options
);
const headers = this.options.defaultHeaders ?? {};
headers["OpenAI-Beta"] = "assistants=v1";
const completedRun = await io.backgroundPoll<OpenAI.Beta.Threads.Run>("poll", {
url,
requestInit: {
headers: createBackgroundFetchHeaders(client, task.idempotencyKey, headers, options),
},
interval: 10,
timeout: 600,
responseFilter: {
status: [200],
body: {
status: ["completed", "expired", "cancelled", "failed"],
},
},
});
return completedRun;
},
{
name: "Run Thread and Wait for Completion",
params,
properties: [{ label: "threadId", text: threadId }],
},
handleOpenAIError
);
}
/**
* Waits for a run to complete by polling in the background.
*/
async waitForCompletion(
key: IntegrationTaskKey,
threadId: string,
runId: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run> {
return this.runTask(
key,
async (client, task, io) => {
const url = createBackgroundFetchUrl(
client,
`/threads/${threadId}/runs/${runId}`,
this.options.defaultQuery,
options
);
const headers = this.options.defaultHeaders ?? {};
headers["OpenAI-Beta"] = "assistants=v1";
const completedRun = await io.backgroundPoll<OpenAI.Beta.Threads.Run>("poll", {
url,
requestInit: {
headers: createBackgroundFetchHeaders(client, task.idempotencyKey, headers, options),
},
interval: 10,
timeout: 600,
responseFilter: {
status: [200],
body: {
status: ["completed", "expired", "cancelled", "failed"],
},
},
});
return completedRun;
},
{
name: "Wait for Run Completion",
properties: [
{ label: "threadId", text: threadId },
{ label: "runId", text: runId },
],
},
handleOpenAIError
);
}
/**
* Creates a run.
*/
async create(
key: IntegrationTaskKey,
threadId: string,
params: Prettify<OpenAI.Beta.Threads.RunCreateParams>,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads.runs
.create(threadId, params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Run Thread",
params,
properties: [{ label: "threadId", text: threadId }],
},
handleOpenAIError
);
}
/**
* Retrieves a run.
*/
async retrieve(
key: IntegrationTaskKey,
threadId: string,
runId: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads.runs
.retrieve(threadId, runId, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Retrieve Run",
properties: [
{ label: "threadId", text: threadId },
{ label: "runId", text: runId },
],
},
handleOpenAIError
);
}
/**
* Cancels a run that is `in_progress`.
*/
async cancel(
key: IntegrationTaskKey,
threadId: string,
runId: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads.runs
.cancel(threadId, runId, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Cancel Run",
properties: [
{ label: "threadId", text: threadId },
{ label: "runId", text: runId },
],
},
handleOpenAIError
);
}
/**
* When a run has the `status: "requires_action"` and `required_action.type` is
* `submit_tool_outputs`, this endpoint can be used to submit the outputs from the
* tool calls once they're all completed. All outputs must be submitted in a single
* request.
*/
async submitToolOutputs(
key: IntegrationTaskKey,
threadId: string,
runId: string,
body: RunSubmitToolOutputsParams,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads.runs
.submitToolOutputs(threadId, runId, body, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Submit Tool Outputs",
properties: [
{ label: "threadId", text: threadId },
{ label: "runId", text: runId },
],
},
handleOpenAIError
);
}
/**
* Returns all runs belonging to a thread.
*/
async list(
key: IntegrationTaskKey,
threadId: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run[]> {
return this.runTask(
key,
async (client, task, io) => {
const { data: page, response } = await client.beta.threads.runs
.list(threadId, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
const allRuns = [];
for await (const fineTuningJob of page) {
allRuns.push(fineTuningJob);
}
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return allRuns;
},
{
name: "List Runs",
properties: [{ label: "threadId", text: threadId }],
},
handleOpenAIError
);
}
}
class Messages {
constructor(
private runTask: OpenAIRunTask,
private options: OpenAIIntegrationOptions
) {}
/**
* Returns all messages for a given thread.
*/
async list(
key: IntegrationTaskKey,
threadId: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.ThreadMessage[]> {
return this.runTask(
key,
async (client, task, io) => {
const { data: page, response } = await client.beta.threads.messages
.list(threadId, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
const allMessages = [];
for await (const fineTuningJob of page) {
allMessages.push(fineTuningJob);
}
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return allMessages;
},
{
name: "List Messages",
properties: [{ label: "threadId", text: threadId }],
},
handleOpenAIError
);
}
/**
* Create a message.
*/
async create(
key: IntegrationTaskKey,
threadId: string,
body: Prettify<OpenAI.Beta.Threads.MessageCreateParams>,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.ThreadMessage> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads.messages
.create(threadId, body, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Create Message",
properties: [{ label: "threadId", text: threadId }],
},
handleOpenAIError
);
}
/**
* Retrieve a message.
*/
async retrieve(
key: IntegrationTaskKey,
threadId: string,
messageId: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.ThreadMessage> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads.messages
.retrieve(threadId, messageId, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Retrieve Message",
properties: [
{ label: "threadId", text: threadId },
{ label: "messageId", text: messageId },
],
},
handleOpenAIError
);
}
/**
* Modifies a message.
*/
async update(
key: IntegrationTaskKey,
threadId: string,
messageId: string,
body: Prettify<OpenAI.Beta.Threads.MessageUpdateParams>,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.ThreadMessage> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads.messages
.update(threadId, messageId, body, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Update Message",
properties: [
{ label: "threadId", text: threadId },
{ label: "messageId", text: messageId },
],
},
handleOpenAIError
);
}
}
@@ -0,0 +1,18 @@
import { calculateResetAt } from "../src/taskUtils";
describe("calculateResetAt", () => {
it("Should be able to correctly calculate based on a variety of formats", () => {
const now = new Date("2023-01-01T00:00:00.000Z");
expect(calculateResetAt("1s", now)).toEqual(new Date("2023-01-01T00:00:01.000Z"));
expect(calculateResetAt("6m59s", now)).toEqual(new Date("2023-01-01T00:06:59.000Z"));
expect(calculateResetAt("5m48s", now)).toEqual(new Date("2023-01-01T00:05:48.000Z"));
expect(calculateResetAt("1h44m5s", now)).toEqual(new Date("2023-01-01T01:44:05.000Z"));
expect(calculateResetAt("1h2s", now)).toEqual(new Date("2023-01-01T01:00:02.000Z"));
expect(calculateResetAt("45m", now)).toEqual(new Date("2023-01-01T00:45:00.000Z"));
expect(calculateResetAt("23h59m0s", now)).toEqual(new Date("2023-01-01T23:59:00.000Z"));
expect(calculateResetAt("1d22h8m1s", now)).toEqual(new Date("2023-01-02T22:08:01.000Z"));
expect(calculateResetAt("3h36m7.312s", now)).toEqual(new Date("2023-01-01T03:36:07.312Z"));
expect(calculateResetAt("72ms", now)).toEqual(new Date("2023-01-01T00:00:00.072Z"));
});
});
+18
View File
@@ -0,0 +1,18 @@
{
"extends": "@trigger.dev/tsconfig/node18.json",
"include": ["./src/**/*.ts", "tsup.config.ts"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2019"],
"paths": {
"@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"],
"@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"],
"@trigger.dev/integration-kit/*": ["../../packages/integration-kit/src/*"],
"@trigger.dev/integration-kit": ["../../packages/integration-kit/src/index"]
},
"declaration": false,
"declarationMap": false,
"baseUrl": ".",
"stripInternal": true
},
"exclude": ["node_modules"]
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"extends": "@trigger.dev/tsconfig/node18.json",
"include": ["./src/**/*.ts", "tsup.config.ts"],
"include": ["./src/**/*.ts", "tsup.config.ts", "src/globals.d.ts", "./test/**/*.ts"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2019"],
"paths": {
+11
View File
@@ -1,5 +1,16 @@
# @trigger.dev/plain
## 2.2.6
### Patch Changes
- Updated dependencies [cb1825bf]
- Updated dependencies [cb1825bf]
- Updated dependencies [d0217344]
- Updated dependencies [cb1825bf]
- @trigger.dev/integration-kit@2.2.6
- @trigger.dev/sdk@2.2.6
## 2.2.5
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/plain",
"version": "2.2.5",
"version": "2.2.6",
"description": "The official Plain.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"build:tsup": "tsup"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.2.5",
"@trigger.dev/sdk": "workspace:^2.2.5",
"@trigger.dev/integration-kit": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.6",
"@team-plain/typescript-sdk": "^2.7.0"
},
"engines": {
+11
View File
@@ -1,5 +1,16 @@
# @trigger.dev/replicate
## 2.2.6
### Patch Changes
- Updated dependencies [cb1825bf]
- Updated dependencies [cb1825bf]
- Updated dependencies [d0217344]
- Updated dependencies [cb1825bf]
- @trigger.dev/integration-kit@2.2.6
- @trigger.dev/sdk@2.2.6
## 2.2.5
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/replicate",
"version": "2.2.5",
"version": "2.2.6",
"description": "Trigger.dev integration for replicate",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.2.5",
"@trigger.dev/sdk": "workspace:^2.2.5",
"@trigger.dev/integration-kit": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.6",
"replicate": "^0.18.1",
"zod": "3.22.3"
},
+11
View File
@@ -1,5 +1,16 @@
# @trigger.dev/resend
## 2.2.6
### Patch Changes
- Updated dependencies [cb1825bf]
- Updated dependencies [cb1825bf]
- Updated dependencies [d0217344]
- Updated dependencies [cb1825bf]
- @trigger.dev/integration-kit@2.2.6
- @trigger.dev/sdk@2.2.6
## 2.2.5
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/resend",
"version": "2.2.5",
"version": "2.2.6",
"description": "The official Resend.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"build:tsup": "tsup"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.2.5",
"@trigger.dev/sdk": "workspace:^2.2.5",
"@trigger.dev/integration-kit": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.6",
"resend": "^2.0.0"
},
"engines": {
+11
View File
@@ -1,5 +1,16 @@
# @trigger.dev/sendgrid
## 2.2.6
### Patch Changes
- Updated dependencies [cb1825bf]
- Updated dependencies [cb1825bf]
- Updated dependencies [d0217344]
- Updated dependencies [cb1825bf]
- @trigger.dev/integration-kit@2.2.6
- @trigger.dev/sdk@2.2.6
## 2.2.5
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/sendgrid",
"version": "2.2.5",
"version": "2.2.6",
"description": "Trigger.dev integration for @sendgrid/mail",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -27,8 +27,8 @@
},
"dependencies": {
"@sendgrid/mail": "^7.7.0",
"@trigger.dev/sdk": "workspace:^2.2.5",
"@trigger.dev/integration-kit": "workspace:^2.2.5"
"@trigger.dev/sdk": "workspace:^2.2.6",
"@trigger.dev/integration-kit": "workspace:^2.2.6"
},
"engines": {
"node": ">=16.8.0"
+10
View File
@@ -1,5 +1,15 @@
# @trigger.dev/slack
## 2.2.6
### Patch Changes
- Updated dependencies [cb1825bf]
- Updated dependencies [cb1825bf]
- Updated dependencies [d0217344]
- Updated dependencies [cb1825bf]
- @trigger.dev/sdk@2.2.6
## 2.2.5
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/slack",
"version": "2.2.5",
"version": "2.2.6",
"description": "The official Slack integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,7 +25,7 @@
},
"dependencies": {
"@slack/web-api": "^6.8.1",
"@trigger.dev/sdk": "workspace:^2.2.5",
"@trigger.dev/sdk": "workspace:^2.2.6",
"zod": "3.22.3"
},
"engines": {
+11
View File
@@ -1,5 +1,16 @@
# @trigger.dev/stripe
## 2.2.6
### Patch Changes
- Updated dependencies [cb1825bf]
- Updated dependencies [cb1825bf]
- Updated dependencies [d0217344]
- Updated dependencies [cb1825bf]
- @trigger.dev/integration-kit@2.2.6
- @trigger.dev/sdk@2.2.6
## 2.2.5
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/stripe",
"version": "2.2.5",
"version": "2.2.6",
"description": "Trigger.dev integration for stripe",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^2.2.5",
"@trigger.dev/sdk": "workspace:^2.2.5",
"@trigger.dev/integration-kit": "workspace:^2.2.6",
"@trigger.dev/sdk": "workspace:^2.2.6",
"stripe": "^12.14.0",
"zod": "3.22.3"
},

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