mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-08 21:35:36 +01:00
Settle paid operations through operation-scoped transactions. Preserve exact ledger evidence and replay while moving aggregate budget decisions to durable deferred work. Co-Authored-By: Paperclip <noreply@paperclip.ing>
1317 lines
45 KiB
TypeScript
1317 lines
45 KiB
TypeScript
import { notifyChatResponseAvailability } from "./chat-response-availability.js";
|
|
import { insertIssueComment } from "./issue-comment-writes.js";
|
|
import { randomUUID } from "node:crypto";
|
|
import { sanitizeQuarantinedCommentForHigherTrust } from "./source-trust.js";
|
|
import {
|
|
and,
|
|
desc,
|
|
eq,
|
|
gt,
|
|
gte,
|
|
inArray,
|
|
isNull,
|
|
lt,
|
|
lte,
|
|
or,
|
|
sql,
|
|
} from "drizzle-orm";
|
|
import {
|
|
agents,
|
|
toolConnections,
|
|
assets,
|
|
issueAttachments,
|
|
projects,
|
|
authUsers,
|
|
budgetReservations,
|
|
companies,
|
|
companyFastResponses,
|
|
companyMemberships,
|
|
companySecrets,
|
|
costEvents,
|
|
fastResponseRequests,
|
|
heartbeatRunEvents,
|
|
heartbeatRuns,
|
|
issueComments,
|
|
issues,
|
|
chatPublications,
|
|
type Db,
|
|
} from "@paperclipai/db";
|
|
import {
|
|
aiConnectionCatalogSlug,
|
|
aiConnectionMetadataSchema,
|
|
centsToUnits,
|
|
unitsToCents,
|
|
updateFastResponseSchema,
|
|
FAST_RESPONSE_DEADLINE_MS,
|
|
type AiProvider,
|
|
type FastResponseSettings,
|
|
type FastResponseHistoryEntry,
|
|
type UpdateFastResponse,
|
|
type FastResponseTestResult,
|
|
} from "@paperclipai/shared";
|
|
import { aiConnectionService } from "./ai-connections.js";
|
|
import { instanceSettingsService } from "./instance-settings.js";
|
|
import { isAutomaticRecoverySuppressedByPauseHold } from "./recovery/pause-hold-guard.js";
|
|
import { accessService } from "./access.js";
|
|
import type { AuthorizationActor } from "./authorization.js";
|
|
import { forbidden, unprocessable } from "../errors.js";
|
|
import {
|
|
logActivity,
|
|
publishActivity,
|
|
type ActivityPublication,
|
|
} from "./activity-log.js";
|
|
import { withAccountingTransaction, withOperationAccountingTransaction } from "./accounting-transaction.js";
|
|
import { withBudgetSpan } from "./budget-tracing.js";
|
|
import {
|
|
budgetServiceInTransaction,
|
|
type BudgetServiceHooks,
|
|
} from "./budgets.js";
|
|
import { createCostEventInTransaction } from "./costs.js";
|
|
import { applyCostCorrectionInTransaction } from "./accounting-corrections.js";
|
|
import { queueBudgetEvaluation } from "./budget-evaluation-work.js";
|
|
import {
|
|
notifyDeliveryWork,
|
|
DELIVERY_QUEUES,
|
|
} from "./delivery-work-notifications.js";
|
|
import { projectSafeChatPublication } from "./chat-publication-projection.js";
|
|
import {
|
|
fastResponsePrompt,
|
|
fastResponseReceipt,
|
|
runFastResponseProvider,
|
|
type FastResponseOutcome,
|
|
type FastResponsePromptInput,
|
|
} from "./fast-response-provider.js";
|
|
|
|
type Request = typeof fastResponseRequests.$inferSelect;
|
|
export type FastResponseSource = Pick<
|
|
Request,
|
|
"companyId" | "issueId" | "agentId" | "responsibleUserId"
|
|
> &
|
|
Partial<
|
|
Pick<
|
|
Request,
|
|
| "sourceCommentId"
|
|
| "endpointId"
|
|
| "conversationId"
|
|
| "deliveryId"
|
|
| "sponsored"
|
|
| "sessionGeneration"
|
|
>
|
|
> & { sourceKey: string; acceptedAt: Date };
|
|
export type FastResponseExternalAuthorization = (
|
|
tx: Db,
|
|
request: Request,
|
|
) => Promise<FastResponsePromptInput>;
|
|
|
|
/** A run created for this turn is not older work that this turn is waiting on. */
|
|
export async function fastResponseTurnQueued(
|
|
db: Db,
|
|
request: Pick<
|
|
Request,
|
|
"companyId" | "agentId" | "issueId" | "sourceCommentId" | "acceptedAt"
|
|
>,
|
|
) {
|
|
if (!request.agentId) return false;
|
|
const runs = await db
|
|
.select({
|
|
status: heartbeatRuns.status,
|
|
createdAt: heartbeatRuns.createdAt,
|
|
context: heartbeatRuns.contextSnapshot,
|
|
})
|
|
.from(heartbeatRuns)
|
|
.where(
|
|
and(
|
|
eq(heartbeatRuns.companyId, request.companyId),
|
|
eq(heartbeatRuns.agentId, request.agentId),
|
|
inArray(heartbeatRuns.status, ["queued", "running"]),
|
|
),
|
|
);
|
|
return runs.some((run) => {
|
|
const sourceCommentId =
|
|
run.context?.wakeCommentId ?? run.context?.commentId;
|
|
const ownsTurn =
|
|
run.context?.issueId === request.issueId &&
|
|
(request.sourceCommentId
|
|
? sourceCommentId === request.sourceCommentId
|
|
: run.createdAt >= request.acceptedAt);
|
|
return (
|
|
run.status === "queued" ||
|
|
(!ownsTurn && run.createdAt < request.acceptedAt)
|
|
);
|
|
});
|
|
}
|
|
|
|
/** Board chat streams without a heartbeat run; fence its receipt when real text begins. */
|
|
export async function supersedeFastResponse(
|
|
db: Db,
|
|
companyId: string,
|
|
sourceCommentId: string,
|
|
) {
|
|
await db
|
|
.update(fastResponseRequests)
|
|
.set({
|
|
errorCode: "real_reply_started",
|
|
publicationStatus: "suppressed",
|
|
status: sql`case when ${fastResponseRequests.status} = 'pending' then 'skipped' else ${fastResponseRequests.status} end`,
|
|
})
|
|
.where(
|
|
and(
|
|
eq(fastResponseRequests.companyId, companyId),
|
|
eq(fastResponseRequests.sourceCommentId, sourceCommentId),
|
|
isNull(fastResponseRequests.commentId),
|
|
inArray(fastResponseRequests.status, ["pending", "running"]),
|
|
),
|
|
);
|
|
}
|
|
|
|
/** Called only at an authenticated, accepted human-turn boundary, on its transaction. */
|
|
export async function enqueueFastResponse(tx: Db, source: FastResponseSource) {
|
|
if (!(await instanceSettingsService(tx).getExperimental()).enableFastResponses) return;
|
|
const [config] = await tx
|
|
.select({ enabled: companyFastResponses.enabled })
|
|
.from(companyFastResponses)
|
|
.where(eq(companyFastResponses.companyId, source.companyId));
|
|
if (
|
|
!config?.enabled ||
|
|
!source.issueId ||
|
|
(!source.agentId && !source.sourceKey.startsWith("board:"))
|
|
)
|
|
return;
|
|
const [comment] = source.sourceCommentId
|
|
? await tx
|
|
.select()
|
|
.from(issueComments)
|
|
.where(
|
|
and(
|
|
eq(issueComments.companyId, source.companyId),
|
|
eq(issueComments.id, source.sourceCommentId),
|
|
),
|
|
)
|
|
: [];
|
|
if (
|
|
comment &&
|
|
(comment.origin === "fast_response" ||
|
|
/^\s*\//.test(comment.body) ||
|
|
comment.deletedAt)
|
|
)
|
|
return;
|
|
if (source.acceptedAt.getTime() + FAST_RESPONSE_DEADLINE_MS <= Date.now())
|
|
return;
|
|
await notifyDeliveryWork(tx, DELIVERY_QUEUES.fastResponse);
|
|
if (source.endpointId && source.deliveryId) await notifyChatResponseAvailability(tx, { companyId: source.companyId, deliveryId: source.deliveryId });
|
|
await tx
|
|
.insert(fastResponseRequests)
|
|
.values({
|
|
...source,
|
|
expiresAt: new Date(
|
|
source.acceptedAt.getTime() + FAST_RESPONSE_DEADLINE_MS,
|
|
),
|
|
})
|
|
.onConflictDoNothing();
|
|
}
|
|
|
|
/** Shared with external transport admission. Caller already checks destination authority. */
|
|
export async function fastResponseSourceCurrent(tx: Db, request: Request) {
|
|
if (!(await instanceSettingsService(tx).getExperimental()).enableFastResponses) return false;
|
|
if (!request.issueId || request.expiresAt.getTime() <= Date.now())
|
|
return false;
|
|
const [company] = await tx
|
|
.select({ status: companies.status })
|
|
.from(companies)
|
|
.where(eq(companies.id, request.companyId));
|
|
const [agent] = request.agentId
|
|
? await tx
|
|
.select({ status: agents.status })
|
|
.from(agents)
|
|
.where(
|
|
and(
|
|
eq(agents.id, request.agentId),
|
|
eq(agents.companyId, request.companyId),
|
|
),
|
|
)
|
|
: [];
|
|
if (
|
|
company?.status !== "active" ||
|
|
(request.agentId &&
|
|
(!agent || ["paused", "terminated"].includes(agent.status)))
|
|
)
|
|
return false;
|
|
const [issue] = await tx
|
|
.select()
|
|
.from(issues)
|
|
.where(
|
|
and(
|
|
eq(issues.id, request.issueId),
|
|
eq(issues.companyId, request.companyId),
|
|
),
|
|
)
|
|
.for("update");
|
|
if (
|
|
!issue ||
|
|
issue.hiddenAt ||
|
|
(request.agentId && issue.assigneeAgentId !== request.agentId) ||
|
|
["done", "cancelled"].includes(issue.status) ||
|
|
(request.sessionGeneration !== null &&
|
|
issue.conversationSessionGeneration !== request.sessionGeneration)
|
|
)
|
|
return false;
|
|
if (
|
|
await isAutomaticRecoverySuppressedByPauseHold(
|
|
tx,
|
|
request.companyId,
|
|
request.issueId,
|
|
)
|
|
)
|
|
return false;
|
|
const [source] = request.sourceCommentId
|
|
? await tx
|
|
.select()
|
|
.from(issueComments)
|
|
.where(
|
|
and(
|
|
eq(issueComments.companyId, request.companyId),
|
|
eq(issueComments.id, request.sourceCommentId),
|
|
isNull(issueComments.deletedAt),
|
|
),
|
|
)
|
|
: [];
|
|
if (
|
|
request.sourceCommentId &&
|
|
(!source || source.updatedAt.getTime() > request.acceptedAt.getTime() + 10)
|
|
)
|
|
return false;
|
|
const [answer] = await tx
|
|
.select({ id: issueComments.id })
|
|
.from(issueComments)
|
|
.where(
|
|
and(
|
|
eq(issueComments.companyId, request.companyId),
|
|
eq(issueComments.issueId, request.issueId),
|
|
eq(issueComments.origin, "comment"),
|
|
isNull(issueComments.deletedAt),
|
|
gt(issueComments.createdAt, source?.createdAt ?? request.acceptedAt),
|
|
request.agentId
|
|
? eq(issueComments.authorAgentId, request.agentId)
|
|
: eq(issueComments.authorUserId, "board-concierge"),
|
|
),
|
|
)
|
|
.limit(1);
|
|
const [stopped] = await tx
|
|
.select({ id: heartbeatRuns.id })
|
|
.from(heartbeatRuns)
|
|
.where(
|
|
and(
|
|
eq(heartbeatRuns.companyId, request.companyId),
|
|
sql`${heartbeatRuns.contextSnapshot}->>'issueId' = ${request.issueId}`,
|
|
inArray(heartbeatRuns.status, ["cancelled", "failed", "succeeded"]),
|
|
request.sourceCommentId
|
|
? sql`coalesce(${heartbeatRuns.contextSnapshot}->>'wakeCommentId', ${heartbeatRuns.contextSnapshot}->>'commentId') = ${request.sourceCommentId}`
|
|
: gte(heartbeatRuns.createdAt, request.acceptedAt),
|
|
),
|
|
)
|
|
.limit(1);
|
|
const [streamed] = await tx
|
|
.select({ id: heartbeatRunEvents.id })
|
|
.from(heartbeatRunEvents)
|
|
.innerJoin(heartbeatRuns, eq(heartbeatRuns.id, heartbeatRunEvents.runId))
|
|
.where(
|
|
and(
|
|
eq(heartbeatRunEvents.companyId, request.companyId),
|
|
gt(heartbeatRunEvents.createdAt, request.acceptedAt),
|
|
sql`${heartbeatRuns.contextSnapshot}->>'issueId' = ${request.issueId}`,
|
|
inArray(heartbeatRunEvents.eventType, ["item.delta", "item.completed"]),
|
|
sql`${heartbeatRunEvents.payload}->'prpEvent'->'payload'->>'kind' = 'agentMessage'`,
|
|
sql`length(coalesce(${heartbeatRunEvents.payload}->'prpEvent'->'payload'->>'text', '')) > 0`,
|
|
),
|
|
)
|
|
.limit(1);
|
|
return !answer && !stopped && !streamed;
|
|
}
|
|
|
|
export function fastResponseService(
|
|
db: Db,
|
|
options: {
|
|
provider?: typeof runFastResponseProvider;
|
|
budgetHooks?: BudgetServiceHooks;
|
|
authorizeExternal?: FastResponseExternalAuthorization;
|
|
publishExternal?: (
|
|
tx: Db,
|
|
request: Request,
|
|
commentId: string,
|
|
text: string,
|
|
) => Promise<boolean>;
|
|
} = {},
|
|
) {
|
|
const providerCall = options.provider ?? runFastResponseProvider;
|
|
async function settings(companyId: string): Promise<FastResponseSettings> {
|
|
const [row] = await db
|
|
.select()
|
|
.from(companyFastResponses)
|
|
.where(eq(companyFastResponses.companyId, companyId));
|
|
return row
|
|
? { ...row, provider: row.provider as AiProvider }
|
|
: {
|
|
companyId,
|
|
enabled: false,
|
|
connectionId: null,
|
|
grantId: null,
|
|
provider: null,
|
|
model: null,
|
|
allowSponsored: true,
|
|
};
|
|
}
|
|
async function choices(companyId: string, userId: string) {
|
|
return (await aiConnectionService(db).list(companyId, userId)).filter(
|
|
(c) => c.ownership === "shared" && c.method === "api_key",
|
|
);
|
|
}
|
|
async function configure(
|
|
companyId: string,
|
|
userId: string,
|
|
input: UpdateFastResponse,
|
|
) {
|
|
const data = updateFastResponseSchema.parse(input);
|
|
return withAccountingTransaction(
|
|
db,
|
|
companyId,
|
|
async (tx, publications) => {
|
|
const [previous] = await tx
|
|
.select()
|
|
.from(companyFastResponses)
|
|
.where(eq(companyFastResponses.companyId, companyId));
|
|
const disabling =
|
|
!data.enabled &&
|
|
previous?.connectionId === data.connectionId &&
|
|
previous?.grantId === data.grantId;
|
|
const connection =
|
|
!disabling && data.connectionId && data.grantId
|
|
? await aiConnectionService(tx).selectFastResponse({
|
|
companyId,
|
|
userId,
|
|
connectionId: data.connectionId,
|
|
grantId: data.grantId,
|
|
})
|
|
: null;
|
|
const values = {
|
|
...data,
|
|
companyId,
|
|
provider: disabling
|
|
? previous.provider
|
|
: (connection?.provider ?? null),
|
|
updatedAt: new Date(),
|
|
};
|
|
await tx
|
|
.insert(companyFastResponses)
|
|
.values(values)
|
|
.onConflictDoUpdate({
|
|
target: companyFastResponses.companyId,
|
|
set: values,
|
|
});
|
|
await logActivity(
|
|
tx,
|
|
{
|
|
companyId,
|
|
actorType: "user",
|
|
actorId: userId,
|
|
action: "fast_response.configured",
|
|
entityType: "company",
|
|
entityId: companyId,
|
|
details: data,
|
|
},
|
|
publications,
|
|
);
|
|
return { ...values, provider: values.provider as AiProvider | null };
|
|
},
|
|
);
|
|
}
|
|
async function resolve(
|
|
tx: Db,
|
|
request: Pick<
|
|
Request,
|
|
| "companyId"
|
|
| "issueId"
|
|
| "responsibleUserId"
|
|
| "agentId"
|
|
| "sponsored"
|
|
| "endpointId"
|
|
> &
|
|
Partial<Pick<Request, "connectionId" | "grantId" | "model">>,
|
|
lockConfiguration = false,
|
|
) {
|
|
if (!(await instanceSettingsService(tx).getExperimental()).enableFastResponses)
|
|
throw unprocessable("disabled");
|
|
const configQuery = tx
|
|
.select()
|
|
.from(companyFastResponses)
|
|
.where(eq(companyFastResponses.companyId, request.companyId));
|
|
const [config] = await (lockConfiguration ? configQuery.for("share") : configQuery);
|
|
if (
|
|
!config?.enabled ||
|
|
!config.connectionId ||
|
|
!config.grantId ||
|
|
!config.model
|
|
)
|
|
throw unprocessable("disabled");
|
|
if (
|
|
request.connectionId &&
|
|
(request.connectionId !== config.connectionId ||
|
|
request.grantId !== config.grantId ||
|
|
request.model !== config.model)
|
|
)
|
|
throw forbidden("configuration_changed");
|
|
if (
|
|
request.sponsored &&
|
|
(!config.allowSponsored ||
|
|
request.responsibleUserId ||
|
|
!request.endpointId)
|
|
)
|
|
throw forbidden("sponsorship_disabled");
|
|
const memberships = request.responsibleUserId
|
|
? await tx
|
|
.select({
|
|
companyId: companyMemberships.companyId,
|
|
membershipRole: companyMemberships.membershipRole,
|
|
status: companyMemberships.status,
|
|
})
|
|
.from(companyMemberships)
|
|
.where(
|
|
and(
|
|
eq(companyMemberships.companyId, request.companyId),
|
|
eq(companyMemberships.principalType, "user"),
|
|
eq(companyMemberships.principalId, request.responsibleUserId),
|
|
),
|
|
)
|
|
: [];
|
|
if (!request.sponsored && !request.responsibleUserId)
|
|
throw forbidden("responsible_user_missing");
|
|
const [issue] = request.issueId
|
|
? await tx
|
|
.select()
|
|
.from(issues)
|
|
.where(
|
|
and(
|
|
eq(issues.companyId, request.companyId),
|
|
eq(issues.id, request.issueId),
|
|
),
|
|
)
|
|
: [];
|
|
if (request.issueId && !issue) throw forbidden();
|
|
if (!request.sponsored && issue) {
|
|
const actor: AuthorizationActor = {
|
|
type: "board",
|
|
userId: request.responsibleUserId!,
|
|
source:
|
|
request.responsibleUserId === "local-board"
|
|
? "local_implicit"
|
|
: "session",
|
|
companyIds: memberships.map((m) => m.companyId),
|
|
memberships,
|
|
};
|
|
if (
|
|
!(
|
|
await accessService(tx).decide({
|
|
actor,
|
|
action: "issue:read",
|
|
resource: {
|
|
type: "issue",
|
|
companyId: request.companyId,
|
|
issueId: issue.id,
|
|
projectId: issue.projectId,
|
|
parentIssueId: issue.parentId,
|
|
assigneeAgentId: issue.assigneeAgentId,
|
|
assigneeUserId: issue.assigneeUserId,
|
|
status: issue.status,
|
|
},
|
|
})
|
|
).allowed
|
|
)
|
|
throw forbidden();
|
|
}
|
|
const connection = await aiConnectionService(tx).selectFastResponse({
|
|
companyId: request.companyId,
|
|
connectionId: config.connectionId,
|
|
grantId: config.grantId,
|
|
userId: request.responsibleUserId,
|
|
agentId: request.agentId,
|
|
sponsoredBackground: request.sponsored,
|
|
});
|
|
return { config, connection, issue };
|
|
}
|
|
async function availability(companyId: string, userId: string) {
|
|
if (!(await instanceSettingsService(db).getExperimental()).enableFastResponses)
|
|
return { available: false as const, reason: "disabled" };
|
|
try {
|
|
await resolve(db, {
|
|
companyId,
|
|
responsibleUserId: userId,
|
|
issueId: null,
|
|
agentId: null,
|
|
sponsored: false,
|
|
endpointId: null,
|
|
});
|
|
return { available: true as const };
|
|
} catch {
|
|
return { available: false as const, reason: "connection_unavailable" };
|
|
}
|
|
}
|
|
async function skip(request: Request, reason: string) {
|
|
await db.transaction(async tx => {
|
|
if (request.deliveryId) await notifyChatResponseAvailability(tx, { companyId: request.companyId, deliveryId: request.deliveryId });
|
|
await tx
|
|
.update(fastResponseRequests)
|
|
.set({
|
|
status: "skipped",
|
|
publicationStatus: "suppressed",
|
|
errorCode: reason,
|
|
finishedAt: new Date(),
|
|
})
|
|
.where(
|
|
and(
|
|
eq(fastResponseRequests.id, request.id),
|
|
eq(fastResponseRequests.companyId, request.companyId),
|
|
eq(fastResponseRequests.status, "pending"),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
async function settle(request: Request, outcome: FastResponseOutcome) {
|
|
await withBudgetSpan("budget.accounting.settlement", () => withOperationAccountingTransaction(
|
|
db,
|
|
request.companyId,
|
|
async (tx, publications) => {
|
|
const receipt = outcome.receipt;
|
|
const links: {
|
|
agentId: string | null;
|
|
issueId: string | null;
|
|
projectId: string | null;
|
|
} = { agentId: null, issueId: null, projectId: null };
|
|
for (const [key, table] of [
|
|
["agentId", agents],
|
|
["issueId", issues],
|
|
["projectId", projects],
|
|
] as const) {
|
|
const id = request[key];
|
|
if (!id) continue;
|
|
const [row] = await tx
|
|
.select({ id: table.id })
|
|
.from(table)
|
|
.where(
|
|
and(eq(table.id, id), eq(table.companyId, request.companyId)),
|
|
)
|
|
.for("key share");
|
|
if (row) links[key] = row.id;
|
|
}
|
|
const [current] = await tx.select().from(fastResponseRequests).where(and(
|
|
eq(fastResponseRequests.companyId, request.companyId), eq(fastResponseRequests.id, request.id),
|
|
)).for("update");
|
|
if (!current) return;
|
|
const [existing] = current.costEventId ? await tx.select({ id: costEvents.id, costStatus: costEvents.costStatus,
|
|
inputTokens: costEvents.inputTokens, cachedInputTokens: costEvents.cachedInputTokens, outputTokens: costEvents.outputTokens,
|
|
providerRequestId: costEvents.providerRequestId, pricingProvenance: costEvents.pricingProvenance,
|
|
costCents: sql<string>`${costEvents.costCents}::text` }).from(costEvents).where(and(
|
|
eq(costEvents.companyId, request.companyId), eq(costEvents.id, current.costEventId),
|
|
)).for("update") : [];
|
|
if (current.status !== "running") {
|
|
if (!existing || outcome.errorCode === "interrupted") return;
|
|
if (current.errorCode !== "interrupted" && (existing.costStatus !== "unpriced" || receipt.costStatus === "unpriced")) return;
|
|
}
|
|
let event: { id: string };
|
|
if (existing) {
|
|
const receiptUsage = { inputTokens: receipt.inputTokens ?? current.inputTokens ?? 0, cachedInputTokens: 0,
|
|
outputTokens: receipt.outputTokens ?? current.outputTokens ?? 0, providerRequestId: receipt.providerRequestId ?? current.providerRequestId };
|
|
if (existing.costStatus === "unpriced" && receipt.costStatus !== "unpriced") await applyCostCorrectionInTransaction(tx, publications, { companyId: request.companyId, eventId: existing.id,
|
|
idempotencyKey: `fast-response:${request.id}:late-receipt`, expectedCents: existing.costCents,
|
|
correctedCents: receipt.costCents ?? "0", reason: "Provider receipt arrived after interrupted accounting recovery",
|
|
costStatus: receipt.costStatus,
|
|
receiptUsage,
|
|
pricing: { ...receipt.pricingProvenance, evidence: JSON.stringify({ providerRequestId: receipt.providerRequestId,
|
|
inputTokens: receipt.inputTokens, outputTokens: receipt.outputTokens, evidence: receipt.pricingProvenance.evidence }) },
|
|
actorId: "cost_accounting" });
|
|
else {
|
|
if (Object.entries(receiptUsage).some(([key, value]) => existing[key as keyof typeof existing] !== value))
|
|
await applyCostCorrectionInTransaction(tx, publications, { companyId: request.companyId, eventId: existing.id,
|
|
idempotencyKey: `fast-response:${request.id}:late-usage`, expectedCents: existing.costCents, correctedCents: existing.costCents,
|
|
costStatus: existing.costStatus === "unpriced" ? "unpriced" : existing.costStatus === "estimated" ? "estimated" : "reported", receiptUsage,
|
|
reason: "Provider usage arrived after interrupted accounting recovery",
|
|
pricing: existing.pricingProvenance ?? { source: "unknown" }, actorId: "cost_accounting" });
|
|
if (existing.costStatus !== "unpriced") await logActivity(tx, { companyId: request.companyId,
|
|
actorType: "system", actorId: "cost_accounting", responsibleUserIdOverride: request.responsibleUserId,
|
|
action: "accounting.late_receipt_observed", entityType: "cost_event", entityId: existing.id,
|
|
details: { effectiveCostCents: existing.costCents, ...receipt },
|
|
}, publications);
|
|
}
|
|
event = existing;
|
|
} else event = await createCostEventInTransaction(
|
|
tx,
|
|
request.companyId,
|
|
{
|
|
usageKind: "fast_response",
|
|
responsibleUserId: request.responsibleUserId,
|
|
...links,
|
|
heartbeatRunId: null,
|
|
idempotencyKey: `fast-response:${request.id}`,
|
|
provider: request.provider!,
|
|
biller: request.provider!,
|
|
model: request.model!,
|
|
billingType: "metered_api",
|
|
costStatus: receipt.costStatus,
|
|
inputTokens: receipt.inputTokens ?? 0,
|
|
outputTokens: receipt.outputTokens ?? 0,
|
|
costCents: receipt.costCents ?? "0",
|
|
providerRequestId: receipt.providerRequestId,
|
|
pricingProvenance: receipt.pricingProvenance,
|
|
occurredAt: request.startedAt!,
|
|
},
|
|
publications,
|
|
{
|
|
actorType: request.responsibleUserId ? "user" : "system",
|
|
actorId: request.responsibleUserId ?? "paperclip-fast-response",
|
|
},
|
|
);
|
|
const finishedAt = new Date();
|
|
if (request.deliveryId) await notifyChatResponseAvailability(tx, { companyId: request.companyId, deliveryId: request.deliveryId });
|
|
await tx
|
|
.update(fastResponseRequests)
|
|
.set({
|
|
status: outcome.text
|
|
? "succeeded"
|
|
: receipt.costStatus === "unpriced"
|
|
? "unknown"
|
|
: "failed",
|
|
errorCode: outcome.errorCode ?? null,
|
|
costEventId: event.id,
|
|
providerRequestId: receipt.providerRequestId ?? current.providerRequestId,
|
|
inputTokens: receipt.inputTokens ?? current.inputTokens,
|
|
outputTokens: receipt.outputTokens ?? current.outputTokens,
|
|
finishedAt,
|
|
durationMs: finishedAt.getTime() - request.acceptedAt.getTime(),
|
|
...(outcome.text ? {} : { publicationStatus: "suppressed" }),
|
|
})
|
|
.where(and(eq(fastResponseRequests.companyId, request.companyId), eq(fastResponseRequests.id, request.id)));
|
|
if (receipt.costStatus !== "unpriced")
|
|
await tx
|
|
.update(budgetReservations)
|
|
.set({
|
|
state: outcome.noProviderWork ? "released" : "settled",
|
|
settledAt: finishedAt,
|
|
})
|
|
.where(
|
|
and(
|
|
eq(budgetReservations.companyId, request.companyId),
|
|
eq(budgetReservations.fastResponseRequestId, request.id),
|
|
eq(budgetReservations.state, "held"),
|
|
),
|
|
);
|
|
},
|
|
), { "budget.accounting.owner": "fast_response" });
|
|
|
|
}
|
|
async function process(
|
|
request: Request,
|
|
signal?: AbortSignal,
|
|
): Promise<FastResponseTestResult> {
|
|
let selected: Awaited<ReturnType<typeof resolve>>,
|
|
credential: string,
|
|
prompt: string;
|
|
let secretId: string | undefined,
|
|
secret: typeof companySecrets.$inferSelect | undefined;
|
|
try {
|
|
selected = await resolve(db, request);
|
|
const [agent] = request.agentId
|
|
? await db
|
|
.select({ name: agents.name })
|
|
.from(agents)
|
|
.where(
|
|
and(
|
|
eq(agents.companyId, request.companyId),
|
|
eq(agents.id, request.agentId),
|
|
),
|
|
)
|
|
: [];
|
|
if (request.endpointId) {
|
|
if (!options.authorizeExternal) throw forbidden();
|
|
const context = await db.transaction((tx) =>
|
|
options.authorizeExternal!(tx as unknown as Db, request),
|
|
);
|
|
prompt = fastResponsePrompt(context);
|
|
} else if (request.issueId) {
|
|
const [source] = request.sourceCommentId
|
|
? await db
|
|
.select()
|
|
.from(issueComments)
|
|
.where(
|
|
and(
|
|
eq(issueComments.companyId, request.companyId),
|
|
eq(issueComments.id, request.sourceCommentId),
|
|
),
|
|
)
|
|
: [];
|
|
const recent = await db
|
|
.select({ body: issueComments.body, sourceTrust: issueComments.sourceTrust })
|
|
.from(issueComments)
|
|
.where(
|
|
and(
|
|
eq(issueComments.companyId, request.companyId),
|
|
eq(issueComments.issueId, request.issueId),
|
|
isNull(issueComments.deletedAt),
|
|
eq(issueComments.origin, "comment"),
|
|
lt(
|
|
issueComments.createdAt,
|
|
source?.createdAt ?? request.acceptedAt,
|
|
),
|
|
),
|
|
)
|
|
.orderBy(desc(issueComments.createdAt))
|
|
.limit(2);
|
|
const attachments = request.sourceCommentId
|
|
? await db
|
|
.select({
|
|
name: assets.originalFilename,
|
|
type: assets.contentType,
|
|
})
|
|
.from(issueAttachments)
|
|
.innerJoin(
|
|
assets,
|
|
and(
|
|
eq(assets.id, issueAttachments.assetId),
|
|
eq(assets.companyId, request.companyId),
|
|
),
|
|
)
|
|
.where(
|
|
and(
|
|
eq(issueAttachments.companyId, request.companyId),
|
|
eq(issueAttachments.issueCommentId, request.sourceCommentId),
|
|
),
|
|
)
|
|
.limit(5)
|
|
: [];
|
|
prompt = fastResponsePrompt({
|
|
attachments: attachments.map((a) => `${a.name} (${a.type})`),
|
|
agentName: agent?.name ?? "Board assistant",
|
|
message:
|
|
(source ? sanitizeQuarantinedCommentForHigherTrust(source).body : undefined) ??
|
|
selected.issue?.description ??
|
|
selected.issue?.title ??
|
|
"",
|
|
title: selected.issue?.title,
|
|
recent: recent.reverse().map((c) => sanitizeQuarantinedCommentForHigherTrust(c).body),
|
|
queued: await fastResponseTurnQueued(db, request),
|
|
});
|
|
} else
|
|
prompt = fastResponsePrompt({
|
|
agentName: "Assistant",
|
|
message: "Fix the border around the settings panel.",
|
|
});
|
|
secretId = selected.connection.grant.credentialSecretRefs.find(
|
|
(r) => r.configPath === "ai.credential",
|
|
)?.secretId;
|
|
[secret] = secretId
|
|
? await db
|
|
.select()
|
|
.from(companySecrets)
|
|
.where(
|
|
and(
|
|
eq(companySecrets.id, secretId),
|
|
eq(companySecrets.companyId, request.companyId),
|
|
),
|
|
)
|
|
: [];
|
|
credential = await aiConnectionService(db).credential(
|
|
selected.connection,
|
|
0,
|
|
{
|
|
responsibleUserId: request.responsibleUserId,
|
|
actorType: request.responsibleUserId ? "user" : "system",
|
|
actorId: request.responsibleUserId ?? "paperclip-fast-response",
|
|
issueId: request.issueId,
|
|
},
|
|
);
|
|
} catch (error) {
|
|
const reason = error instanceof Error && error.message === "disabled"
|
|
? "disabled"
|
|
: "connection_unavailable";
|
|
await skip(request, reason);
|
|
return { status: "unavailable", reason };
|
|
}
|
|
const metadata = aiConnectionMetadataSchema.parse(
|
|
selected.connection.connection.config.ai,
|
|
);
|
|
let admitted: Request | null = null;
|
|
try {
|
|
admitted = await withOperationAccountingTransaction(
|
|
db,
|
|
request.companyId,
|
|
async (tx) => {
|
|
if (request.expiresAt.getTime() <= Date.now() || signal?.aborted)
|
|
return null;
|
|
const [company] = await tx.select({ status: companies.status }).from(companies)
|
|
.where(eq(companies.id, request.companyId)).for("share");
|
|
if (!company || company.status !== "active") return null;
|
|
if (selected.issue?.projectId) {
|
|
const [project] = await tx.select({ id: projects.id }).from(projects).where(and(
|
|
eq(projects.companyId, request.companyId), eq(projects.id, selected.issue.projectId),
|
|
)).for("share");
|
|
if (!project) return null;
|
|
}
|
|
const current = await resolve(tx, request, true);
|
|
if (
|
|
current.config.updatedAt.getTime() !==
|
|
selected.config.updatedAt.getTime() ||
|
|
current.config.connectionId !== selected.config.connectionId ||
|
|
current.config.grantId !== selected.config.grantId ||
|
|
current.config.model !== selected.config.model ||
|
|
current.issue?.projectId !== selected.issue?.projectId ||
|
|
current.connection.grant.credentialSecretRefs.find(ref => ref.configPath === "ai.credential")?.secretId !== secretId ||
|
|
current.connection.connection.updatedAt.getTime() !==
|
|
selected.connection.connection.updatedAt.getTime() ||
|
|
current.connection.grant.updatedAt.getTime() !==
|
|
selected.connection.grant.updatedAt.getTime()
|
|
)
|
|
return null;
|
|
if (secretId) {
|
|
const [now] = await tx
|
|
.select()
|
|
.from(companySecrets)
|
|
.where(eq(companySecrets.id, secretId))
|
|
.for("share");
|
|
if (
|
|
!now ||
|
|
now.status !== "active" ||
|
|
now.latestVersion !== secret?.latestVersion
|
|
)
|
|
return null;
|
|
}
|
|
if (
|
|
request.issueId &&
|
|
!(await fastResponseSourceCurrent(tx, request))
|
|
)
|
|
return null;
|
|
if (request.endpointId) await options.authorizeExternal!(tx, request);
|
|
const [agent] = request.agentId
|
|
? await tx
|
|
.select()
|
|
.from(agents)
|
|
.where(
|
|
and(
|
|
eq(agents.id, request.agentId),
|
|
eq(agents.companyId, request.companyId),
|
|
),
|
|
).for("share")
|
|
: [];
|
|
if (
|
|
company?.status !== "active" ||
|
|
(request.agentId &&
|
|
(!agent || ["paused", "terminated"].includes(agent.status)))
|
|
)
|
|
return null;
|
|
const { block, policies, observations } = await budgetServiceInTransaction(tx, []).evaluateAdmission(
|
|
request.companyId, request.agentId, { projectId: current.issue?.projectId });
|
|
if (block) return null;
|
|
const amount = policies.reduce((max, p) => {
|
|
const n = centsToUnits(p.reservationCents);
|
|
return n > max ? n : max;
|
|
}, 0n);
|
|
for (const [index, policy] of policies.entries()) {
|
|
const committed = centsToUnits(observations[index].ledgerCents) + centsToUnits(observations[index].effectiveHeldCents);
|
|
if (
|
|
committed >= centsToUnits(policy.amount) ||
|
|
committed + amount > centsToUnits(policy.amount)
|
|
)
|
|
return null;
|
|
}
|
|
const [claimed] = await tx
|
|
.update(fastResponseRequests)
|
|
.set({
|
|
status: "running",
|
|
startedAt: new Date(),
|
|
connectionId: current.config.connectionId,
|
|
grantId: current.config.grantId,
|
|
model: current.config.model,
|
|
provider: aiConnectionCatalogSlug(
|
|
current.connection.provider,
|
|
metadata.routing,
|
|
),
|
|
projectId: current.issue?.projectId ?? null,
|
|
})
|
|
.where(
|
|
and(
|
|
eq(fastResponseRequests.id, request.id),
|
|
eq(fastResponseRequests.companyId, request.companyId),
|
|
eq(fastResponseRequests.status, "pending"),
|
|
),
|
|
)
|
|
.returning();
|
|
if (!claimed) return null;
|
|
await tx.insert(budgetReservations).values({
|
|
companyId: request.companyId,
|
|
fastResponseRequestId: request.id,
|
|
agentId: request.agentId,
|
|
projectId: current.issue?.projectId,
|
|
amountCents: unitsToCents(amount),
|
|
providerStartedAt: claimed.startedAt,
|
|
});
|
|
await queueBudgetEvaluation(tx, { companyId: request.companyId, agentId: request.agentId, projectId: current.issue?.projectId });
|
|
return claimed;
|
|
},
|
|
);
|
|
} catch {
|
|
/* An optional receipt must never fail the accepted turn. */
|
|
}
|
|
if (!admitted) {
|
|
await skip(request, "not_admitted");
|
|
return { status: "unavailable", reason: "not_admitted" };
|
|
}
|
|
const timeout = AbortSignal.timeout(
|
|
Math.max(1, request.expiresAt.getTime() - Date.now()),
|
|
);
|
|
let outcome: FastResponseOutcome;
|
|
try {
|
|
outcome = await providerCall({
|
|
metadata,
|
|
credential,
|
|
model: admitted.model!,
|
|
prompt,
|
|
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
});
|
|
} catch {
|
|
outcome = {
|
|
errorCode: "provider_failed",
|
|
receipt: fastResponseReceipt(),
|
|
};
|
|
}
|
|
// Commit the visible receipt first: accounting settlement must not spend its display deadline.
|
|
if (outcome.text && request.issueId) {
|
|
const text = outcome.text;
|
|
try {
|
|
const publications: ActivityPublication[] = [];
|
|
await db.transaction(async (tx) => {
|
|
if (!(await fastResponseSourceCurrent(tx as unknown as Db, request)))
|
|
return;
|
|
await resolve(tx as unknown as Db, admitted!);
|
|
if (request.endpointId)
|
|
await options.authorizeExternal!(tx as unknown as Db, request);
|
|
const [locked] = await tx
|
|
.select()
|
|
.from(fastResponseRequests)
|
|
.where(and(eq(fastResponseRequests.companyId, request.companyId), eq(fastResponseRequests.id, request.id)))
|
|
.for("update");
|
|
if (
|
|
!locked ||
|
|
locked.commentId ||
|
|
locked.status !== "running" ||
|
|
locked.errorCode === "real_reply_started"
|
|
)
|
|
return;
|
|
if (request.expiresAt.getTime() <= Date.now()) return;
|
|
const safe = projectSafeChatPublication({
|
|
classification: "external",
|
|
source: "agent_comment",
|
|
text,
|
|
});
|
|
const [source] = request.sourceCommentId
|
|
? await tx
|
|
.select({ sourceTrust: issueComments.sourceTrust })
|
|
.from(issueComments)
|
|
.where(
|
|
and(
|
|
eq(issueComments.companyId, request.companyId),
|
|
eq(issueComments.id, request.sourceCommentId),
|
|
),
|
|
)
|
|
: [];
|
|
const [comment] = await insertIssueComment(tx, {
|
|
companyId: request.companyId,
|
|
issueId: request.issueId!,
|
|
authorAgentId: request.agentId,
|
|
authorUserId: request.agentId ? null : "board-concierge",
|
|
authorType: request.agentId ? "agent" : "system",
|
|
body: safe.text,
|
|
sourceTrust: source?.sourceTrust ?? null,
|
|
conversationSessionGeneration: request.sessionGeneration,
|
|
origin: "fast_response",
|
|
fastResponseRequestId: request.id,
|
|
});
|
|
if (
|
|
request.endpointId &&
|
|
request.conversationId &&
|
|
!(await options.publishExternal?.(
|
|
tx as unknown as Db,
|
|
request,
|
|
comment.id,
|
|
safe.text,
|
|
))
|
|
)
|
|
await tx.insert(chatPublications).values({
|
|
companyId: request.companyId,
|
|
issueId: request.issueId!,
|
|
endpointId: request.endpointId,
|
|
conversationId: request.conversationId,
|
|
commentId: comment.id,
|
|
idempotencyKey: `fast-response:${request.id}`,
|
|
payload: safe,
|
|
});
|
|
if (request.deliveryId) await notifyChatResponseAvailability(tx, { companyId: request.companyId, deliveryId: request.deliveryId });
|
|
await tx
|
|
.update(fastResponseRequests)
|
|
.set({
|
|
commentId: comment.id,
|
|
publicationStatus: request.endpointId ? "pending" : "published",
|
|
})
|
|
.where(and(eq(fastResponseRequests.companyId, request.companyId), eq(fastResponseRequests.id, request.id)));
|
|
await logActivity(
|
|
tx as unknown as Db,
|
|
{
|
|
companyId: request.companyId,
|
|
actorType: "system",
|
|
actorId: "paperclip-fast-response",
|
|
action: "issue.comment_added",
|
|
entityType: "issue",
|
|
entityId: request.issueId!,
|
|
details: { commentId: comment.id, origin: "fast_response" },
|
|
},
|
|
publications,
|
|
);
|
|
});
|
|
for (const publication of publications) publishActivity(publication);
|
|
} catch {
|
|
/* Never substitute an unsafe or obsolete acknowledgement. */
|
|
}
|
|
}
|
|
await settle(admitted, outcome);
|
|
await db
|
|
.update(fastResponseRequests)
|
|
.set({ publicationStatus: request.issueId ? "suppressed" : "test" })
|
|
.where(
|
|
and(
|
|
eq(fastResponseRequests.id, request.id),
|
|
eq(fastResponseRequests.companyId, request.companyId),
|
|
isNull(fastResponseRequests.commentId),
|
|
),
|
|
);
|
|
return outcome.text
|
|
? {
|
|
status: "succeeded",
|
|
text: outcome.text,
|
|
durationMs: Date.now() - request.acceptedAt.getTime(),
|
|
invocationId: request.id,
|
|
usage: outcome.receipt,
|
|
}
|
|
: {
|
|
status: "failed",
|
|
reason: outcome.errorCode ?? "provider_failed",
|
|
invocationId: request.id,
|
|
};
|
|
}
|
|
async function test(companyId: string, userId: string) {
|
|
const now = new Date();
|
|
const [request] = await db
|
|
.insert(fastResponseRequests)
|
|
.values({
|
|
companyId,
|
|
sourceKey: `test:${randomUUID()}`,
|
|
responsibleUserId: userId,
|
|
acceptedAt: now,
|
|
expiresAt: new Date(now.getTime() + FAST_RESPONSE_DEADLINE_MS),
|
|
})
|
|
.returning();
|
|
return process(request);
|
|
}
|
|
async function sweepPending(signal?: AbortSignal) {
|
|
const stale = await db
|
|
.select()
|
|
.from(fastResponseRequests)
|
|
.where(
|
|
and(
|
|
eq(fastResponseRequests.status, "running"),
|
|
lt(fastResponseRequests.expiresAt, new Date(Date.now() - 30_000)),
|
|
),
|
|
)
|
|
.limit(50);
|
|
for (const row of stale)
|
|
await settle(row, {
|
|
errorCode: "interrupted",
|
|
receipt: fastResponseReceipt(),
|
|
});
|
|
await db
|
|
.update(fastResponseRequests)
|
|
.set({
|
|
status: "skipped",
|
|
publicationStatus: "suppressed",
|
|
errorCode: "expired",
|
|
finishedAt: new Date(),
|
|
})
|
|
.where(
|
|
and(
|
|
eq(fastResponseRequests.status, "pending"),
|
|
lte(fastResponseRequests.expiresAt, new Date()),
|
|
),
|
|
);
|
|
const pending = await db
|
|
.select()
|
|
.from(fastResponseRequests)
|
|
.where(
|
|
and(
|
|
eq(fastResponseRequests.status, "pending"),
|
|
sql`${fastResponseRequests.sourceKey} not like 'test:%'`,
|
|
gt(fastResponseRequests.expiresAt, new Date()),
|
|
),
|
|
)
|
|
.orderBy(fastResponseRequests.acceptedAt)
|
|
.limit(8);
|
|
await Promise.allSettled(pending.map((row) => process(row, signal)));
|
|
}
|
|
async function hasPending() {
|
|
const [row] = await db
|
|
.select({ id: fastResponseRequests.id })
|
|
.from(fastResponseRequests)
|
|
.where(inArray(fastResponseRequests.status, ["pending", "running"]))
|
|
.limit(1);
|
|
return Boolean(row);
|
|
}
|
|
async function history(
|
|
companyId: string,
|
|
actor: AuthorizationActor,
|
|
range: { from?: Date; to?: Date; limit?: number } = {},
|
|
): Promise<FastResponseHistoryEntry[]> {
|
|
if (
|
|
!(
|
|
await accessService(db).decide({
|
|
actor,
|
|
action: "company_scope:read",
|
|
resource: { type: "company", companyId },
|
|
})
|
|
).allowed
|
|
)
|
|
throw forbidden();
|
|
const rows = await db
|
|
.select({
|
|
request: fastResponseRequests,
|
|
userName: authUsers.name,
|
|
cost: costEvents,
|
|
issue: issues,
|
|
publicationState: chatPublications.state,
|
|
connectionName: toolConnections.name,
|
|
})
|
|
.from(fastResponseRequests)
|
|
.leftJoin(
|
|
costEvents,
|
|
and(
|
|
eq(costEvents.companyId, companyId),
|
|
eq(costEvents.id, fastResponseRequests.costEventId),
|
|
),
|
|
)
|
|
.leftJoin(
|
|
authUsers,
|
|
eq(authUsers.id, fastResponseRequests.responsibleUserId),
|
|
)
|
|
.leftJoin(
|
|
toolConnections,
|
|
and(
|
|
eq(toolConnections.companyId, companyId),
|
|
eq(toolConnections.id, fastResponseRequests.connectionId),
|
|
),
|
|
)
|
|
.leftJoin(
|
|
chatPublications,
|
|
and(
|
|
eq(chatPublications.companyId, companyId),
|
|
eq(
|
|
chatPublications.idempotencyKey,
|
|
sql`'fast-response:' || ${fastResponseRequests.id}::text`,
|
|
),
|
|
),
|
|
)
|
|
.leftJoin(
|
|
issues,
|
|
and(
|
|
eq(issues.companyId, companyId),
|
|
eq(issues.id, fastResponseRequests.issueId),
|
|
),
|
|
)
|
|
.where(
|
|
and(
|
|
eq(fastResponseRequests.companyId, companyId),
|
|
range.from
|
|
? gte(fastResponseRequests.acceptedAt, range.from)
|
|
: undefined,
|
|
range.to ? lte(fastResponseRequests.acceptedAt, range.to) : undefined,
|
|
),
|
|
)
|
|
.orderBy(desc(fastResponseRequests.acceptedAt))
|
|
.limit(Math.min(range.limit ?? 100, 500));
|
|
return Promise.all(
|
|
rows.map(
|
|
async ({
|
|
request: r,
|
|
userName,
|
|
cost,
|
|
issue,
|
|
publicationState,
|
|
connectionName,
|
|
}) => {
|
|
const visible =
|
|
issue &&
|
|
(
|
|
await accessService(db).decide({
|
|
actor,
|
|
action: "issue:read",
|
|
resource: {
|
|
type: "issue",
|
|
companyId,
|
|
issueId: issue.id,
|
|
projectId: issue.projectId,
|
|
parentIssueId: issue.parentId,
|
|
assigneeAgentId: issue.assigneeAgentId,
|
|
assigneeUserId: issue.assigneeUserId,
|
|
status: issue.status,
|
|
},
|
|
})
|
|
).allowed;
|
|
return {
|
|
id: r.id,
|
|
feature: r.sourceKey.startsWith("test:")
|
|
? "settings.test"
|
|
: "message",
|
|
actorType: r.sponsored ? "system" : "user",
|
|
responsibleUserId: r.responsibleUserId,
|
|
userName,
|
|
issueId: visible ? issue!.id : null,
|
|
issueIdentifier: visible ? issue!.identifier : null,
|
|
agentId: r.agentId,
|
|
runId: null,
|
|
connectionId: r.connectionId ?? "",
|
|
provider: r.provider ?? "",
|
|
model: r.model ?? "",
|
|
status: r.status,
|
|
connectionName,
|
|
publicationStatus: publicationState ?? r.publicationStatus,
|
|
errorCode: r.errorCode,
|
|
startedAt: r.acceptedAt.toISOString(),
|
|
finishedAt: r.finishedAt?.toISOString() ?? null,
|
|
durationMs: r.durationMs,
|
|
inputTokens: r.inputTokens,
|
|
outputTokens: r.outputTokens,
|
|
costCents:
|
|
cost && cost.costStatus !== "unpriced"
|
|
? String(cost.costCents)
|
|
: null,
|
|
costStatus: cost?.costStatus ?? null,
|
|
};
|
|
},
|
|
),
|
|
);
|
|
}
|
|
return {
|
|
settings,
|
|
choices,
|
|
configure,
|
|
availability,
|
|
test,
|
|
history,
|
|
process,
|
|
sweepPending,
|
|
hasPending,
|
|
authorize: (tx: Db, request: Request) => resolve(tx, request),
|
|
};
|
|
}
|