Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Visit [http://localhost:3000](http://localhost:3000) to get started.
### Action Nodes

<!-- PLUGINS:START - Do not remove. Auto-generated by discover-plugins -->
- **AI Gateway**: Generate Text, Generate Image
- **AI Gateway**: Generate Text, Generate Image, Generate Embeddings
- **Blob**: Put Blob, List Blobs
- **Clerk**: Get User, Create User, Update User, Delete User
- **fal.ai**: Generate Image, Generate Video, Upscale Image, Remove Background, Image to Image
Expand All @@ -92,6 +92,7 @@ Visit [http://localhost:3000](http://localhost:3000) to get started.
- **Stripe**: Create Customer, Get Customer, Create Invoice
- **Superagent**: Guard, Redact
- **v0**: Create Chat, Send Message
- **Webflow**: List Sites, Get Site, Publish Site
<!-- PLUGINS:END -->

## Code Generation
Expand Down
71 changes: 70 additions & 1 deletion plugins/ai-gateway/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AiGatewayIcon } from "./icon";
const aiGatewayPlugin: IntegrationPlugin = {
type: "ai-gateway",
label: "AI Gateway",
description: "Generate text and images using AI models",
description: "Generate text, images, and embeddings using AI models",

icon: AiGatewayIcon,

Expand Down Expand Up @@ -141,6 +141,75 @@ const aiGatewayPlugin: IntegrationPlugin = {
},
],
},
{
slug: "generate-embeddings",
label: "Generate Embeddings",
description: "Generate text embeddings using AI models",
category: "AI Gateway",
stepFunction: "generateEmbeddingsStep",
stepImportPath: "generate-embeddings",
outputFields: [
{ field: "embedding", description: "Single embedding vector" },
{ field: "embeddings", description: "Batch embedding vectors" },
],
configFields: [
{
key: "embeddingMode",
label: "Embedding Mode",
type: "select",
defaultValue: "single",
options: [
{ value: "single", label: "Single Value" },
{ value: "batch", label: "Batch Values" },
],
required: true,
},
{
key: "embeddingModel",
label: "Model",
type: "select",
defaultValue: "openai/text-embedding-3-small",
options: [
{ value: "amazon/titan-embed-text-v2", label: "Amazon Titan Embed Text V2"},
{ value: "cohere/embed-v4.0", label: "Cohere Embed V4.0"},
{ value: "google/gemini-embedding-001", label: "Gemini Embedding 001"},
{ value: "google/text-embedding-005", label: "Google Text Embedding 005" },
{ value: "google/text-multilingual-embedding-002", label: "Google Text Multilingual Embedding 002" },
{ value: "mistral/codestral-embed", label: "Mistral Codestral Embed" },
{ value: "mistral/mistral-embed", label: "Mistral Embed" },
{ value: "openai/text-embedding-3-large", label: "OpenAI Text Embedding 3 Large" },
{ value: "openai/text-embedding-3-small", label: "OpenAI Text Embedding 3 Small" },
{ value: "openai/text-embedding-ada-002", label: "OpenAI Text Embedding Ada 002" },
{ value: "voyage/voyage-3-large", label: "Voyage 3 Large" },
{ value: "voyage/voyage-3.5", label: "Voyage 3.5" },
{ value: "voyage/voyage-3.5-lite", label: "Voyage 3.5 Lite" },
{ value: "voyage/voyage-code-3", label: "Voyage Code 3" },
],
required: true,
},
{
key: "embeddingValue",
label: "Text to Embed",
type: "template-input",
placeholder: "Enter text to embed. Use {{NodeName.field}} to reference previous outputs.",
example: "sunny day at the beach",
showWhen: { field: "embeddingMode", equals: "single" },
required: true,
},
{
key: "embeddingValues",
label: "Texts to Embed",
type: "template-textarea",
placeholder:
"Enter one text per line for batch embedding:\n- First text\n- Second text\nUse {{NodeName.field}} to reference previous outputs.",
example:
"sunny day at the beach\nrainy afternoon in the city\nsnowy night in the mountains",
rows: 6,
showWhen: { field: "embeddingMode", equals: "batch" },
required: true,
}
],
},
],
};

Expand Down
144 changes: 144 additions & 0 deletions plugins/ai-gateway/steps/generate-embeddings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import "server-only";

import { createGateway, embed, embedMany } from "ai";
import { fetchCredentials } from "@/lib/credential-fetcher";
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
import { getErrorMessageAsync } from "@/lib/utils";
import type { AiGatewayCredentials } from "../credentials";

type GenerateEmbeddingsResult =
| { success: true; embedding: number[]; }
| {
success: true;
embeddings: number[][];
}
| { success: false; error: string };

export type GenerateEmbeddingsCoreInput = {
embeddingMode: "single" | "batch";
embeddingModel: string;
embeddingValue?: string;
embeddingValues?: string;
};

export type GenerateEmbeddingsInput = StepInput &
GenerateEmbeddingsCoreInput & {
integrationId?: string;
};


/**
* Parse batch embedding values from textarea input (one per line)
*/
function parseEmbeddingValues(valuesText?: string): string[] {
if (!valuesText) {
return [];
}

return valuesText
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0);
}

/**
* Core logic - portable between app and export
*/
async function stepHandler(
input: GenerateEmbeddingsCoreInput,
credentials: AiGatewayCredentials,
): Promise<GenerateEmbeddingsResult> {
const apiKey = credentials.AI_GATEWAY_API_KEY;

if (!apiKey) {
return {
success: false,
error:
"AI_GATEWAY_API_KEY is not configured. Please add it in Project Integrations.",
};
}

const mode = input.embeddingMode || "single";
const modelId = input.embeddingModel || "openai/text-embedding-3-small";

try {
const gateway = createGateway({
apiKey,
});

// Handle single embedding mode
if (mode === "single") {
const value = input.embeddingValue?.trim() || "";

if (!value) {
return {
success: false,
error: "Embedding value is required for single mode",
};
}

const { embedding } = await embed({
model: gateway.textEmbeddingModel(modelId),
value,
});

return {
success: true,
embedding,
};
}

// Handle batch embedding mode
if (mode === "batch") {
const valuesText = input.embeddingValues?.trim() || "";
const values = parseEmbeddingValues(valuesText);

if (values.length === 0) {
return {
success: false,
error:
"At least one embedding value is required for batch mode (one per line)",
};
}

const { embeddings } = await embedMany({
model: gateway.textEmbeddingModel(modelId),
values,
});

return {
success: true,
embeddings,
};
}

return {
success: false,
error: `Invalid embedding mode: ${mode}`,
};
} catch (error) {
const message = await getErrorMessageAsync(error);
return {
success: false,
error: `Embedding generation failed: ${message}`,
};
}
}

/**
* App entry point - fetches credentials and wraps with logging
*/
export async function generateEmbeddingsStep(
input: GenerateEmbeddingsInput,
): Promise<GenerateEmbeddingsResult> {
"use step";

const credentials = input.integrationId
? await fetchCredentials(input.integrationId)
: {};

return withStepLogging(input, () => stepHandler(input, credentials));
}
generateEmbeddingsStep.maxRetries = 0;

export const _integrationType = "ai-gateway";