feat: Gemini AI integration — conversations, messages, image gen

- 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
This commit is contained in:
Replit Agent
2026-03-20 02:41:12 +00:00
parent cdb104e34f
commit e86dab0d65
54 changed files with 3620 additions and 28 deletions

View File

@@ -12,6 +12,7 @@
"@workspace/api-zod": "workspace:*", "@workspace/api-zod": "workspace:*",
"@workspace/db": "workspace:*", "@workspace/db": "workspace:*",
"@workspace/integrations-anthropic-ai": "workspace:*", "@workspace/integrations-anthropic-ai": "workspace:*",
"@workspace/integrations-gemini-ai": "workspace:*",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cors": "^2", "cors": "^2",
"drizzle-orm": "catalog:", "drizzle-orm": "catalog:",

View File

@@ -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;

View File

@@ -16,6 +16,7 @@ import estimateRouter from "./estimate.js";
import relayRouter from "./relay.js"; import relayRouter from "./relay.js";
import adminRelayRouter from "./admin-relay.js"; import adminRelayRouter from "./admin-relay.js";
import adminRelayQueueRouter from "./admin-relay-queue.js"; import adminRelayQueueRouter from "./admin-relay-queue.js";
import geminiRouter from "./gemini.js";
const router: IRouter = Router(); const router: IRouter = Router();
@@ -30,6 +31,7 @@ router.use(relayRouter);
router.use(adminRelayRouter); router.use(adminRelayRouter);
router.use(adminRelayQueueRouter); router.use(adminRelayQueueRouter);
router.use(demoRouter); router.use(demoRouter);
router.use("/gemini", geminiRouter);
router.use(testkitRouter); router.use(testkitRouter);
router.use(uiRouter); router.use(uiRouter);
router.use(nodeDiagnosticsRouter); router.use(nodeDiagnosticsRouter);

View File

@@ -15,6 +15,9 @@
}, },
{ {
"path": "../../lib/integrations-anthropic-ai" "path": "../../lib/integrations-anthropic-ai"
},
{
"path": "../../lib/integrations-gemini-ai"
} }
] ]
} }

View File

@@ -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 (10010,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;
};

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,8 @@ tags:
description: Pre-funded session balance mode (Mode 2 -- pay once, run many) description: Pre-funded session balance mode (Mode 2 -- pay once, run many)
- name: demo - name: demo
description: Free demo endpoint (rate-limited) description: Free demo endpoint (rate-limited)
- name: gemini
description: Gemini AI chat and image operations
paths: paths:
/healthz: /healthz:
get: get:
@@ -385,6 +387,139 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/ErrorResponse" $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: components:
schemas: schemas:
HealthStatus: HealthStatus:
@@ -663,8 +798,84 @@ components:
properties: properties:
result: result:
type: string 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: securitySchemes:
sessionMacaroon: sessionMacaroon:
type: http type: http
scheme: bearer scheme: bearer
description: Session macaroon issued when a session activates. Pass as `Authorization: Bearer <macaroon>`. description: "Session macaroon issued when a session activates. Pass as `Authorization: Bearer <macaroon>`."

View File

@@ -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,
},
},
},
};

View File

@@ -3,7 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"codegen": "orval --config ./orval.config.ts" "codegen": "orval --config ./orval.config.cjs"
}, },
"devDependencies": { "devDependencies": {
"orval": "^8.5.2" "orval": "^8.5.2"

View File

@@ -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({ export const GetJobResponse = zod.object({
id: z.string(), 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({ export const ClaimRefundBody = zod.object({
paymentRequest: z.string(), invoice: zod.string().describe("BOLT11 invoice for exactly refundAmountSats"),
}); });
export const RunDemoQueryParams = z.object({ export const ClaimRefundResponse = zod.object({
request: z.string().min(1), 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: 10010,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 (10010,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({ export const GetSessionResponse = zod.object({
request: z.string().min(1), 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 <macaroon>`.
* @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 (10010,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(),
}); });

View File

@@ -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 {};

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 (10010,000 sats)
* @minimum 100
* @maximum 10000
*/
amount_sats: number;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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[];
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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";

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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"
}
}

View File

@@ -0,0 +1,6 @@
export {
batchProcess,
batchProcessWithSSE,
isRateLimitError,
type BatchOptions,
} from "./utils";

View File

@@ -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<T, R>(
items: T[],
processor: (item: T, index: number) => Promise<R>,
options: BatchOptions = {}
): Promise<R[]> {
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<T, R>(
items: T[],
processor: (item: T, index: number) => Promise<R>,
sendEvent: (event: { type: string; [key: string]: unknown }) => void,
options: Omit<BatchOptions, "concurrency" | "onProgress"> = {}
): Promise<R[]> {
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;
}

View File

@@ -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,
},
});

View File

@@ -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",
};
}

View File

@@ -0,0 +1 @@
export { ai, generateImage } from "./client";

View File

@@ -0,0 +1,3 @@
export { ai } from "./client";
export { generateImage } from "./image";
export { batchProcess, batchProcessWithSSE, isRateLimitError, type BatchOptions } from "./batch";

View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"declarationMap": true,
"emitDeclarationOnly": true,
"outDir": "dist",
"rootDir": "src",
"types": ["node"]
},
"include": ["src"]
}

317
pnpm-lock.yaml generated
View File

@@ -6,15 +6,63 @@ settings:
catalogs: catalogs:
default: 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': '@tanstack/react-query':
specifier: ^5.90.21 specifier: ^5.90.21
version: 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: react:
specifier: 19.1.0 specifier: 19.1.0
version: 19.1.0 version: 19.1.0
react-dom: react-dom:
specifier: 19.1.0 specifier: 19.1.0
version: 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: zod:
specifier: ^3.25.76 specifier: ^3.25.76
version: 3.25.76 version: 3.25.76
@@ -130,6 +178,9 @@ importers:
'@workspace/integrations-anthropic-ai': '@workspace/integrations-anthropic-ai':
specifier: workspace:* specifier: workspace:*
version: link:../../lib/integrations-anthropic-ai version: link:../../lib/integrations-anthropic-ai
'@workspace/integrations-gemini-ai':
specifier: workspace:*
version: link:../../lib/integrations-gemini-ai
cookie-parser: cookie-parser:
specifier: ^1.4.7 specifier: ^1.4.7
version: 1.4.7 version: 1.4.7
@@ -561,6 +612,18 @@ importers:
specifier: 'catalog:' specifier: 'catalog:'
version: 25.3.5 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: scripts:
devDependencies: devDependencies:
'@types/node': '@types/node':
@@ -1305,6 +1368,15 @@ packages:
'@gerrit0/mini-shiki@3.23.0': '@gerrit0/mini-shiki@3.23.0':
resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==} 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': '@hookform/resolvers@3.10.0':
resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==} resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==}
peerDependencies: peerDependencies:
@@ -1447,6 +1519,36 @@ packages:
'@orval/zod@8.5.3': '@orval/zod@8.5.3':
resolution: {integrity: sha512-qcbnpGE0VrgCDm0hNWQSOmzbfgdnr1xo+PYQ3PJjxfLuk3kGdJmFANTr53/1lI3sZUvWZwX5nKJCLWVxvwJEgg==} 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': '@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
@@ -2494,6 +2596,9 @@ packages:
'@types/responselike@1.0.3': '@types/responselike@1.0.3':
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} 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': '@types/send@1.2.1':
resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
@@ -2787,6 +2892,9 @@ packages:
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
engines: {node: '>=0.6'} engines: {node: '>=0.6'}
bignumber.js@9.3.1:
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
body-parser@2.2.2: body-parser@2.2.2:
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -2827,6 +2935,9 @@ packages:
bser@2.1.1: bser@2.1.1:
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} 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: buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -3092,6 +3203,10 @@ packages:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'} 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: date-fns-jalali@4.1.0-0:
resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==}
@@ -3304,6 +3419,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
ee-first@1.1.1: ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
@@ -3664,6 +3782,9 @@ packages:
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
fast-deep-equal@3.1.3: fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -3705,6 +3826,10 @@ packages:
picomatch: picomatch:
optional: true optional: true
fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
figures@6.1.0: figures@6.1.0:
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -3754,6 +3879,10 @@ packages:
fontfaceobserver@2.3.0: fontfaceobserver@2.3.0:
resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==} 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: forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -3803,6 +3932,14 @@ packages:
function-bind@1.1.2: function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 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: gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -3870,6 +4007,14 @@ packages:
resolution: {integrity: sha512-+A4Hq7m7Ze592k9gZRy4gJ27DrXRNnC1vPjxTt1qQxEY8RxagBkBxivkCwg7FxSTG0iLLEMaUx13oOr0R2/qcQ==} resolution: {integrity: sha512-+A4Hq7m7Ze592k9gZRy4gJ27DrXRNnC1vPjxTt1qQxEY8RxagBkBxivkCwg7FxSTG0iLLEMaUx13oOr0R2/qcQ==}
engines: {node: '>=20'} 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: gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -4134,6 +4279,9 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
hasBin: true hasBin: true
json-bigint@1.0.0:
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
json-buffer@3.0.1: json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
@@ -4166,6 +4314,12 @@ packages:
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
engines: {node: '>=0.10.0'} 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: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -4234,6 +4388,9 @@ packages:
resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==}
engines: {node: '>=4'} engines: {node: '>=4'}
long@5.3.2:
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
loose-envify@1.4.0: loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true hasBin: true
@@ -4535,6 +4692,11 @@ packages:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^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: node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0} engines: {node: 4.x || >=6.0.0}
@@ -4544,6 +4706,10 @@ packages:
encoding: encoding:
optional: true 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: node-forge@1.3.3:
resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==} resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==}
engines: {node: '>= 6.13.0'} engines: {node: '>= 6.13.0'}
@@ -4680,6 +4846,10 @@ packages:
resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 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: p-retry@7.1.1:
resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==} resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==}
engines: {node: '>=20'} engines: {node: '>=20'}
@@ -4859,6 +5029,10 @@ packages:
prop-types@15.8.1: prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} 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: proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
@@ -5167,6 +5341,10 @@ packages:
resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==}
engines: {node: '>=4'} engines: {node: '>=4'}
retry@0.13.1:
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
engines: {node: '>= 4'}
reusify@1.1.0: reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'} engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -5744,6 +5922,10 @@ packages:
wcwidth@1.0.1: wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} 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: webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@@ -6939,6 +7121,17 @@ snapshots:
'@shikijs/types': 3.23.0 '@shikijs/types': 3.23.0
'@shikijs/vscode-textmate': 10.0.2 '@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))': '@hookform/resolvers@3.10.0(react-hook-form@7.71.2(react@19.1.0))':
dependencies: dependencies:
react-hook-form: 7.71.2(react@19.1.0) react-hook-form: 7.71.2(react@19.1.0)
@@ -7175,6 +7368,29 @@ snapshots:
- supports-color - supports-color
- typescript - 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/number@1.1.1': {}
'@radix-ui/primitive@1.1.3': {} '@radix-ui/primitive@1.1.3': {}
@@ -8560,6 +8776,8 @@ snapshots:
dependencies: dependencies:
'@types/node': 25.3.5 '@types/node': 25.3.5
'@types/retry@0.12.0': {}
'@types/send@1.2.1': '@types/send@1.2.1':
dependencies: dependencies:
'@types/node': 25.3.5 '@types/node': 25.3.5
@@ -8940,6 +9158,8 @@ snapshots:
big-integer@1.6.52: {} big-integer@1.6.52: {}
bignumber.js@9.3.1: {}
body-parser@2.2.2: body-parser@2.2.2:
dependencies: dependencies:
bytes: 3.1.2 bytes: 3.1.2
@@ -8997,6 +9217,8 @@ snapshots:
dependencies: dependencies:
node-int64: 0.4.0 node-int64: 0.4.0
buffer-equal-constant-time@1.0.1: {}
buffer-from@1.1.2: {} buffer-from@1.1.2: {}
buffer@5.7.1: buffer@5.7.1:
@@ -9275,6 +9497,8 @@ snapshots:
d3-timer@3.0.1: {} d3-timer@3.0.1: {}
data-uri-to-buffer@4.0.1: {}
date-fns-jalali@4.1.0-0: {} date-fns-jalali@4.1.0-0: {}
date-fns@3.6.0: {} date-fns@3.6.0: {}
@@ -9377,6 +9601,10 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
gopd: 1.2.0 gopd: 1.2.0
ecdsa-sig-formatter@1.0.11:
dependencies:
safe-buffer: 5.2.1
ee-first@1.1.1: {} ee-first@1.1.1: {}
electron-to-chromium@1.5.307: {} electron-to-chromium@1.5.307: {}
@@ -9802,6 +10030,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
extend@3.0.2: {}
fast-deep-equal@3.1.3: {} fast-deep-equal@3.1.3: {}
fast-equals@5.4.0: {} fast-equals@5.4.0: {}
@@ -9846,6 +10076,11 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.3 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: figures@6.1.0:
dependencies: dependencies:
is-unicode-supported: 2.1.0 is-unicode-supported: 2.1.0
@@ -9909,6 +10144,10 @@ snapshots:
fontfaceobserver@2.3.0: {} fontfaceobserver@2.3.0: {}
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
forwarded@0.2.0: {} forwarded@0.2.0: {}
framer-motion@12.35.1(react-dom@19.1.0(react@19.1.0))(react@19.1.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: {} 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: {} gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {} get-caller-file@2.0.5: {}
@@ -10030,6 +10285,19 @@ snapshots:
slash: 5.1.0 slash: 5.1.0
unicorn-magic: 0.4.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: {} gopd@1.2.0: {}
got@11.8.6: got@11.8.6:
@@ -10295,6 +10563,10 @@ snapshots:
jsesc@3.1.0: {} jsesc@3.1.0: {}
json-bigint@1.0.0:
dependencies:
bignumber.js: 9.3.1
json-buffer@3.0.1: {} json-buffer@3.0.1: {}
json-schema-to-ts@3.1.1: json-schema-to-ts@3.1.1:
@@ -10322,6 +10594,17 @@ snapshots:
jsonpointer@5.0.1: {} 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: keyv@4.5.4:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
@@ -10383,6 +10666,8 @@ snapshots:
dependencies: dependencies:
chalk: 2.4.2 chalk: 2.4.2
long@5.3.2: {}
loose-envify@1.4.0: loose-envify@1.4.0:
dependencies: dependencies:
js-tokens: 4.0.0 js-tokens: 4.0.0
@@ -10875,10 +11160,18 @@ snapshots:
react: 19.1.0 react: 19.1.0
react-dom: 19.1.0(react@19.1.0) react-dom: 19.1.0(react@19.1.0)
node-domexception@1.0.0: {}
node-fetch@2.7.0: node-fetch@2.7.0:
dependencies: dependencies:
whatwg-url: 5.0.0 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-forge@1.3.3: {}
node-int64@0.4.0: {} node-int64@0.4.0: {}
@@ -11048,6 +11341,11 @@ snapshots:
dependencies: dependencies:
p-limit: 4.0.0 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: p-retry@7.1.1:
dependencies: dependencies:
is-network-error: 1.3.1 is-network-error: 1.3.1
@@ -11197,6 +11495,21 @@ snapshots:
object-assign: 4.1.1 object-assign: 4.1.1
react-is: 16.13.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: proxy-addr@2.0.7:
dependencies: dependencies:
forwarded: 0.2.0 forwarded: 0.2.0
@@ -11590,6 +11903,8 @@ snapshots:
onetime: 2.0.1 onetime: 2.0.1
signal-exit: 3.0.7 signal-exit: 3.0.7
retry@0.13.1: {}
reusify@1.1.0: {} reusify@1.1.0: {}
rimraf@3.0.2: rimraf@3.0.2:
@@ -12131,6 +12446,8 @@ snapshots:
dependencies: dependencies:
defaults: 1.0.4 defaults: 1.0.4
web-streams-polyfill@3.3.3: {}
webidl-conversions@3.0.1: {} webidl-conversions@3.0.1: {}
webidl-conversions@5.0.0: {} webidl-conversions@5.0.0: {}

View File

@@ -12,6 +12,9 @@
{ {
"path": "./lib/integrations-anthropic-ai" "path": "./lib/integrations-anthropic-ai"
}, },
{
"path": "./lib/integrations-gemini-ai"
},
{ {
"path": "./artifacts/api-server" "path": "./artifacts/api-server"
} }