Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/core/src/v3/apiClient/runStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ export class SSEStreamSubscription implements StreamSubscription {
// permanently. `404` (stream gone) and `410` (session closed)
// are sensible defaults; tune per-caller for other 4xx.
nonRetryableStatuses?: readonly number[];
// Optional fetch override. Used by transports that need to route
// the SSE connect through a custom path (proxy, custom headers,
// tracing). Defaults to global `fetch`.
fetchClient?: typeof fetch;
}
) {
this.lastEventId = options.lastEventId;
Expand Down Expand Up @@ -331,7 +335,8 @@ export class SSEStreamSubscription implements StreamSubscription {
headers["Timeout-Seconds"] = this.options.timeoutInSeconds.toString();
}

const response = await fetch(this.url, {
const fetchClient = this.options.fetchClient ?? fetch;
const response = await fetchClient(this.url, {
headers,
signal: this.internalAbort.signal,
});
Expand Down
177 changes: 167 additions & 10 deletions packages/trigger-sdk/src/v3/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
type TaskWithSchema,
SESSION_IN_EVENT_ID_HEADER,
TRIGGER_CONTROL_SUBTYPE,
generateJWT,
type WriterStreamOptions,
} from "@trigger.dev/core/v3";
import type {
Expand Down Expand Up @@ -8411,6 +8412,32 @@ export type { InferChatClientData, InferChatUIMessage } from "./ai-shared.js";
/**
* Options for {@link createChatStartSessionAction}.
*/
/**
* Discriminator for per-endpoint `baseURL` / `fetch` callbacks on
* `createChatStartSessionAction`.
*
* - `"sessions"` — `POST /api/v1/sessions` (session create + first run trigger).
* - `"auth"` — `POST /api/v1/auth/jwt/claims` (only fired when
* `tokenTTL` is set; otherwise the publicAccessToken from session create
* is reused as-is).
*/
export type ChatStartSessionEndpoint = "sessions" | "auth";

export type ChatStartSessionEndpointContext = {
endpoint: ChatStartSessionEndpoint;
chatId: string;
};

export type ChatStartSessionBaseURLResolver = (
ctx: ChatStartSessionEndpointContext
) => string;

export type ChatStartSessionFetchOverride = (
url: string,
init: RequestInit,
ctx: ChatStartSessionEndpointContext
) => Promise<Response>;

export type CreateChatStartSessionActionOptions = {
/** TTL for the session-scoped public access token. @default "1h" */
tokenTTL?: string | number | Date;
Expand All @@ -8419,6 +8446,21 @@ export type CreateChatStartSessionActionOptions = {
* Per-call `params.triggerConfig` shallow-merges on top.
*/
triggerConfig?: Partial<SessionTriggerConfig>;
/**
* Override the Trigger.dev API base URL. String applies to both
* `/api/v1/sessions` and `/api/v1/auth/jwt/claims`; function picks per
* endpoint. When unset, falls back to `apiClientManager.baseURL`
* (typically the `TRIGGER_API_URL` env var). Set this to route session
* create through a trusted edge proxy that injects server-side signal
* into `basePayload.metadata` before forwarding upstream.
*/
baseURL?: string | ChatStartSessionBaseURLResolver;
/**
* Per-request fetch override. Receives the resolved URL, RequestInit,
* and endpoint context. Use for header injection, proxy routing, or
* custom retry. Applies to both session-create and JWT-claims POSTs.
*/
fetch?: ChatStartSessionFetchOverride;
};

/**
Expand Down Expand Up @@ -8542,27 +8584,47 @@ function createChatStartSessionAction(
: {}),
};

const created = await sessions.start({
type: "chat.agent",
const startBody = {
type: "chat.agent" as const,
externalId: params.chatId,
taskIdentifier: taskId,
triggerConfig,
metadata: params.metadata,
});
};

const baseURLOption = options?.baseURL;
const fetchOverride = options?.fetch;
const hasOverride = baseURLOption !== undefined || fetchOverride !== undefined;

const created: { id: string; runId: string; publicAccessToken: string } = hasOverride
? await callSessionsCreateWithOverride({
chatId: params.chatId,
body: startBody,
baseURLOption,
fetchOverride,
})
: await sessions.start(startBody);

// Session create returns a session PAT directly when called with a
// start token, but when the SDK call goes via the secret key we still
// need to mint our own (the server returns a PAT regardless, but
// re-minting here lets the customer override `tokenTTL`).
const publicAccessToken =
options?.tokenTTL !== undefined
? await auth.createPublicToken({
scopes: {
read: { sessions: params.chatId },
write: { sessions: params.chatId },
},
expirationTime: options.tokenTTL,
})
? hasOverride
? await mintPublicTokenWithOverride({
chatId: params.chatId,
expirationTime: options.tokenTTL,
baseURLOption,
fetchOverride,
})
: await auth.createPublicToken({
scopes: {
read: { sessions: params.chatId },
write: { sessions: params.chatId },
},
expirationTime: options.tokenTTL,
})
: created.publicAccessToken;

return {
Expand All @@ -8573,6 +8635,101 @@ function createChatStartSessionAction(
};
}

function resolveChatStartBaseURL(
endpoint: ChatStartSessionEndpoint,
chatId: string,
option: string | ChatStartSessionBaseURLResolver | undefined
): string {
const fallback = apiClientManager.baseURL ?? "https://api.trigger.dev";
const raw =
typeof option === "function"
? option({ endpoint, chatId })
: option ?? fallback;
return raw.replace(/\/$/, "");
}

function overrideRequestHeaders(accessToken: string): Record<string, string> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
"x-trigger-source": "sdk",
};
// Forward the preview-branch hint so override-mode requests land on the
// same env the standard ApiClient path would have routed to. Mirrors
// ApiClient.#getHeaders. Read from TRIGGER_PREVIEW_BRANCH /
// VERCEL_GIT_COMMIT_REF via apiClientManager.branchName.
if (apiClientManager.branchName) {
headers["x-trigger-branch"] = apiClientManager.branchName;
}
return headers;
}

async function callSessionsCreateWithOverride(args: {
chatId: string;
body: { type: "chat.agent"; externalId: string; taskIdentifier: string; triggerConfig: SessionTriggerConfig; metadata?: Record<string, unknown> };
baseURLOption: string | ChatStartSessionBaseURLResolver | undefined;
fetchOverride: ChatStartSessionFetchOverride | undefined;
}): Promise<{ id: string; runId: string; publicAccessToken: string }> {
const accessToken = apiClientManager.accessToken;
if (!accessToken) {
throw new Error(
"chat.createStartSessionAction: no API access token configured. Set TRIGGER_SECRET_KEY or call apiClientManager.setGlobalAPIClientConfiguration before invoking the action."
);
}
const ctx: ChatStartSessionEndpointContext = { endpoint: "sessions", chatId: args.chatId };
const url = `${resolveChatStartBaseURL("sessions", args.chatId, args.baseURLOption)}/api/v1/sessions`;
const init: RequestInit = {
method: "POST",
headers: overrideRequestHeaders(accessToken),
body: JSON.stringify(args.body),
};
const response = args.fetchOverride
? await args.fetchOverride(url, init, ctx)
: await fetch(url, init);
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(`sessions.start failed: ${response.status} ${text}`);
}
const json = (await response.json()) as { id: string; runId: string; publicAccessToken: string };
return json;
}

async function mintPublicTokenWithOverride(args: {
chatId: string;
expirationTime: string | number | Date;
baseURLOption: string | ChatStartSessionBaseURLResolver | undefined;
fetchOverride: ChatStartSessionFetchOverride | undefined;
}): Promise<string> {
const accessToken = apiClientManager.accessToken;
if (!accessToken) {
throw new Error(
"chat.createStartSessionAction: no API access token configured for JWT mint."
);
}
const ctx: ChatStartSessionEndpointContext = { endpoint: "auth", chatId: args.chatId };
const url = `${resolveChatStartBaseURL("auth", args.chatId, args.baseURLOption)}/api/v1/auth/jwt/claims`;
const init: RequestInit = {
method: "POST",
headers: overrideRequestHeaders(accessToken),
};
const response = args.fetchOverride
? await args.fetchOverride(url, init, ctx)
: await fetch(url, init);
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(`auth.createPublicToken failed: ${response.status} ${text}`);
}
const claims = (await response.json()) as Record<string, unknown>;
return generateJWT({
secretKey: accessToken,
payload: {
...claims,
scopes: [`read:sessions:${args.chatId}`, `write:sessions:${args.chatId}`],
},
expirationTime: args.expirationTime,
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export const chat = {
/** Create a chat agent. See {@link chatAgent}. */
agent: chatAgent,
Expand Down
Loading
Loading