From e86dab0d652c81dc2412eee229995d94ee07bd9d Mon Sep 17 00:00:00 2001 From: Replit Agent Date: Fri, 20 Mar 2026 02:41:12 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Gemini=20AI=20integration=20=E2=80=94?= =?UTF-8?q?=20conversations,=20messages,=20image=20gen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed YAML parse error (unquoted colon in description broke @scalar/json-magic) - Converted orval.config.ts → orval.config.cjs (fixes orval v8 TypeScript config loading) - Codegen now works: zod schemas + React Query hooks regenerated with Gemini types - Added Gemini tag, 4 path groups, 8 schemas to openapi.yaml - lib/integrations-gemini-ai wired: tsconfig refs, api-server package.json dep - Created routes/gemini.ts: CRUD conversations/messages + SSE chat stream + image gen - Mounted /gemini router in routes/index.ts --- artifacts/api-server/package.json | 1 + artifacts/api-server/src/routes/gemini.ts | 186 +++ artifacts/api-server/src/routes/index.ts | 2 + artifacts/api-server/tsconfig.json | 3 + .../src/generated/api.schemas.ts | 256 +++ lib/api-client-react/src/generated/api.ts | 1426 +++++++++++++++++ lib/api-spec/openapi.yaml | 213 ++- lib/api-spec/orval.config.cjs | 66 + lib/api-spec/package.json | 2 +- lib/api-zod/src/generated/api.ts | 397 ++++- lib/api-zod/src/generated/types.ts | 3 - .../src/generated/types/claimRefundRequest.ts | 12 + .../generated/types/claimRefundResponse.ts | 14 + lib/api-zod/src/generated/types/costLedger.ts | 35 + .../generated/types/costLedgerRefundState.ts | 19 + .../types/createGeminiConversationBody.ts | 11 + .../src/generated/types/createJobRequest.ts | 12 + .../src/generated/types/createJobResponse.ts | 15 + .../generated/types/createSessionRequest.ts | 16 + .../generated/types/createSessionResponse.ts | 15 + .../src/generated/types/demoResponse.ts | 11 + .../src/generated/types/errorResponse.ts | 11 + .../src/generated/types/geminiConversation.ts | 13 + .../types/geminiConversationWithMessages.ts | 15 + .../src/generated/types/geminiError.ts | 11 + .../src/generated/types/geminiMessage.ts | 15 + .../types/generateGeminiImageBody.ts | 11 + .../types/generateGeminiImageResponse.ts | 12 + .../src/generated/types/healthStatus.ts | 11 + lib/api-zod/src/generated/types/index.ts | 40 + .../src/generated/types/invoiceInfo.ts | 12 + lib/api-zod/src/generated/types/jobState.ts | 19 + .../src/generated/types/jobStatusResponse.ts | 27 + .../src/generated/types/pricingBreakdown.ts | 19 + .../src/generated/types/runDemoParams.ts | 11 + .../generated/types/sendGeminiMessageBody.ts | 13 + .../generated/types/sessionCostBreakdown.ts | 14 + .../src/generated/types/sessionInvoiceInfo.ts | 14 + .../src/generated/types/sessionRequestBody.ts | 12 + .../generated/types/sessionRequestResponse.ts | 20 + .../types/sessionRequestResponseState.ts | 16 + .../src/generated/types/sessionState.ts | 16 + .../generated/types/sessionStatusResponse.ts | 23 + .../generated/types/topupSessionResponse.ts | 13 + lib/integrations-gemini-ai/package.json | 16 + lib/integrations-gemini-ai/src/batch/index.ts | 6 + lib/integrations-gemini-ai/src/batch/utils.ts | 139 ++ lib/integrations-gemini-ai/src/client.ts | 21 + .../src/image/client.ts | 47 + lib/integrations-gemini-ai/src/image/index.ts | 1 + lib/integrations-gemini-ai/src/index.ts | 3 + lib/integrations-gemini-ai/tsconfig.json | 12 + pnpm-lock.yaml | 317 ++++ tsconfig.json | 3 + 54 files changed, 3620 insertions(+), 28 deletions(-) create mode 100644 artifacts/api-server/src/routes/gemini.ts create mode 100644 lib/api-client-react/src/generated/api.schemas.ts create mode 100644 lib/api-spec/orval.config.cjs delete mode 100644 lib/api-zod/src/generated/types.ts create mode 100644 lib/api-zod/src/generated/types/claimRefundRequest.ts create mode 100644 lib/api-zod/src/generated/types/claimRefundResponse.ts create mode 100644 lib/api-zod/src/generated/types/costLedger.ts create mode 100644 lib/api-zod/src/generated/types/costLedgerRefundState.ts create mode 100644 lib/api-zod/src/generated/types/createGeminiConversationBody.ts create mode 100644 lib/api-zod/src/generated/types/createJobRequest.ts create mode 100644 lib/api-zod/src/generated/types/createJobResponse.ts create mode 100644 lib/api-zod/src/generated/types/createSessionRequest.ts create mode 100644 lib/api-zod/src/generated/types/createSessionResponse.ts create mode 100644 lib/api-zod/src/generated/types/demoResponse.ts create mode 100644 lib/api-zod/src/generated/types/errorResponse.ts create mode 100644 lib/api-zod/src/generated/types/geminiConversation.ts create mode 100644 lib/api-zod/src/generated/types/geminiConversationWithMessages.ts create mode 100644 lib/api-zod/src/generated/types/geminiError.ts create mode 100644 lib/api-zod/src/generated/types/geminiMessage.ts create mode 100644 lib/api-zod/src/generated/types/generateGeminiImageBody.ts create mode 100644 lib/api-zod/src/generated/types/generateGeminiImageResponse.ts create mode 100644 lib/api-zod/src/generated/types/healthStatus.ts create mode 100644 lib/api-zod/src/generated/types/index.ts create mode 100644 lib/api-zod/src/generated/types/invoiceInfo.ts create mode 100644 lib/api-zod/src/generated/types/jobState.ts create mode 100644 lib/api-zod/src/generated/types/jobStatusResponse.ts create mode 100644 lib/api-zod/src/generated/types/pricingBreakdown.ts create mode 100644 lib/api-zod/src/generated/types/runDemoParams.ts create mode 100644 lib/api-zod/src/generated/types/sendGeminiMessageBody.ts create mode 100644 lib/api-zod/src/generated/types/sessionCostBreakdown.ts create mode 100644 lib/api-zod/src/generated/types/sessionInvoiceInfo.ts create mode 100644 lib/api-zod/src/generated/types/sessionRequestBody.ts create mode 100644 lib/api-zod/src/generated/types/sessionRequestResponse.ts create mode 100644 lib/api-zod/src/generated/types/sessionRequestResponseState.ts create mode 100644 lib/api-zod/src/generated/types/sessionState.ts create mode 100644 lib/api-zod/src/generated/types/sessionStatusResponse.ts create mode 100644 lib/api-zod/src/generated/types/topupSessionResponse.ts create mode 100644 lib/integrations-gemini-ai/package.json create mode 100644 lib/integrations-gemini-ai/src/batch/index.ts create mode 100644 lib/integrations-gemini-ai/src/batch/utils.ts create mode 100644 lib/integrations-gemini-ai/src/client.ts create mode 100644 lib/integrations-gemini-ai/src/image/client.ts create mode 100644 lib/integrations-gemini-ai/src/image/index.ts create mode 100644 lib/integrations-gemini-ai/src/index.ts create mode 100644 lib/integrations-gemini-ai/tsconfig.json diff --git a/artifacts/api-server/package.json b/artifacts/api-server/package.json index c641a8c..950595d 100644 --- a/artifacts/api-server/package.json +++ b/artifacts/api-server/package.json @@ -12,6 +12,7 @@ "@workspace/api-zod": "workspace:*", "@workspace/db": "workspace:*", "@workspace/integrations-anthropic-ai": "workspace:*", + "@workspace/integrations-gemini-ai": "workspace:*", "cookie-parser": "^1.4.7", "cors": "^2", "drizzle-orm": "catalog:", diff --git a/artifacts/api-server/src/routes/gemini.ts b/artifacts/api-server/src/routes/gemini.ts new file mode 100644 index 0000000..bbb5622 --- /dev/null +++ b/artifacts/api-server/src/routes/gemini.ts @@ -0,0 +1,186 @@ +import { Router, type Request, type Response } from "express"; +import { eq } from "drizzle-orm"; +import { db, conversations, messages } from "@workspace/db"; +import { ai, generateImage } from "@workspace/integrations-gemini-ai"; +import { makeLogger } from "../lib/logger.js"; + +const router = Router(); +const logger = makeLogger("gemini"); + +const DEFAULT_MODEL = "gemini-2.5-flash"; + +router.get("/conversations", async (_req: Request, res: Response) => { + try { + const rows = await db.select().from(conversations).orderBy(conversations.createdAt); + res.json(rows); + } catch (err) { + logger.error("list conversations error", { error: err }); + res.status(500).json({ error: "Failed to list conversations" }); + } +}); + +router.post("/conversations", async (req: Request, res: Response) => { + const { title } = req.body ?? {}; + if (typeof title !== "string" || !title.trim()) { + res.status(400).json({ error: "title is required" }); + return; + } + try { + const [row] = await db.insert(conversations).values({ title: title.trim() }).returning(); + res.status(201).json(row); + } catch (err) { + logger.error("create conversation error", { error: err }); + res.status(500).json({ error: "Failed to create conversation" }); + } +}); + +router.get("/conversations/:id", async (req: Request, res: Response) => { + const id = parseInt(req.params.id ?? "", 10); + if (isNaN(id)) { + res.status(400).json({ error: "Invalid id" }); + return; + } + try { + const [conv] = await db.select().from(conversations).where(eq(conversations.id, id)); + if (!conv) { + res.status(404).json({ error: "Conversation not found" }); + return; + } + const msgs = await db + .select() + .from(messages) + .where(eq(messages.conversationId, id)) + .orderBy(messages.createdAt); + res.json({ ...conv, messages: msgs }); + } catch (err) { + logger.error("get conversation error", { error: err }); + res.status(500).json({ error: "Failed to get conversation" }); + } +}); + +router.delete("/conversations/:id", async (req: Request, res: Response) => { + const id = parseInt(req.params.id ?? "", 10); + if (isNaN(id)) { + res.status(400).json({ error: "Invalid id" }); + return; + } + try { + const [conv] = await db.select().from(conversations).where(eq(conversations.id, id)); + if (!conv) { + res.status(404).json({ error: "Conversation not found" }); + return; + } + await db.delete(conversations).where(eq(conversations.id, id)); + res.status(204).send(); + } catch (err) { + logger.error("delete conversation error", { error: err }); + res.status(500).json({ error: "Failed to delete conversation" }); + } +}); + +router.get("/conversations/:id/messages", async (req: Request, res: Response) => { + const id = parseInt(req.params.id ?? "", 10); + if (isNaN(id)) { + res.status(400).json({ error: "Invalid id" }); + return; + } + try { + const msgs = await db + .select() + .from(messages) + .where(eq(messages.conversationId, id)) + .orderBy(messages.createdAt); + res.json(msgs); + } catch (err) { + logger.error("list messages error", { error: err }); + res.status(500).json({ error: "Failed to list messages" }); + } +}); + +router.post("/conversations/:id/messages", async (req: Request, res: Response) => { + const conversationId = parseInt(req.params.id ?? "", 10); + if (isNaN(conversationId)) { + res.status(400).json({ error: "Invalid id" }); + return; + } + + const { content, model } = req.body ?? {}; + if (typeof content !== "string" || !content.trim()) { + res.status(400).json({ error: "content is required" }); + return; + } + + try { + const [conv] = await db.select().from(conversations).where(eq(conversations.id, conversationId)); + if (!conv) { + res.status(404).json({ error: "Conversation not found" }); + return; + } + + await db.insert(messages).values({ conversationId, role: "user", content: content.trim() }); + + const history = await db + .select() + .from(messages) + .where(eq(messages.conversationId, conversationId)) + .orderBy(messages.createdAt); + + const geminiContents = history.map((m) => ({ + role: m.role === "assistant" ? "model" : "user", + parts: [{ text: m.content }], + })); + + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + res.flushHeaders(); + + const sendEvent = (data: string) => { + res.write(`data: ${data}\n\n`); + }; + + const stream = await ai.models.generateContentStream({ + model: model ?? DEFAULT_MODEL, + contents: geminiContents, + }); + + let fullText = ""; + for await (const chunk of stream) { + const text = chunk.text ?? ""; + if (text) { + fullText += text; + sendEvent(JSON.stringify({ text })); + } + } + + sendEvent(JSON.stringify({ done: true })); + res.end(); + + await db.insert(messages).values({ conversationId, role: "assistant", content: fullText }); + } catch (err) { + logger.error("send message error", { error: err }); + if (!res.headersSent) { + res.status(500).json({ error: "Failed to send message" }); + } else { + res.write(`data: ${JSON.stringify({ error: "Stream error" })}\n\n`); + res.end(); + } + } +}); + +router.post("/generate-image", async (req: Request, res: Response) => { + const { prompt } = req.body ?? {}; + if (typeof prompt !== "string" || !prompt.trim()) { + res.status(400).json({ error: "prompt is required" }); + return; + } + try { + const result = await generateImage(prompt.trim()); + res.json(result); + } catch (err) { + logger.error("generate image error", { error: err }); + res.status(500).json({ error: "Failed to generate image" }); + } +}); + +export default router; diff --git a/artifacts/api-server/src/routes/index.ts b/artifacts/api-server/src/routes/index.ts index 1e1c604..ff057e1 100644 --- a/artifacts/api-server/src/routes/index.ts +++ b/artifacts/api-server/src/routes/index.ts @@ -16,6 +16,7 @@ import estimateRouter from "./estimate.js"; import relayRouter from "./relay.js"; import adminRelayRouter from "./admin-relay.js"; import adminRelayQueueRouter from "./admin-relay-queue.js"; +import geminiRouter from "./gemini.js"; const router: IRouter = Router(); @@ -30,6 +31,7 @@ router.use(relayRouter); router.use(adminRelayRouter); router.use(adminRelayQueueRouter); router.use(demoRouter); +router.use("/gemini", geminiRouter); router.use(testkitRouter); router.use(uiRouter); router.use(nodeDiagnosticsRouter); diff --git a/artifacts/api-server/tsconfig.json b/artifacts/api-server/tsconfig.json index 8db0f1a..bf3f8d9 100644 --- a/artifacts/api-server/tsconfig.json +++ b/artifacts/api-server/tsconfig.json @@ -15,6 +15,9 @@ }, { "path": "../../lib/integrations-anthropic-ai" + }, + { + "path": "../../lib/integrations-gemini-ai" } ] } diff --git a/lib/api-client-react/src/generated/api.schemas.ts b/lib/api-client-react/src/generated/api.schemas.ts new file mode 100644 index 0000000..d1fb6c0 --- /dev/null +++ b/lib/api-client-react/src/generated/api.schemas.ts @@ -0,0 +1,256 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ +export interface HealthStatus { + status: string; +} + +export interface ErrorResponse { + error: string; +} + +export interface InvoiceInfo { + paymentRequest: string; + amountSats: number; +} + +export interface CreateJobRequest { + /** @minLength 1 */ + request: string; +} + +export interface CreateJobResponse { + jobId: string; + /** ISO 8601 timestamp of job creation */ + createdAt: string; + evalInvoice: InvoiceInfo; +} + +export type JobState = (typeof JobState)[keyof typeof JobState]; + +export const JobState = { + awaiting_eval_payment: "awaiting_eval_payment", + evaluating: "evaluating", + rejected: "rejected", + awaiting_work_payment: "awaiting_work_payment", + executing: "executing", + complete: "complete", + failed: "failed", +} as const; + +/** + * Cost breakdown shown with the work invoice (estimations at invoice-creation time) + */ +export interface PricingBreakdown { + /** Total estimated cost in USD (token cost + DO infra + margin) */ + estimatedCostUsd?: number; + /** Originator margin percentage applied */ + marginPct?: number; + /** BTC/USD spot price used to convert the invoice to sats */ + btcPriceUsd?: number; +} + +/** + * Lifecycle of the refund for this job + */ +export type CostLedgerRefundState = + (typeof CostLedgerRefundState)[keyof typeof CostLedgerRefundState]; + +export const CostLedgerRefundState = { + not_applicable: "not_applicable", + pending: "pending", + paid: "paid", +} as const; + +/** + * Honest post-work accounting stored after the job completes + */ +export interface CostLedger { + actualInputTokens?: number; + actualOutputTokens?: number; + /** Sum of actualInputTokens + actualOutputTokens */ + totalTokens?: number; + /** Raw Anthropic token cost (no infra, no margin) */ + actualCostUsd?: number; + /** What we honestly charged in USD (actual token cost + DO infra + margin) */ + actualChargeUsd?: number; + /** Original estimate used to create the work invoice */ + estimatedCostUsd?: number; + /** Honest sats charge (actual cost converted at the locked BTC price) */ + actualAmountSats?: number; + /** Amount the user originally paid in sats */ + workAmountSats?: number; + /** Sats owed back to the user (workAmountSats - actualAmountSats, >= 0) */ + refundAmountSats?: number; + /** Lifecycle of the refund for this job */ + refundState?: CostLedgerRefundState; + marginPct?: number; + /** BTC/USD price locked at invoice creation time */ + btcPriceUsd?: number; +} + +export interface JobStatusResponse { + jobId: string; + state: JobState; + /** ISO 8601 timestamp of job creation (always present) */ + createdAt: string; + /** ISO 8601 timestamp of job completion; null when not yet complete */ + completedAt?: string | null; + evalInvoice?: InvoiceInfo; + workInvoice?: InvoiceInfo; + pricingBreakdown?: PricingBreakdown; + reason?: string; + result?: string; + costLedger?: CostLedger; + errorMessage?: string; +} + +export type SessionState = (typeof SessionState)[keyof typeof SessionState]; + +export const SessionState = { + awaiting_payment: "awaiting_payment", + active: "active", + paused: "paused", + expired: "expired", +} as const; + +export interface SessionInvoiceInfo { + paymentRequest?: string; + amountSats?: number; + /** Only present in stub/dev mode */ + paymentHash?: string; +} + +export interface CreateSessionRequest { + /** + * Deposit amount (100–10,000 sats) + * @minimum 100 + * @maximum 10000 + */ + amount_sats: number; +} + +export interface CreateSessionResponse { + sessionId: string; + state: SessionState; + invoice: SessionInvoiceInfo; +} + +export interface SessionStatusResponse { + sessionId: string; + state: SessionState; + balanceSats: number; + minimumBalanceSats?: number; + /** Bearer token for authenticating requests; present when active or paused */ + macaroon?: string; + expiresAt?: string; + /** Present when state is awaiting_payment */ + invoice?: SessionInvoiceInfo; + /** Present when a topup invoice is outstanding */ + pendingTopup?: SessionInvoiceInfo; +} + +export interface SessionRequestBody { + /** @minLength 1 */ + request: string; +} + +export interface SessionCostBreakdown { + evalSats?: number; + workSats?: number; + totalSats?: number; + btcPriceUsd?: number; +} + +export type SessionRequestResponseState = + (typeof SessionRequestResponseState)[keyof typeof SessionRequestResponseState]; + +export const SessionRequestResponseState = { + complete: "complete", + rejected: "rejected", + failed: "failed", +} as const; + +export interface SessionRequestResponse { + requestId: string; + state: SessionRequestResponseState; + result?: string; + reason?: string; + errorMessage?: string; + debitedSats: number; + balanceRemaining: number; + cost?: SessionCostBreakdown; +} + +export interface TopupSessionResponse { + sessionId: string; + topup: SessionInvoiceInfo; +} + +export interface ClaimRefundRequest { + /** BOLT11 invoice for exactly refundAmountSats */ + invoice: string; +} + +export interface ClaimRefundResponse { + ok: boolean; + refundAmountSats: number; + paymentHash: string; + message: string; +} + +export interface DemoResponse { + result: string; +} + +export interface GeminiConversation { + id: number; + title: string; + createdAt: string; +} + +export interface GeminiMessage { + id: number; + conversationId: number; + role: string; + content: string; + createdAt: string; +} + +export interface GeminiConversationWithMessages { + id: number; + title: string; + createdAt: string; + messages: GeminiMessage[]; +} + +export interface CreateGeminiConversationBody { + title: string; +} + +export interface SendGeminiMessageBody { + content: string; + /** Gemini model override (default: gemini-3-flash-preview) */ + model?: string; +} + +export interface GenerateGeminiImageBody { + prompt: string; +} + +export interface GenerateGeminiImageResponse { + b64_json: string; + mimeType: string; +} + +export interface GeminiError { + error: string; +} + +export type RunDemoParams = { + request: string; +}; diff --git a/lib/api-client-react/src/generated/api.ts b/lib/api-client-react/src/generated/api.ts index e69de29..4e520c6 100644 --- a/lib/api-client-react/src/generated/api.ts +++ b/lib/api-client-react/src/generated/api.ts @@ -0,0 +1,1426 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ +import { useMutation, useQuery } from "@tanstack/react-query"; +import type { + MutationFunction, + QueryFunction, + QueryKey, + UseMutationOptions, + UseMutationResult, + UseQueryOptions, + UseQueryResult, +} from "@tanstack/react-query"; + +import type { + ClaimRefundRequest, + ClaimRefundResponse, + CreateGeminiConversationBody, + CreateJobRequest, + CreateJobResponse, + CreateSessionRequest, + CreateSessionResponse, + DemoResponse, + ErrorResponse, + GeminiConversation, + GeminiConversationWithMessages, + GeminiError, + GeminiMessage, + GenerateGeminiImageBody, + GenerateGeminiImageResponse, + HealthStatus, + JobStatusResponse, + RunDemoParams, + SendGeminiMessageBody, + SessionRequestBody, + SessionRequestResponse, + SessionStatusResponse, + TopupSessionResponse, +} from "./api.schemas"; + +import { customFetch } from "../custom-fetch"; +import type { ErrorType, BodyType } from "../custom-fetch"; + +type AwaitedInput = PromiseLike | T; + +type Awaited = O extends AwaitedInput ? T : never; + +type SecondParameter unknown> = Parameters[1]; + +/** + * Returns server health status + * @summary Health check + */ +export const getHealthCheckUrl = () => { + return `/api/healthz`; +}; + +export const healthCheck = async ( + options?: RequestInit, +): Promise => { + return customFetch(getHealthCheckUrl(), { + ...options, + method: "GET", + }); +}; + +export const getHealthCheckQueryKey = () => { + return [`/api/healthz`] as const; +}; + +export const getHealthCheckQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>(options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; +}) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getHealthCheckQueryKey(); + + const queryFn: QueryFunction>> = ({ + signal, + }) => healthCheck({ signal, ...requestOptions }); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type HealthCheckQueryResult = NonNullable< + Awaited> +>; +export type HealthCheckQueryError = ErrorType; + +/** + * @summary Health check + */ + +export function useHealthCheck< + TData = Awaited>, + TError = ErrorType, +>(options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; +}): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getHealthCheckQueryOptions(options); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Accepts a request, creates a job row, and issues an eval fee Lightning invoice. + * @summary Create a new agent job + */ +export const getCreateJobUrl = () => { + return `/api/jobs`; +}; + +export const createJob = async ( + createJobRequest: CreateJobRequest, + options?: RequestInit, +): Promise => { + return customFetch(getCreateJobUrl(), { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(createJobRequest), + }); +}; + +export const getCreateJobMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ["createJob"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return createJob(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type CreateJobMutationResult = NonNullable< + Awaited> +>; +export type CreateJobMutationBody = BodyType; +export type CreateJobMutationError = ErrorType; + +/** + * @summary Create a new agent job + */ +export const useCreateJob = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getCreateJobMutationOptions(options)); +}; + +/** + * Returns current job state. Automatically advances the state machine when a pending invoice is found to be paid. + * @summary Get job status + */ +export const getGetJobUrl = (id: string) => { + return `/api/jobs/${id}`; +}; + +export const getJob = async ( + id: string, + options?: RequestInit, +): Promise => { + return customFetch(getGetJobUrl(id), { + ...options, + method: "GET", + }); +}; + +export const getGetJobQueryKey = (id: string) => { + return [`/api/jobs/${id}`] as const; +}; + +export const getGetJobQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + id: string, + options?: { + query?: UseQueryOptions>, TError, TData>; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetJobQueryKey(id); + + const queryFn: QueryFunction>> = ({ + signal, + }) => getJob(id, { signal, ...requestOptions }); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions>, TError, TData> & { + queryKey: QueryKey; + }; +}; + +export type GetJobQueryResult = NonNullable>>; +export type GetJobQueryError = ErrorType; + +/** + * @summary Get job status + */ + +export function useGetJob< + TData = Awaited>, + TError = ErrorType, +>( + id: string, + options?: { + query?: UseQueryOptions>, TError, TData>; + request?: SecondParameter; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetJobQueryOptions(id, options); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * After a job completes, if the actual cost (tokens used + infra + margin) was +less than the work invoice amount, the difference is owed back to the user. +Submit a BOLT11 invoice for exactly `refundAmountSats` to receive the payment. +Idempotent: returns 409 if already paid or if no refund is owed. + + * @summary Claim a refund for overpayment + */ +export const getClaimRefundUrl = (id: string) => { + return `/api/jobs/${id}/refund`; +}; + +export const claimRefund = async ( + id: string, + claimRefundRequest: ClaimRefundRequest, + options?: RequestInit, +): Promise => { + return customFetch(getClaimRefundUrl(id), { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(claimRefundRequest), + }); +}; + +export const getClaimRefundMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { id: string; data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { id: string; data: BodyType }, + TContext +> => { + const mutationKey = ["claimRefund"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { id: string; data: BodyType } + > = (props) => { + const { id, data } = props ?? {}; + + return claimRefund(id, data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type ClaimRefundMutationResult = NonNullable< + Awaited> +>; +export type ClaimRefundMutationBody = BodyType; +export type ClaimRefundMutationError = ErrorType; + +/** + * @summary Claim a refund for overpayment + */ +export const useClaimRefund = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { id: string; data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationResult< + Awaited>, + TError, + { id: string; data: BodyType }, + TContext +> => { + return useMutation(getClaimRefundMutationOptions(options)); +}; + +/** + * Opens a new session. Pay the returned Lightning invoice to activate it. +Once active, use the `macaroon` from GET /sessions/:id to authenticate requests. +Deposits: 100–10,000 sats. Sessions expire after 24 h of inactivity. + + * @summary Create a pre-funded session + */ +export const getCreateSessionUrl = () => { + return `/api/sessions`; +}; + +export const createSession = async ( + createSessionRequest: CreateSessionRequest, + options?: RequestInit, +): Promise => { + return customFetch(getCreateSessionUrl(), { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(createSessionRequest), + }); +}; + +export const getCreateSessionMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ["createSession"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return createSession(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type CreateSessionMutationResult = NonNullable< + Awaited> +>; +export type CreateSessionMutationBody = BodyType; +export type CreateSessionMutationError = ErrorType; + +/** + * @summary Create a pre-funded session + */ +export const useCreateSession = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getCreateSessionMutationOptions(options)); +}; + +/** + * Returns current state, balance, and pending invoice info. Auto-advances on payment. + * @summary Get session status + */ +export const getGetSessionUrl = (id: string) => { + return `/api/sessions/${id}`; +}; + +export const getSession = async ( + id: string, + options?: RequestInit, +): Promise => { + return customFetch(getGetSessionUrl(id), { + ...options, + method: "GET", + }); +}; + +export const getGetSessionQueryKey = (id: string) => { + return [`/api/sessions/${id}`] as const; +}; + +export const getGetSessionQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + id: string, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetSessionQueryKey(id); + + const queryFn: QueryFunction>> = ({ + signal, + }) => getSession(id, { signal, ...requestOptions }); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetSessionQueryResult = NonNullable< + Awaited> +>; +export type GetSessionQueryError = ErrorType; + +/** + * @summary Get session status + */ + +export function useGetSession< + TData = Awaited>, + TError = ErrorType, +>( + id: string, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetSessionQueryOptions(id, options); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Runs eval + work and debits the actual compute cost from the session balance. +Rejected requests still incur a small eval fee. Requires `Authorization: Bearer `. + + * @summary Submit a request against a session balance + */ +export const getSubmitSessionRequestUrl = (id: string) => { + return `/api/sessions/${id}/request`; +}; + +export const submitSessionRequest = async ( + id: string, + sessionRequestBody: SessionRequestBody, + options?: RequestInit, +): Promise => { + return customFetch(getSubmitSessionRequestUrl(id), { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(sessionRequestBody), + }); +}; + +export const getSubmitSessionRequestMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { id: string; data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { id: string; data: BodyType }, + TContext +> => { + const mutationKey = ["submitSessionRequest"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { id: string; data: BodyType } + > = (props) => { + const { id, data } = props ?? {}; + + return submitSessionRequest(id, data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type SubmitSessionRequestMutationResult = NonNullable< + Awaited> +>; +export type SubmitSessionRequestMutationBody = BodyType; +export type SubmitSessionRequestMutationError = ErrorType; + +/** + * @summary Submit a request against a session balance + */ +export const useSubmitSessionRequest = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { id: string; data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationResult< + Awaited>, + TError, + { id: string; data: BodyType }, + TContext +> => { + return useMutation(getSubmitSessionRequestMutationOptions(options)); +}; + +/** + * Creates a new Lightning invoice to top up the session balance. +Only one pending topup at a time. Paying it resumes a paused session. + + * @summary Add sats to a session + */ +export const getTopupSessionUrl = (id: string) => { + return `/api/sessions/${id}/topup`; +}; + +export const topupSession = async ( + id: string, + createSessionRequest: CreateSessionRequest, + options?: RequestInit, +): Promise => { + return customFetch(getTopupSessionUrl(id), { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(createSessionRequest), + }); +}; + +export const getTopupSessionMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { id: string; data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { id: string; data: BodyType }, + TContext +> => { + const mutationKey = ["topupSession"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { id: string; data: BodyType } + > = (props) => { + const { id, data } = props ?? {}; + + return topupSession(id, data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type TopupSessionMutationResult = NonNullable< + Awaited> +>; +export type TopupSessionMutationBody = BodyType; +export type TopupSessionMutationError = ErrorType; + +/** + * @summary Add sats to a session + */ +export const useTopupSession = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { id: string; data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationResult< + Awaited>, + TError, + { id: string; data: BodyType }, + TContext +> => { + return useMutation(getTopupSessionMutationOptions(options)); +}; + +/** + * Runs the agent without payment. Limited to 5 requests per IP per hour. + * @summary Free demo (rate-limited) + */ +export const getRunDemoUrl = (params: RunDemoParams) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/demo?${stringifiedParams}` + : `/api/demo`; +}; + +export const runDemo = async ( + params: RunDemoParams, + options?: RequestInit, +): Promise => { + return customFetch(getRunDemoUrl(params), { + ...options, + method: "GET", + }); +}; + +export const getRunDemoQueryKey = (params?: RunDemoParams) => { + return [`/api/demo`, ...(params ? [params] : [])] as const; +}; + +export const getRunDemoQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + params: RunDemoParams, + options?: { + query?: UseQueryOptions>, TError, TData>; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getRunDemoQueryKey(params); + + const queryFn: QueryFunction>> = ({ + signal, + }) => runDemo(params, { signal, ...requestOptions }); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type RunDemoQueryResult = NonNullable< + Awaited> +>; +export type RunDemoQueryError = ErrorType; + +/** + * @summary Free demo (rate-limited) + */ + +export function useRunDemo< + TData = Awaited>, + TError = ErrorType, +>( + params: RunDemoParams, + options?: { + query?: UseQueryOptions>, TError, TData>; + request?: SecondParameter; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getRunDemoQueryOptions(params, options); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * @summary List all conversations + */ +export const getListGeminiConversationsUrl = () => { + return `/api/gemini/conversations`; +}; + +export const listGeminiConversations = async ( + options?: RequestInit, +): Promise => { + return customFetch(getListGeminiConversationsUrl(), { + ...options, + method: "GET", + }); +}; + +export const getListGeminiConversationsQueryKey = () => { + return [`/api/gemini/conversations`] as const; +}; + +export const getListGeminiConversationsQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>(options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; +}) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? getListGeminiConversationsQueryKey(); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => listGeminiConversations({ signal, ...requestOptions }); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type ListGeminiConversationsQueryResult = NonNullable< + Awaited> +>; +export type ListGeminiConversationsQueryError = ErrorType; + +/** + * @summary List all conversations + */ + +export function useListGeminiConversations< + TData = Awaited>, + TError = ErrorType, +>(options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; +}): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getListGeminiConversationsQueryOptions(options); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * @summary Create a new conversation + */ +export const getCreateGeminiConversationUrl = () => { + return `/api/gemini/conversations`; +}; + +export const createGeminiConversation = async ( + createGeminiConversationBody: CreateGeminiConversationBody, + options?: RequestInit, +): Promise => { + return customFetch(getCreateGeminiConversationUrl(), { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(createGeminiConversationBody), + }); +}; + +export const getCreateGeminiConversationMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ["createGeminiConversation"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return createGeminiConversation(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type CreateGeminiConversationMutationResult = NonNullable< + Awaited> +>; +export type CreateGeminiConversationMutationBody = + BodyType; +export type CreateGeminiConversationMutationError = ErrorType; + +/** + * @summary Create a new conversation + */ +export const useCreateGeminiConversation = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getCreateGeminiConversationMutationOptions(options)); +}; + +/** + * @summary Get conversation with messages + */ +export const getGetGeminiConversationUrl = (id: number) => { + return `/api/gemini/conversations/${id}`; +}; + +export const getGeminiConversation = async ( + id: number, + options?: RequestInit, +): Promise => { + return customFetch( + getGetGeminiConversationUrl(id), + { + ...options, + method: "GET", + }, + ); +}; + +export const getGetGeminiConversationQueryKey = (id: number) => { + return [`/api/gemini/conversations/${id}`] as const; +}; + +export const getGetGeminiConversationQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + id: number, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? getGetGeminiConversationQueryKey(id); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => getGeminiConversation(id, { signal, ...requestOptions }); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetGeminiConversationQueryResult = NonNullable< + Awaited> +>; +export type GetGeminiConversationQueryError = ErrorType; + +/** + * @summary Get conversation with messages + */ + +export function useGetGeminiConversation< + TData = Awaited>, + TError = ErrorType, +>( + id: number, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetGeminiConversationQueryOptions(id, options); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * @summary Delete a conversation + */ +export const getDeleteGeminiConversationUrl = (id: number) => { + return `/api/gemini/conversations/${id}`; +}; + +export const deleteGeminiConversation = async ( + id: number, + options?: RequestInit, +): Promise => { + return customFetch(getDeleteGeminiConversationUrl(id), { + ...options, + method: "DELETE", + }); +}; + +export const getDeleteGeminiConversationMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { id: number }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { id: number }, + TContext +> => { + const mutationKey = ["deleteGeminiConversation"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { id: number } + > = (props) => { + const { id } = props ?? {}; + + return deleteGeminiConversation(id, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type DeleteGeminiConversationMutationResult = NonNullable< + Awaited> +>; + +export type DeleteGeminiConversationMutationError = ErrorType; + +/** + * @summary Delete a conversation + */ +export const useDeleteGeminiConversation = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { id: number }, + TContext + >; + request?: SecondParameter; +}): UseMutationResult< + Awaited>, + TError, + { id: number }, + TContext +> => { + return useMutation(getDeleteGeminiConversationMutationOptions(options)); +}; + +/** + * @summary List messages in a conversation + */ +export const getListGeminiMessagesUrl = (id: number) => { + return `/api/gemini/conversations/${id}/messages`; +}; + +export const listGeminiMessages = async ( + id: number, + options?: RequestInit, +): Promise => { + return customFetch(getListGeminiMessagesUrl(id), { + ...options, + method: "GET", + }); +}; + +export const getListGeminiMessagesQueryKey = (id: number) => { + return [`/api/gemini/conversations/${id}/messages`] as const; +}; + +export const getListGeminiMessagesQueryOptions = < + TData = Awaited>, + TError = ErrorType, +>( + id: number, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getListGeminiMessagesQueryKey(id); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => listGeminiMessages(id, { signal, ...requestOptions }); + + return { + queryKey, + queryFn, + enabled: !!id, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type ListGeminiMessagesQueryResult = NonNullable< + Awaited> +>; +export type ListGeminiMessagesQueryError = ErrorType; + +/** + * @summary List messages in a conversation + */ + +export function useListGeminiMessages< + TData = Awaited>, + TError = ErrorType, +>( + id: number, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getListGeminiMessagesQueryOptions(id, options); + + const query = useQuery(queryOptions) as UseQueryResult & { + queryKey: QueryKey; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * @summary Send a message and receive an AI response (SSE stream) + */ +export const getSendGeminiMessageUrl = (id: number) => { + return `/api/gemini/conversations/${id}/messages`; +}; + +export const sendGeminiMessage = async ( + id: number, + sendGeminiMessageBody: SendGeminiMessageBody, + options?: RequestInit, +): Promise => { + return customFetch(getSendGeminiMessageUrl(id), { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(sendGeminiMessageBody), + }); +}; + +export const getSendGeminiMessageMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { id: number; data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { id: number; data: BodyType }, + TContext +> => { + const mutationKey = ["sendGeminiMessage"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { id: number; data: BodyType } + > = (props) => { + const { id, data } = props ?? {}; + + return sendGeminiMessage(id, data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type SendGeminiMessageMutationResult = NonNullable< + Awaited> +>; +export type SendGeminiMessageMutationBody = BodyType; +export type SendGeminiMessageMutationError = ErrorType; + +/** + * @summary Send a message and receive an AI response (SSE stream) + */ +export const useSendGeminiMessage = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { id: number; data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationResult< + Awaited>, + TError, + { id: number; data: BodyType }, + TContext +> => { + return useMutation(getSendGeminiMessageMutationOptions(options)); +}; + +/** + * @summary Generate an image from a text prompt + */ +export const getGenerateGeminiImageUrl = () => { + return `/api/gemini/generate-image`; +}; + +export const generateGeminiImage = async ( + generateGeminiImageBody: GenerateGeminiImageBody, + options?: RequestInit, +): Promise => { + return customFetch(getGenerateGeminiImageUrl(), { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(generateGeminiImageBody), + }); +}; + +export const getGenerateGeminiImageMutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ["generateGeminiImage"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return generateGeminiImage(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type GenerateGeminiImageMutationResult = NonNullable< + Awaited> +>; +export type GenerateGeminiImageMutationBody = BodyType; +export type GenerateGeminiImageMutationError = ErrorType; + +/** + * @summary Generate an image from a text prompt + */ +export const useGenerateGeminiImage = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getGenerateGeminiImageMutationOptions(options)); +}; diff --git a/lib/api-spec/openapi.yaml b/lib/api-spec/openapi.yaml index 8371f9f..c1ef014 100644 --- a/lib/api-spec/openapi.yaml +++ b/lib/api-spec/openapi.yaml @@ -16,6 +16,8 @@ tags: description: Pre-funded session balance mode (Mode 2 -- pay once, run many) - name: demo description: Free demo endpoint (rate-limited) + - name: gemini + description: Gemini AI chat and image operations paths: /healthz: get: @@ -385,6 +387,139 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorResponse" + /gemini/conversations: + get: + operationId: listGeminiConversations + tags: [gemini] + summary: List all conversations + responses: + "200": + description: List of conversations + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/GeminiConversation" + post: + operationId: createGeminiConversation + tags: [gemini] + summary: Create a new conversation + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateGeminiConversationBody" + responses: + "201": + description: Created conversation + content: + application/json: + schema: + $ref: "#/components/schemas/GeminiConversation" + /gemini/conversations/{id}: + get: + operationId: getGeminiConversation + tags: [gemini] + summary: Get conversation with messages + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + "200": + description: Conversation with messages + content: + application/json: + schema: + $ref: "#/components/schemas/GeminiConversationWithMessages" + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/GeminiError" + delete: + operationId: deleteGeminiConversation + tags: [gemini] + summary: Delete a conversation + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + "204": + description: Deleted + "404": + description: Not found + content: + application/json: + schema: + $ref: "#/components/schemas/GeminiError" + /gemini/conversations/{id}/messages: + get: + operationId: listGeminiMessages + tags: [gemini] + summary: List messages in a conversation + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + "200": + description: List of messages + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/GeminiMessage" + post: + operationId: sendGeminiMessage + tags: [gemini] + summary: Send a message and receive an AI response (SSE stream) + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SendGeminiMessageBody" + responses: + "200": + description: SSE stream of assistant response chunks + content: + text/event-stream: {} + /gemini/generate-image: + post: + operationId: generateGeminiImage + tags: [gemini] + summary: Generate an image from a text prompt + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/GenerateGeminiImageBody" + responses: + "200": + description: Generated image + content: + application/json: + schema: + $ref: "#/components/schemas/GenerateGeminiImageResponse" components: schemas: HealthStatus: @@ -663,8 +798,84 @@ components: properties: result: type: string + GeminiConversation: + type: object + required: [id, title, createdAt] + properties: + id: + type: integer + title: + type: string + createdAt: + type: string + format: date-time + GeminiMessage: + type: object + required: [id, conversationId, role, content, createdAt] + properties: + id: + type: integer + conversationId: + type: integer + role: + type: string + content: + type: string + createdAt: + type: string + format: date-time + GeminiConversationWithMessages: + type: object + required: [id, title, createdAt, messages] + properties: + id: + type: integer + title: + type: string + createdAt: + type: string + format: date-time + messages: + type: array + items: + $ref: "#/components/schemas/GeminiMessage" + CreateGeminiConversationBody: + type: object + required: [title] + properties: + title: + type: string + SendGeminiMessageBody: + type: object + required: [content] + properties: + content: + type: string + model: + type: string + description: "Gemini model override (default: gemini-3-flash-preview)" + GenerateGeminiImageBody: + type: object + required: [prompt] + properties: + prompt: + type: string + GenerateGeminiImageResponse: + type: object + required: [b64_json, mimeType] + properties: + b64_json: + type: string + mimeType: + type: string + GeminiError: + type: object + required: [error] + properties: + error: + type: string securitySchemes: sessionMacaroon: type: http scheme: bearer - description: Session macaroon issued when a session activates. Pass as `Authorization: Bearer `. + description: "Session macaroon issued when a session activates. Pass as `Authorization: Bearer `." diff --git a/lib/api-spec/orval.config.cjs b/lib/api-spec/orval.config.cjs new file mode 100644 index 0000000..8005d4a --- /dev/null +++ b/lib/api-spec/orval.config.cjs @@ -0,0 +1,66 @@ +const path = require("path"); + +const root = path.resolve(__dirname, "..", ".."); +const apiClientReactSrc = path.resolve(root, "lib", "api-client-react", "src"); +const apiZodSrc = path.resolve(root, "lib", "api-zod", "src"); + +const titleTransformer = (config) => { + config.info ??= {}; + config.info.title = "Api"; + return config; +}; + +module.exports = { + "api-client-react": { + input: { + target: path.resolve(__dirname, "./openapi.yaml"), + override: { + transformer: titleTransformer, + }, + }, + output: { + workspace: apiClientReactSrc, + target: "generated", + client: "react-query", + mode: "split", + baseUrl: "/api", + clean: true, + prettier: true, + override: { + fetch: { + includeHttpResponseReturnType: false, + }, + mutator: { + path: path.resolve(apiClientReactSrc, "custom-fetch.ts"), + name: "customFetch", + }, + }, + }, + }, + zod: { + input: { + target: path.resolve(__dirname, "./openapi.yaml"), + override: { + transformer: titleTransformer, + }, + }, + output: { + workspace: apiZodSrc, + client: "zod", + target: "generated", + schemas: { path: "generated/types", type: "typescript" }, + mode: "split", + clean: true, + prettier: true, + override: { + zod: { + coerce: { + query: ["boolean", "number", "string"], + param: ["boolean", "number", "string"], + }, + }, + useDates: true, + }, + }, + }, +}; diff --git a/lib/api-spec/package.json b/lib/api-spec/package.json index e55de91..ee8aa85 100644 --- a/lib/api-spec/package.json +++ b/lib/api-spec/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { - "codegen": "orval --config ./orval.config.ts" + "codegen": "orval --config ./orval.config.cjs" }, "devDependencies": { "orval": "^8.5.2" diff --git a/lib/api-zod/src/generated/api.ts b/lib/api-zod/src/generated/api.ts index 4ce044a..3705344 100644 --- a/lib/api-zod/src/generated/api.ts +++ b/lib/api-zod/src/generated/api.ts @@ -1,45 +1,396 @@ -import { z } from "zod"; +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ +import * as zod from "zod"; -export const HealthCheckResponse = z.object({ - status: z.string(), +/** + * Returns server health status + * @summary Health check + */ +export const HealthCheckResponse = zod.object({ + status: zod.string(), }); -export const ErrorResponse = z.object({ - error: z.string(), +/** + * Accepts a request, creates a job row, and issues an eval fee Lightning invoice. + * @summary Create a new agent job + */ + +export const CreateJobBody = zod.object({ + request: zod.string().min(1), }); -export const CreateJobBody = z.object({ - request: z.string().min(1).max(500), +/** + * Returns current job state. Automatically advances the state machine when a pending invoice is found to be paid. + * @summary Get job status + */ +export const GetJobParams = zod.object({ + id: zod.coerce.string(), }); -export const GetJobParams = z.object({ - id: z.string(), +export const GetJobResponse = zod.object({ + jobId: zod.string(), + state: zod.enum([ + "awaiting_eval_payment", + "evaluating", + "rejected", + "awaiting_work_payment", + "executing", + "complete", + "failed", + ]), + createdAt: zod + .date() + .describe("ISO 8601 timestamp of job creation (always present)"), + completedAt: zod + .date() + .nullish() + .describe( + "ISO 8601 timestamp of job completion; null when not yet complete", + ), + evalInvoice: zod + .object({ + paymentRequest: zod.string(), + amountSats: zod.number(), + }) + .optional(), + workInvoice: zod + .object({ + paymentRequest: zod.string(), + amountSats: zod.number(), + }) + .optional(), + pricingBreakdown: zod + .object({ + estimatedCostUsd: zod + .number() + .optional() + .describe( + "Total estimated cost in USD (token cost + DO infra + margin)", + ), + marginPct: zod + .number() + .optional() + .describe("Originator margin percentage applied"), + btcPriceUsd: zod + .number() + .optional() + .describe("BTC\/USD spot price used to convert the invoice to sats"), + }) + .optional() + .describe( + "Cost breakdown shown with the work invoice (estimations at invoice-creation time)", + ), + reason: zod.string().optional(), + result: zod.string().optional(), + costLedger: zod + .object({ + actualInputTokens: zod.number().optional(), + actualOutputTokens: zod.number().optional(), + totalTokens: zod + .number() + .optional() + .describe("Sum of actualInputTokens + actualOutputTokens"), + actualCostUsd: zod + .number() + .optional() + .describe("Raw Anthropic token cost (no infra, no margin)"), + actualChargeUsd: zod + .number() + .optional() + .describe( + "What we honestly charged in USD (actual token cost + DO infra + margin)", + ), + estimatedCostUsd: zod + .number() + .optional() + .describe("Original estimate used to create the work invoice"), + actualAmountSats: zod + .number() + .optional() + .describe( + "Honest sats charge (actual cost converted at the locked BTC price)", + ), + workAmountSats: zod + .number() + .optional() + .describe("Amount the user originally paid in sats"), + refundAmountSats: zod + .number() + .optional() + .describe( + "Sats owed back to the user (workAmountSats - actualAmountSats, >= 0)", + ), + refundState: zod + .enum(["not_applicable", "pending", "paid"]) + .optional() + .describe("Lifecycle of the refund for this job"), + marginPct: zod.number().optional(), + btcPriceUsd: zod + .number() + .optional() + .describe("BTC\/USD price locked at invoice creation time"), + }) + .optional() + .describe("Honest post-work accounting stored after the job completes"), + errorMessage: zod.string().optional(), }); -export const GetJobRefundParams = z.object({ - id: z.string(), +/** + * After a job completes, if the actual cost (tokens used + infra + margin) was +less than the work invoice amount, the difference is owed back to the user. +Submit a BOLT11 invoice for exactly `refundAmountSats` to receive the payment. +Idempotent: returns 409 if already paid or if no refund is owed. + + * @summary Claim a refund for overpayment + */ +export const ClaimRefundParams = zod.object({ + id: zod.coerce.string(), }); -export const ClaimRefundBody = z.object({ - paymentRequest: z.string(), +export const ClaimRefundBody = zod.object({ + invoice: zod.string().describe("BOLT11 invoice for exactly refundAmountSats"), }); -export const RunDemoQueryParams = z.object({ - request: z.string().min(1), +export const ClaimRefundResponse = zod.object({ + ok: zod.boolean(), + refundAmountSats: zod.number(), + paymentHash: zod.string(), + message: zod.string(), }); -export const CreateSessionBody = z.object({ - amount_sats: z.number().int().min(100).max(10000), +/** + * Opens a new session. Pay the returned Lightning invoice to activate it. +Once active, use the `macaroon` from GET /sessions/:id to authenticate requests. +Deposits: 100–10,000 sats. Sessions expire after 24 h of inactivity. + + * @summary Create a pre-funded session + */ +export const createSessionBodyAmountSatsMin = 100; +export const createSessionBodyAmountSatsMax = 10000; + +export const CreateSessionBody = zod.object({ + amount_sats: zod + .number() + .min(createSessionBodyAmountSatsMin) + .max(createSessionBodyAmountSatsMax) + .describe("Deposit amount (100–10,000 sats)"), }); -export const GetSessionParams = z.object({ - id: z.string(), +/** + * Returns current state, balance, and pending invoice info. Auto-advances on payment. + * @summary Get session status + */ +export const GetSessionParams = zod.object({ + id: zod.coerce.string(), }); -export const SubmitSessionRequestBody = z.object({ - request: z.string().min(1), +export const GetSessionResponse = zod.object({ + sessionId: zod.string(), + state: zod.enum(["awaiting_payment", "active", "paused", "expired"]), + balanceSats: zod.number(), + minimumBalanceSats: zod.number().optional(), + macaroon: zod + .string() + .optional() + .describe( + "Bearer token for authenticating requests; present when active or paused", + ), + expiresAt: zod.date().optional(), + invoice: zod + .object({ + paymentRequest: zod.string().optional(), + amountSats: zod.number().optional(), + paymentHash: zod + .string() + .optional() + .describe("Only present in stub\/dev mode"), + }) + .optional() + .describe("Present when state is awaiting_payment"), + pendingTopup: zod + .object({ + paymentRequest: zod.string().optional(), + amountSats: zod.number().optional(), + paymentHash: zod + .string() + .optional() + .describe("Only present in stub\/dev mode"), + }) + .optional() + .describe("Present when a topup invoice is outstanding"), }); -export const TopupSessionBody = z.object({ - amount_sats: z.number().int().min(100).max(10000), +/** + * Runs eval + work and debits the actual compute cost from the session balance. +Rejected requests still incur a small eval fee. Requires `Authorization: Bearer `. + + * @summary Submit a request against a session balance + */ +export const SubmitSessionRequestParams = zod.object({ + id: zod.coerce.string(), +}); + +export const SubmitSessionRequestBody = zod.object({ + request: zod.string().min(1), +}); + +export const SubmitSessionRequestResponse = zod.object({ + requestId: zod.string(), + state: zod.enum(["complete", "rejected", "failed"]), + result: zod.string().optional(), + reason: zod.string().optional(), + errorMessage: zod.string().optional(), + debitedSats: zod.number(), + balanceRemaining: zod.number(), + cost: zod + .object({ + evalSats: zod.number().optional(), + workSats: zod.number().optional(), + totalSats: zod.number().optional(), + btcPriceUsd: zod.number().optional(), + }) + .optional(), +}); + +/** + * Creates a new Lightning invoice to top up the session balance. +Only one pending topup at a time. Paying it resumes a paused session. + + * @summary Add sats to a session + */ +export const TopupSessionParams = zod.object({ + id: zod.coerce.string(), +}); + +export const topupSessionBodyAmountSatsMin = 100; +export const topupSessionBodyAmountSatsMax = 10000; + +export const TopupSessionBody = zod.object({ + amount_sats: zod + .number() + .min(topupSessionBodyAmountSatsMin) + .max(topupSessionBodyAmountSatsMax) + .describe("Deposit amount (100–10,000 sats)"), +}); + +export const TopupSessionResponse = zod.object({ + sessionId: zod.string(), + topup: zod.object({ + paymentRequest: zod.string().optional(), + amountSats: zod.number().optional(), + paymentHash: zod + .string() + .optional() + .describe("Only present in stub\/dev mode"), + }), +}); + +/** + * Runs the agent without payment. Limited to 5 requests per IP per hour. + * @summary Free demo (rate-limited) + */ +export const RunDemoQueryParams = zod.object({ + request: zod.coerce.string(), +}); + +export const RunDemoResponse = zod.object({ + result: zod.string(), +}); + +/** + * @summary List all conversations + */ +export const ListGeminiConversationsResponseItem = zod.object({ + id: zod.number(), + title: zod.string(), + createdAt: zod.date(), +}); +export const ListGeminiConversationsResponse = zod.array( + ListGeminiConversationsResponseItem, +); + +/** + * @summary Create a new conversation + */ +export const CreateGeminiConversationBody = zod.object({ + title: zod.string(), +}); + +/** + * @summary Get conversation with messages + */ +export const GetGeminiConversationParams = zod.object({ + id: zod.coerce.number(), +}); + +export const GetGeminiConversationResponse = zod.object({ + id: zod.number(), + title: zod.string(), + createdAt: zod.date(), + messages: zod.array( + zod.object({ + id: zod.number(), + conversationId: zod.number(), + role: zod.string(), + content: zod.string(), + createdAt: zod.date(), + }), + ), +}); + +/** + * @summary Delete a conversation + */ +export const DeleteGeminiConversationParams = zod.object({ + id: zod.coerce.number(), +}); + +/** + * @summary List messages in a conversation + */ +export const ListGeminiMessagesParams = zod.object({ + id: zod.coerce.number(), +}); + +export const ListGeminiMessagesResponseItem = zod.object({ + id: zod.number(), + conversationId: zod.number(), + role: zod.string(), + content: zod.string(), + createdAt: zod.date(), +}); +export const ListGeminiMessagesResponse = zod.array( + ListGeminiMessagesResponseItem, +); + +/** + * @summary Send a message and receive an AI response (SSE stream) + */ +export const SendGeminiMessageParams = zod.object({ + id: zod.coerce.number(), +}); + +export const SendGeminiMessageBody = zod.object({ + content: zod.string(), + model: zod + .string() + .optional() + .describe("Gemini model override (default: gemini-3-flash-preview)"), +}); + +/** + * @summary Generate an image from a text prompt + */ +export const GenerateGeminiImageBody = zod.object({ + prompt: zod.string(), +}); + +export const GenerateGeminiImageResponse = zod.object({ + b64_json: zod.string(), + mimeType: zod.string(), }); diff --git a/lib/api-zod/src/generated/types.ts b/lib/api-zod/src/generated/types.ts deleted file mode 100644 index 3776f91..0000000 --- a/lib/api-zod/src/generated/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -// TypeScript types derived from Zod schemas in ./api -// All schemas and their inferred types are exported from ./api via src/index.ts -export {}; diff --git a/lib/api-zod/src/generated/types/claimRefundRequest.ts b/lib/api-zod/src/generated/types/claimRefundRequest.ts new file mode 100644 index 0000000..cdd7eae --- /dev/null +++ b/lib/api-zod/src/generated/types/claimRefundRequest.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface ClaimRefundRequest { + /** BOLT11 invoice for exactly refundAmountSats */ + invoice: string; +} diff --git a/lib/api-zod/src/generated/types/claimRefundResponse.ts b/lib/api-zod/src/generated/types/claimRefundResponse.ts new file mode 100644 index 0000000..8643ece --- /dev/null +++ b/lib/api-zod/src/generated/types/claimRefundResponse.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface ClaimRefundResponse { + ok: boolean; + refundAmountSats: number; + paymentHash: string; + message: string; +} diff --git a/lib/api-zod/src/generated/types/costLedger.ts b/lib/api-zod/src/generated/types/costLedger.ts new file mode 100644 index 0000000..6b2e39e --- /dev/null +++ b/lib/api-zod/src/generated/types/costLedger.ts @@ -0,0 +1,35 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ +import type { CostLedgerRefundState } from "./costLedgerRefundState"; + +/** + * Honest post-work accounting stored after the job completes + */ +export interface CostLedger { + actualInputTokens?: number; + actualOutputTokens?: number; + /** Sum of actualInputTokens + actualOutputTokens */ + totalTokens?: number; + /** Raw Anthropic token cost (no infra, no margin) */ + actualCostUsd?: number; + /** What we honestly charged in USD (actual token cost + DO infra + margin) */ + actualChargeUsd?: number; + /** Original estimate used to create the work invoice */ + estimatedCostUsd?: number; + /** Honest sats charge (actual cost converted at the locked BTC price) */ + actualAmountSats?: number; + /** Amount the user originally paid in sats */ + workAmountSats?: number; + /** Sats owed back to the user (workAmountSats - actualAmountSats, >= 0) */ + refundAmountSats?: number; + /** Lifecycle of the refund for this job */ + refundState?: CostLedgerRefundState; + marginPct?: number; + /** BTC/USD price locked at invoice creation time */ + btcPriceUsd?: number; +} diff --git a/lib/api-zod/src/generated/types/costLedgerRefundState.ts b/lib/api-zod/src/generated/types/costLedgerRefundState.ts new file mode 100644 index 0000000..7f44b92 --- /dev/null +++ b/lib/api-zod/src/generated/types/costLedgerRefundState.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +/** + * Lifecycle of the refund for this job + */ +export type CostLedgerRefundState = + (typeof CostLedgerRefundState)[keyof typeof CostLedgerRefundState]; + +export const CostLedgerRefundState = { + not_applicable: "not_applicable", + pending: "pending", + paid: "paid", +} as const; diff --git a/lib/api-zod/src/generated/types/createGeminiConversationBody.ts b/lib/api-zod/src/generated/types/createGeminiConversationBody.ts new file mode 100644 index 0000000..e24407e --- /dev/null +++ b/lib/api-zod/src/generated/types/createGeminiConversationBody.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface CreateGeminiConversationBody { + title: string; +} diff --git a/lib/api-zod/src/generated/types/createJobRequest.ts b/lib/api-zod/src/generated/types/createJobRequest.ts new file mode 100644 index 0000000..15b1d8f --- /dev/null +++ b/lib/api-zod/src/generated/types/createJobRequest.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface CreateJobRequest { + /** @minLength 1 */ + request: string; +} diff --git a/lib/api-zod/src/generated/types/createJobResponse.ts b/lib/api-zod/src/generated/types/createJobResponse.ts new file mode 100644 index 0000000..56a6c04 --- /dev/null +++ b/lib/api-zod/src/generated/types/createJobResponse.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ +import type { InvoiceInfo } from "./invoiceInfo"; + +export interface CreateJobResponse { + jobId: string; + /** ISO 8601 timestamp of job creation */ + createdAt: Date; + evalInvoice: InvoiceInfo; +} diff --git a/lib/api-zod/src/generated/types/createSessionRequest.ts b/lib/api-zod/src/generated/types/createSessionRequest.ts new file mode 100644 index 0000000..6bfcca2 --- /dev/null +++ b/lib/api-zod/src/generated/types/createSessionRequest.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface CreateSessionRequest { + /** + * Deposit amount (100–10,000 sats) + * @minimum 100 + * @maximum 10000 + */ + amount_sats: number; +} diff --git a/lib/api-zod/src/generated/types/createSessionResponse.ts b/lib/api-zod/src/generated/types/createSessionResponse.ts new file mode 100644 index 0000000..83d9621 --- /dev/null +++ b/lib/api-zod/src/generated/types/createSessionResponse.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ +import type { SessionInvoiceInfo } from "./sessionInvoiceInfo"; +import type { SessionState } from "./sessionState"; + +export interface CreateSessionResponse { + sessionId: string; + state: SessionState; + invoice: SessionInvoiceInfo; +} diff --git a/lib/api-zod/src/generated/types/demoResponse.ts b/lib/api-zod/src/generated/types/demoResponse.ts new file mode 100644 index 0000000..acceb16 --- /dev/null +++ b/lib/api-zod/src/generated/types/demoResponse.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface DemoResponse { + result: string; +} diff --git a/lib/api-zod/src/generated/types/errorResponse.ts b/lib/api-zod/src/generated/types/errorResponse.ts new file mode 100644 index 0000000..8c46a3a --- /dev/null +++ b/lib/api-zod/src/generated/types/errorResponse.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface ErrorResponse { + error: string; +} diff --git a/lib/api-zod/src/generated/types/geminiConversation.ts b/lib/api-zod/src/generated/types/geminiConversation.ts new file mode 100644 index 0000000..98f65fc --- /dev/null +++ b/lib/api-zod/src/generated/types/geminiConversation.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface GeminiConversation { + id: number; + title: string; + createdAt: Date; +} diff --git a/lib/api-zod/src/generated/types/geminiConversationWithMessages.ts b/lib/api-zod/src/generated/types/geminiConversationWithMessages.ts new file mode 100644 index 0000000..2c937a6 --- /dev/null +++ b/lib/api-zod/src/generated/types/geminiConversationWithMessages.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ +import type { GeminiMessage } from "./geminiMessage"; + +export interface GeminiConversationWithMessages { + id: number; + title: string; + createdAt: Date; + messages: GeminiMessage[]; +} diff --git a/lib/api-zod/src/generated/types/geminiError.ts b/lib/api-zod/src/generated/types/geminiError.ts new file mode 100644 index 0000000..b76c8f7 --- /dev/null +++ b/lib/api-zod/src/generated/types/geminiError.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface GeminiError { + error: string; +} diff --git a/lib/api-zod/src/generated/types/geminiMessage.ts b/lib/api-zod/src/generated/types/geminiMessage.ts new file mode 100644 index 0000000..387705b --- /dev/null +++ b/lib/api-zod/src/generated/types/geminiMessage.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface GeminiMessage { + id: number; + conversationId: number; + role: string; + content: string; + createdAt: Date; +} diff --git a/lib/api-zod/src/generated/types/generateGeminiImageBody.ts b/lib/api-zod/src/generated/types/generateGeminiImageBody.ts new file mode 100644 index 0000000..ef03f09 --- /dev/null +++ b/lib/api-zod/src/generated/types/generateGeminiImageBody.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface GenerateGeminiImageBody { + prompt: string; +} diff --git a/lib/api-zod/src/generated/types/generateGeminiImageResponse.ts b/lib/api-zod/src/generated/types/generateGeminiImageResponse.ts new file mode 100644 index 0000000..bc31c0c --- /dev/null +++ b/lib/api-zod/src/generated/types/generateGeminiImageResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface GenerateGeminiImageResponse { + b64_json: string; + mimeType: string; +} diff --git a/lib/api-zod/src/generated/types/healthStatus.ts b/lib/api-zod/src/generated/types/healthStatus.ts new file mode 100644 index 0000000..f1ad88c --- /dev/null +++ b/lib/api-zod/src/generated/types/healthStatus.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface HealthStatus { + status: string; +} diff --git a/lib/api-zod/src/generated/types/index.ts b/lib/api-zod/src/generated/types/index.ts new file mode 100644 index 0000000..1f2fc54 --- /dev/null +++ b/lib/api-zod/src/generated/types/index.ts @@ -0,0 +1,40 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export * from "./claimRefundRequest"; +export * from "./claimRefundResponse"; +export * from "./costLedger"; +export * from "./costLedgerRefundState"; +export * from "./createGeminiConversationBody"; +export * from "./createJobRequest"; +export * from "./createJobResponse"; +export * from "./createSessionRequest"; +export * from "./createSessionResponse"; +export * from "./demoResponse"; +export * from "./errorResponse"; +export * from "./geminiConversation"; +export * from "./geminiConversationWithMessages"; +export * from "./geminiError"; +export * from "./geminiMessage"; +export * from "./generateGeminiImageBody"; +export * from "./generateGeminiImageResponse"; +export * from "./healthStatus"; +export * from "./invoiceInfo"; +export * from "./jobState"; +export * from "./jobStatusResponse"; +export * from "./pricingBreakdown"; +export * from "./runDemoParams"; +export * from "./sendGeminiMessageBody"; +export * from "./sessionCostBreakdown"; +export * from "./sessionInvoiceInfo"; +export * from "./sessionRequestBody"; +export * from "./sessionRequestResponse"; +export * from "./sessionRequestResponseState"; +export * from "./sessionState"; +export * from "./sessionStatusResponse"; +export * from "./topupSessionResponse"; diff --git a/lib/api-zod/src/generated/types/invoiceInfo.ts b/lib/api-zod/src/generated/types/invoiceInfo.ts new file mode 100644 index 0000000..1370753 --- /dev/null +++ b/lib/api-zod/src/generated/types/invoiceInfo.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface InvoiceInfo { + paymentRequest: string; + amountSats: number; +} diff --git a/lib/api-zod/src/generated/types/jobState.ts b/lib/api-zod/src/generated/types/jobState.ts new file mode 100644 index 0000000..b4c113c --- /dev/null +++ b/lib/api-zod/src/generated/types/jobState.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export type JobState = (typeof JobState)[keyof typeof JobState]; + +export const JobState = { + awaiting_eval_payment: "awaiting_eval_payment", + evaluating: "evaluating", + rejected: "rejected", + awaiting_work_payment: "awaiting_work_payment", + executing: "executing", + complete: "complete", + failed: "failed", +} as const; diff --git a/lib/api-zod/src/generated/types/jobStatusResponse.ts b/lib/api-zod/src/generated/types/jobStatusResponse.ts new file mode 100644 index 0000000..0d15ac7 --- /dev/null +++ b/lib/api-zod/src/generated/types/jobStatusResponse.ts @@ -0,0 +1,27 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ +import type { CostLedger } from "./costLedger"; +import type { InvoiceInfo } from "./invoiceInfo"; +import type { JobState } from "./jobState"; +import type { PricingBreakdown } from "./pricingBreakdown"; + +export interface JobStatusResponse { + jobId: string; + state: JobState; + /** ISO 8601 timestamp of job creation (always present) */ + createdAt: Date; + /** ISO 8601 timestamp of job completion; null when not yet complete */ + completedAt?: Date | null; + evalInvoice?: InvoiceInfo; + workInvoice?: InvoiceInfo; + pricingBreakdown?: PricingBreakdown; + reason?: string; + result?: string; + costLedger?: CostLedger; + errorMessage?: string; +} diff --git a/lib/api-zod/src/generated/types/pricingBreakdown.ts b/lib/api-zod/src/generated/types/pricingBreakdown.ts new file mode 100644 index 0000000..4c1ed4c --- /dev/null +++ b/lib/api-zod/src/generated/types/pricingBreakdown.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +/** + * Cost breakdown shown with the work invoice (estimations at invoice-creation time) + */ +export interface PricingBreakdown { + /** Total estimated cost in USD (token cost + DO infra + margin) */ + estimatedCostUsd?: number; + /** Originator margin percentage applied */ + marginPct?: number; + /** BTC/USD spot price used to convert the invoice to sats */ + btcPriceUsd?: number; +} diff --git a/lib/api-zod/src/generated/types/runDemoParams.ts b/lib/api-zod/src/generated/types/runDemoParams.ts new file mode 100644 index 0000000..e7bbdac --- /dev/null +++ b/lib/api-zod/src/generated/types/runDemoParams.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export type RunDemoParams = { + request: string; +}; diff --git a/lib/api-zod/src/generated/types/sendGeminiMessageBody.ts b/lib/api-zod/src/generated/types/sendGeminiMessageBody.ts new file mode 100644 index 0000000..ed36c0e --- /dev/null +++ b/lib/api-zod/src/generated/types/sendGeminiMessageBody.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface SendGeminiMessageBody { + content: string; + /** Gemini model override (default: gemini-3-flash-preview) */ + model?: string; +} diff --git a/lib/api-zod/src/generated/types/sessionCostBreakdown.ts b/lib/api-zod/src/generated/types/sessionCostBreakdown.ts new file mode 100644 index 0000000..44414fe --- /dev/null +++ b/lib/api-zod/src/generated/types/sessionCostBreakdown.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface SessionCostBreakdown { + evalSats?: number; + workSats?: number; + totalSats?: number; + btcPriceUsd?: number; +} diff --git a/lib/api-zod/src/generated/types/sessionInvoiceInfo.ts b/lib/api-zod/src/generated/types/sessionInvoiceInfo.ts new file mode 100644 index 0000000..131df00 --- /dev/null +++ b/lib/api-zod/src/generated/types/sessionInvoiceInfo.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface SessionInvoiceInfo { + paymentRequest?: string; + amountSats?: number; + /** Only present in stub/dev mode */ + paymentHash?: string; +} diff --git a/lib/api-zod/src/generated/types/sessionRequestBody.ts b/lib/api-zod/src/generated/types/sessionRequestBody.ts new file mode 100644 index 0000000..050fe23 --- /dev/null +++ b/lib/api-zod/src/generated/types/sessionRequestBody.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export interface SessionRequestBody { + /** @minLength 1 */ + request: string; +} diff --git a/lib/api-zod/src/generated/types/sessionRequestResponse.ts b/lib/api-zod/src/generated/types/sessionRequestResponse.ts new file mode 100644 index 0000000..2319d56 --- /dev/null +++ b/lib/api-zod/src/generated/types/sessionRequestResponse.ts @@ -0,0 +1,20 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ +import type { SessionCostBreakdown } from "./sessionCostBreakdown"; +import type { SessionRequestResponseState } from "./sessionRequestResponseState"; + +export interface SessionRequestResponse { + requestId: string; + state: SessionRequestResponseState; + result?: string; + reason?: string; + errorMessage?: string; + debitedSats: number; + balanceRemaining: number; + cost?: SessionCostBreakdown; +} diff --git a/lib/api-zod/src/generated/types/sessionRequestResponseState.ts b/lib/api-zod/src/generated/types/sessionRequestResponseState.ts new file mode 100644 index 0000000..af49569 --- /dev/null +++ b/lib/api-zod/src/generated/types/sessionRequestResponseState.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export type SessionRequestResponseState = + (typeof SessionRequestResponseState)[keyof typeof SessionRequestResponseState]; + +export const SessionRequestResponseState = { + complete: "complete", + rejected: "rejected", + failed: "failed", +} as const; diff --git a/lib/api-zod/src/generated/types/sessionState.ts b/lib/api-zod/src/generated/types/sessionState.ts new file mode 100644 index 0000000..3daf9b2 --- /dev/null +++ b/lib/api-zod/src/generated/types/sessionState.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ + +export type SessionState = (typeof SessionState)[keyof typeof SessionState]; + +export const SessionState = { + awaiting_payment: "awaiting_payment", + active: "active", + paused: "paused", + expired: "expired", +} as const; diff --git a/lib/api-zod/src/generated/types/sessionStatusResponse.ts b/lib/api-zod/src/generated/types/sessionStatusResponse.ts new file mode 100644 index 0000000..782384f --- /dev/null +++ b/lib/api-zod/src/generated/types/sessionStatusResponse.ts @@ -0,0 +1,23 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ +import type { SessionInvoiceInfo } from "./sessionInvoiceInfo"; +import type { SessionState } from "./sessionState"; + +export interface SessionStatusResponse { + sessionId: string; + state: SessionState; + balanceSats: number; + minimumBalanceSats?: number; + /** Bearer token for authenticating requests; present when active or paused */ + macaroon?: string; + expiresAt?: Date; + /** Present when state is awaiting_payment */ + invoice?: SessionInvoiceInfo; + /** Present when a topup invoice is outstanding */ + pendingTopup?: SessionInvoiceInfo; +} diff --git a/lib/api-zod/src/generated/types/topupSessionResponse.ts b/lib/api-zod/src/generated/types/topupSessionResponse.ts new file mode 100644 index 0000000..3fef02e --- /dev/null +++ b/lib/api-zod/src/generated/types/topupSessionResponse.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * Api + * API specification + * OpenAPI spec version: 0.1.0 + */ +import type { SessionInvoiceInfo } from "./sessionInvoiceInfo"; + +export interface TopupSessionResponse { + sessionId: string; + topup: SessionInvoiceInfo; +} diff --git a/lib/integrations-gemini-ai/package.json b/lib/integrations-gemini-ai/package.json new file mode 100644 index 0000000..bfa408c --- /dev/null +++ b/lib/integrations-gemini-ai/package.json @@ -0,0 +1,16 @@ +{ + "name": "@workspace/integrations-gemini-ai", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts", + "./batch": "./src/batch/index.ts", + "./image": "./src/image/index.ts" + }, + "dependencies": { + "@google/genai": "^1.44.0", + "p-limit": "^7.3.0", + "p-retry": "^7.1.1" + } +} diff --git a/lib/integrations-gemini-ai/src/batch/index.ts b/lib/integrations-gemini-ai/src/batch/index.ts new file mode 100644 index 0000000..f84c2c1 --- /dev/null +++ b/lib/integrations-gemini-ai/src/batch/index.ts @@ -0,0 +1,6 @@ +export { + batchProcess, + batchProcessWithSSE, + isRateLimitError, + type BatchOptions, +} from "./utils"; diff --git a/lib/integrations-gemini-ai/src/batch/utils.ts b/lib/integrations-gemini-ai/src/batch/utils.ts new file mode 100644 index 0000000..1c94578 --- /dev/null +++ b/lib/integrations-gemini-ai/src/batch/utils.ts @@ -0,0 +1,139 @@ +import pLimit from "p-limit"; +import pRetry from "p-retry"; + +/** + * Batch Processing Utilities + * + * Generic batch processing with built-in rate limiting and automatic retries. + * Use for any task that requires processing multiple items through an LLM or external API. + * + * USAGE: + * ```typescript + * import { batchProcess } from "@workspace/integrations-gemini-ai/batch"; + * import { ai } from "@workspace/integrations-gemini-ai"; + * + * const results = await batchProcess( + * artworks, + * async (artwork) => { + * const response = await ai.models.generateContent({ + * model: "gemini-2.5-flash", + * contents: [{ role: "user", parts: [{ text: `Categorize: ${artwork.name}` }] }], + * config: { responseMimeType: "application/json" }, + * }); + * return JSON.parse(response.text ?? "{}"); + * }, + * { concurrency: 2, retries: 5 } + * ); + * ``` + */ + +export interface BatchOptions { + concurrency?: number; + retries?: number; + minTimeout?: number; + maxTimeout?: number; + onProgress?: (completed: number, total: number, item: unknown) => void; +} + +export function isRateLimitError(error: unknown): boolean { + const errorMsg = error instanceof Error ? error.message : String(error); + return ( + errorMsg.includes("429") || + errorMsg.includes("RATELIMIT_EXCEEDED") || + errorMsg.toLowerCase().includes("quota") || + errorMsg.toLowerCase().includes("rate limit") + ); +} + +export async function batchProcess( + items: T[], + processor: (item: T, index: number) => Promise, + options: BatchOptions = {} +): Promise { + const { + concurrency = 2, + retries = 7, + minTimeout = 2000, + maxTimeout = 128000, + onProgress, + } = options; + + const limit = pLimit(concurrency); + let completed = 0; + + const promises = items.map((item, index) => + limit(() => + pRetry( + async () => { + try { + const result = await processor(item, index); + completed++; + onProgress?.(completed, items.length, item); + return result; + } catch (error: unknown) { + if (isRateLimitError(error)) { + throw error; + } + throw new pRetry.AbortError( + error instanceof Error ? error : new Error(String(error)) + ); + } + }, + { retries, minTimeout, maxTimeout, factor: 2 } + ) + ) + ); + + return Promise.all(promises); +} + +export async function batchProcessWithSSE( + items: T[], + processor: (item: T, index: number) => Promise, + sendEvent: (event: { type: string; [key: string]: unknown }) => void, + options: Omit = {} +): Promise { + const { retries = 5, minTimeout = 1000, maxTimeout = 15000 } = options; + + sendEvent({ type: "started", total: items.length }); + + const results: R[] = []; + let errors = 0; + + for (let index = 0; index < items.length; index++) { + const item = items[index]; + sendEvent({ type: "processing", index, item }); + + try { + const result = await pRetry( + () => processor(item, index), + { + retries, + minTimeout, + maxTimeout, + factor: 2, + onFailedAttempt: (error) => { + if (!isRateLimitError(error)) { + throw new pRetry.AbortError( + error instanceof Error ? error : new Error(String(error)) + ); + } + }, + } + ); + results.push(result); + sendEvent({ type: "progress", index, result }); + } catch (error) { + errors++; + results.push(undefined as R); + sendEvent({ + type: "progress", + index, + error: error instanceof Error ? error.message : "Processing failed", + }); + } + } + + sendEvent({ type: "complete", processed: items.length, errors }); + return results; +} diff --git a/lib/integrations-gemini-ai/src/client.ts b/lib/integrations-gemini-ai/src/client.ts new file mode 100644 index 0000000..2d4be6f --- /dev/null +++ b/lib/integrations-gemini-ai/src/client.ts @@ -0,0 +1,21 @@ +import { GoogleGenAI } from "@google/genai"; + +if (!process.env.AI_INTEGRATIONS_GEMINI_BASE_URL) { + throw new Error( + "AI_INTEGRATIONS_GEMINI_BASE_URL must be set. Did you forget to provision the Gemini AI integration?", + ); +} + +if (!process.env.AI_INTEGRATIONS_GEMINI_API_KEY) { + throw new Error( + "AI_INTEGRATIONS_GEMINI_API_KEY must be set. Did you forget to provision the Gemini AI integration?", + ); +} + +export const ai = new GoogleGenAI({ + apiKey: process.env.AI_INTEGRATIONS_GEMINI_API_KEY, + httpOptions: { + apiVersion: "", + baseUrl: process.env.AI_INTEGRATIONS_GEMINI_BASE_URL, + }, +}); diff --git a/lib/integrations-gemini-ai/src/image/client.ts b/lib/integrations-gemini-ai/src/image/client.ts new file mode 100644 index 0000000..2cf58e3 --- /dev/null +++ b/lib/integrations-gemini-ai/src/image/client.ts @@ -0,0 +1,47 @@ +import { GoogleGenAI, Modality } from "@google/genai"; + +if (!process.env.AI_INTEGRATIONS_GEMINI_BASE_URL) { + throw new Error( + "AI_INTEGRATIONS_GEMINI_BASE_URL must be set. Did you forget to provision the Gemini AI integration?", + ); +} + +if (!process.env.AI_INTEGRATIONS_GEMINI_API_KEY) { + throw new Error( + "AI_INTEGRATIONS_GEMINI_API_KEY must be set. Did you forget to provision the Gemini AI integration?", + ); +} + +export const ai = new GoogleGenAI({ + apiKey: process.env.AI_INTEGRATIONS_GEMINI_API_KEY, + httpOptions: { + apiVersion: "", + baseUrl: process.env.AI_INTEGRATIONS_GEMINI_BASE_URL, + }, +}); + +export async function generateImage( + prompt: string +): Promise<{ b64_json: string; mimeType: string }> { + const response = await ai.models.generateContent({ + model: "gemini-2.5-flash-image", + contents: [{ role: "user", parts: [{ text: prompt }] }], + config: { + responseModalities: [Modality.TEXT, Modality.IMAGE], + }, + }); + + const candidate = response.candidates?.[0]; + const imagePart = candidate?.content?.parts?.find( + (part: { inlineData?: { data?: string; mimeType?: string } }) => part.inlineData + ); + + if (!imagePart?.inlineData?.data) { + throw new Error("No image data in response"); + } + + return { + b64_json: imagePart.inlineData.data, + mimeType: imagePart.inlineData.mimeType || "image/png", + }; +} diff --git a/lib/integrations-gemini-ai/src/image/index.ts b/lib/integrations-gemini-ai/src/image/index.ts new file mode 100644 index 0000000..efd397c --- /dev/null +++ b/lib/integrations-gemini-ai/src/image/index.ts @@ -0,0 +1 @@ +export { ai, generateImage } from "./client"; diff --git a/lib/integrations-gemini-ai/src/index.ts b/lib/integrations-gemini-ai/src/index.ts new file mode 100644 index 0000000..7d8f5ff --- /dev/null +++ b/lib/integrations-gemini-ai/src/index.ts @@ -0,0 +1,3 @@ +export { ai } from "./client"; +export { generateImage } from "./image"; +export { batchProcess, batchProcessWithSSE, isRateLimitError, type BatchOptions } from "./batch"; diff --git a/lib/integrations-gemini-ai/tsconfig.json b/lib/integrations-gemini-ai/tsconfig.json new file mode 100644 index 0000000..53af906 --- /dev/null +++ b/lib/integrations-gemini-ai/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b1de6b..1007a83 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,15 +6,63 @@ settings: catalogs: default: + '@replit/vite-plugin-cartographer': + specifier: ^0.5.0 + version: 0.5.0 + '@replit/vite-plugin-runtime-error-modal': + specifier: ^0.0.6 + version: 0.0.6 + '@tailwindcss/vite': + specifier: ^4.1.14 + version: 4.2.1 '@tanstack/react-query': specifier: ^5.90.21 version: 5.90.21 + '@types/node': + specifier: ^25.3.3 + version: 25.3.5 + '@types/react': + specifier: ^19.2.0 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.0 + version: 19.2.3 + '@vitejs/plugin-react': + specifier: ^5.0.4 + version: 5.1.4 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + drizzle-orm: + specifier: ^0.45.1 + version: 0.45.1 + framer-motion: + specifier: ^12.23.24 + version: 12.35.1 + lucide-react: + specifier: ^0.545.0 + version: 0.545.0 react: specifier: 19.1.0 version: 19.1.0 react-dom: specifier: 19.1.0 version: 19.1.0 + tailwind-merge: + specifier: ^3.3.1 + version: 3.5.0 + tailwindcss: + specifier: ^4.1.14 + version: 4.2.1 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + vite: + specifier: ^7.3.0 + version: 7.3.1 zod: specifier: ^3.25.76 version: 3.25.76 @@ -130,6 +178,9 @@ importers: '@workspace/integrations-anthropic-ai': specifier: workspace:* version: link:../../lib/integrations-anthropic-ai + '@workspace/integrations-gemini-ai': + specifier: workspace:* + version: link:../../lib/integrations-gemini-ai cookie-parser: specifier: ^1.4.7 version: 1.4.7 @@ -561,6 +612,18 @@ importers: specifier: 'catalog:' version: 25.3.5 + lib/integrations-gemini-ai: + dependencies: + '@google/genai': + specifier: ^1.44.0 + version: 1.46.0 + p-limit: + specifier: ^7.3.0 + version: 7.3.0 + p-retry: + specifier: ^7.1.1 + version: 7.1.1 + scripts: devDependencies: '@types/node': @@ -1305,6 +1368,15 @@ packages: '@gerrit0/mini-shiki@3.23.0': resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==} + '@google/genai@1.46.0': + resolution: {integrity: sha512-ewPMN5JkKfgU5/kdco9ZhXBHDPhVqZpMQqIFQhwsHLf8kyZfx1cNpw1pHo1eV6PGEW7EhIBFi3aYZraFndAXqg==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + '@hookform/resolvers@3.10.0': resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==} peerDependencies: @@ -1447,6 +1519,36 @@ packages: '@orval/zod@8.5.3': resolution: {integrity: sha512-qcbnpGE0VrgCDm0hNWQSOmzbfgdnr1xo+PYQ3PJjxfLuk3kGdJmFANTr53/1lI3sZUvWZwX5nKJCLWVxvwJEgg==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -2494,6 +2596,9 @@ packages: '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} @@ -2787,6 +2892,9 @@ packages: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -2827,6 +2935,9 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -3092,6 +3203,10 @@ packages: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + date-fns-jalali@4.1.0-0: resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} @@ -3304,6 +3419,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -3664,6 +3782,9 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3705,6 +3826,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -3754,6 +3879,10 @@ packages: fontfaceobserver@2.3.0: resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -3803,6 +3932,14 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gaxios@7.1.4: + resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -3870,6 +4007,14 @@ packages: resolution: {integrity: sha512-+A4Hq7m7Ze592k9gZRy4gJ27DrXRNnC1vPjxTt1qQxEY8RxagBkBxivkCwg7FxSTG0iLLEMaUx13oOr0R2/qcQ==} engines: {node: '>=20'} + google-auth-library@10.6.2: + resolution: {integrity: sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -4134,6 +4279,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -4166,6 +4314,12 @@ packages: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -4234,6 +4388,9 @@ packages: resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} engines: {node: '>=4'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -4535,6 +4692,11 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -4544,6 +4706,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-forge@1.3.3: resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==} engines: {node: '>= 6.13.0'} @@ -4680,6 +4846,10 @@ packages: resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + p-retry@7.1.1: resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==} engines: {node: '>=20'} @@ -4859,6 +5029,10 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -5167,6 +5341,10 @@ packages: resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} engines: {node: '>=4'} + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -5744,6 +5922,10 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -6939,6 +7121,17 @@ snapshots: '@shikijs/types': 3.23.0 '@shikijs/vscode-textmate': 10.0.2 + '@google/genai@1.46.0': + dependencies: + google-auth-library: 10.6.2 + p-retry: 4.6.2 + protobufjs: 7.5.4 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + '@hookform/resolvers@3.10.0(react-hook-form@7.71.2(react@19.1.0))': dependencies: react-hook-form: 7.71.2(react@19.1.0) @@ -7175,6 +7368,29 @@ snapshots: - supports-color - typescript + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@radix-ui/number@1.1.1': {} '@radix-ui/primitive@1.1.3': {} @@ -8560,6 +8776,8 @@ snapshots: dependencies: '@types/node': 25.3.5 + '@types/retry@0.12.0': {} + '@types/send@1.2.1': dependencies: '@types/node': 25.3.5 @@ -8940,6 +9158,8 @@ snapshots: big-integer@1.6.52: {} + bignumber.js@9.3.1: {} + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -8997,6 +9217,8 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -9275,6 +9497,8 @@ snapshots: d3-timer@3.0.1: {} + data-uri-to-buffer@4.0.1: {} + date-fns-jalali@4.1.0-0: {} date-fns@3.6.0: {} @@ -9377,6 +9601,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} electron-to-chromium@1.5.307: {} @@ -9802,6 +10030,8 @@ snapshots: transitivePeerDependencies: - supports-color + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-equals@5.4.0: {} @@ -9846,6 +10076,11 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -9909,6 +10144,10 @@ snapshots: fontfaceobserver@2.3.0: {} + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded@0.2.0: {} framer-motion@12.35.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0): @@ -9946,6 +10185,22 @@ snapshots: function-bind@1.1.2: {} + gaxios@7.1.4: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.4 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -10030,6 +10285,19 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.4.0 + google-auth-library@10.6.2: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.4 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + gopd@1.2.0: {} got@11.8.6: @@ -10295,6 +10563,10 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + json-buffer@3.0.1: {} json-schema-to-ts@3.1.1: @@ -10322,6 +10594,17 @@ snapshots: jsonpointer@5.0.1: {} + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -10383,6 +10666,8 @@ snapshots: dependencies: chalk: 2.4.2 + long@5.3.2: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -10875,10 +11160,18 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + node-domexception@1.0.0: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-forge@1.3.3: {} node-int64@0.4.0: {} @@ -11048,6 +11341,11 @@ snapshots: dependencies: p-limit: 4.0.0 + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + p-retry@7.1.1: dependencies: is-network-error: 1.3.1 @@ -11197,6 +11495,21 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 25.3.5 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -11590,6 +11903,8 @@ snapshots: onetime: 2.0.1 signal-exit: 3.0.7 + retry@0.13.1: {} + reusify@1.1.0: {} rimraf@3.0.2: @@ -12131,6 +12446,8 @@ snapshots: dependencies: defaults: 1.0.4 + web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: {} webidl-conversions@5.0.0: {} diff --git a/tsconfig.json b/tsconfig.json index c7dcb44..eacc3eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,9 @@ { "path": "./lib/integrations-anthropic-ai" }, + { + "path": "./lib/integrations-gemini-ai" + }, { "path": "./artifacts/api-server" }