Skip to main content

Vercel AI SDK

Add Sekuire governance to every generateText() and streamText() call in your Vercel AI SDK application.


What You Get

  • Model allowlists enforced on every LLM call
  • Rate limiting per agent, per minute
  • Tool restrictions checked before tool execution
  • Network and filesystem policies applied at the provider level
  • Zero changes to your existing Vercel AI SDK code - just swap the model

Prerequisites

  • Node.js 18+
  • @sekuire/sdk installed
  • ai and @ai-sdk/provider packages
  • An LLM API key (e.g., OPENAI_API_KEY)

Installation

pnpm add @sekuire/sdk ai @ai-sdk/provider openai

Configure Governance

Create a sekuire.yml in your project root:

sekuire.yml
project:
name: vercel-ai-sdk-integration
version: 1.0.0

agent:
name: Research Assistant
system_prompt: ./system_prompt.md
tools: ./tools.json
llm:
provider: openai
model: gpt-4o-mini
api_key_env: OPENAI_API_KEY
temperature: 0.7
max_tokens: 1024
models:
allowed_models:
- gpt-4o-mini
- gpt-4o
blocked_models:
- gpt-3.5-turbo
toolsets:
allowed_tools:
- name: web_search
- name: "files:read"
- name: calculator
blocked_tools:
- file_delete
- env_set

permissions:
network:
enabled: true
require_tls: true
allowed_domains:
- api.openai.com
- "*.wikipedia.org"
blocked_domains:
- "*.malware.net"
filesystem:
enabled: true
allowed_paths:
- "./data/*"
- "/tmp/*"
blocked_paths:
- "/etc/*"
- "~/.ssh/*"
allowed_extensions:
- .txt
- .json
- .csv
- .md

rate_limits:
per_agent:
requests_per_minute: 10

Key sections:

  • agent.models - Which models are allowed or blocked
  • agent.toolsets - Which tools are allowed or blocked
  • permissions - Network and filesystem restrictions
  • rate_limits - Per-agent request caps per minute

Create the Provider

The provider implements the LanguageModelV1 interface from @ai-sdk/provider. It loads your sekuire.yml, creates an LLM provider with policy enforcement, and exposes it as a standard Vercel AI SDK model.

sekuire-provider.ts
import type {
LanguageModelV1,
LanguageModelV1CallWarning,
LanguageModelV1FinishReason,
LanguageModelV1StreamPart,
LanguageModelV1Prompt,
LanguageModelV1CallOptions,
LanguageModelV1FunctionToolCall,
} from "@ai-sdk/provider";
import {
loadConfig,
getAgentConfig,
createLLMProvider,
PolicyEnforcer,
} from "@sekuire/sdk";
import type {
LLMProvider,
LLMMessage as Message,
ChatOptions,
LLMToolDefinition as ToolDefinition,
LLMToolCall as ToolCallFunction,
ActivePolicy,
SekuireConfig,
AgentConfig,
} from "@sekuire/sdk";
import fs from "fs/promises";

interface SekuireProviderOptions {
configPath?: string;
}

interface SekuireModelHandle {
provider: LLMProvider;
enforcer: PolicyEnforcer | undefined;
agentConfig: AgentConfig;
config: SekuireConfig;
}

The core of the integration is message conversion. Vercel AI SDK uses a structured prompt format with typed content parts. The provider converts these to Sekuire's flat message format:

sekuire-provider.ts
function convertPromptToMessages(prompt: LanguageModelV1Prompt): Message[] {
const messages: Message[] = [];

for (const msg of prompt) {
switch (msg.role) {
case "system":
messages.push({ role: "system", content: msg.content });
break;

case "user": {
const textParts = msg.content.filter(
(part): part is Extract<(typeof msg.content)[number], { type: "text" }> =>
part.type === "text"
);
messages.push({ role: "user", content: textParts.map((p) => p.text).join("") });
break;
}

case "assistant": {
const textParts: string[] = [];
const toolCalls: ToolCallFunction[] = [];

for (const part of msg.content) {
if (part.type === "text") textParts.push(part.text);
else if (part.type === "tool-call") {
toolCalls.push({
id: part.toolCallId,
type: "function",
function: {
name: part.toolName,
arguments: typeof part.args === "string"
? part.args
: JSON.stringify(part.args),
},
});
}
}

const content = textParts.join("") || null;
if (toolCalls.length > 0) {
messages.push({ role: "assistant", content, tool_calls: toolCalls });
} else {
messages.push({ role: "assistant", content: content || "" });
}
break;
}

case "tool": {
for (const part of msg.content) {
if (part.type === "tool-result") {
messages.push({
role: "tool",
tool_call_id: part.toolCallId,
content: typeof part.result === "string"
? part.result
: JSON.stringify(part.result),
});
}
}
break;
}
}
}

return messages;
}

The LanguageModelV1 implementation wires doGenerate() and doStream() through the Sekuire provider:

sekuire-provider.ts
function createSekuireLanguageModel(handle: SekuireModelHandle): LanguageModelV1 {
const { provider, enforcer } = handle;
const modelId = provider.getModelName();

return {
specificationVersion: "v1",
provider: `sekuire:${provider.getProviderName()}`,
modelId,
defaultObjectGenerationMode: undefined,

async doGenerate(options) {
if (enforcer && !("setPolicyEnforcer" in provider)) {
enforcer.enforceModel(modelId);
enforcer.enforceRateLimit("request");
}

const messages = convertPromptToMessages(options.prompt);
const chatOptions = convertChatOptions(options);
const response = await provider.chat(messages, chatOptions);

return {
text: response.content || undefined,
toolCalls: mapToolCalls(response.tool_calls),
finishReason: mapFinishReason(response.finish_reason),
usage: {
promptTokens: response.usage?.prompt_tokens ?? 0,
completionTokens: response.usage?.completion_tokens ?? 0,
},
rawCall: { rawPrompt: messages, rawSettings: chatOptions },
warnings: [] as LanguageModelV1CallWarning[],
};
},

async doStream(options) {
if (enforcer && !("setPolicyEnforcer" in provider)) {
enforcer.enforceModel(modelId);
enforcer.enforceRateLimit("request");
}

const messages = convertPromptToMessages(options.prompt);
const chatOptions = convertChatOptions(options);
const generator = provider.chatStream(messages, chatOptions);

const readableStream = new ReadableStream<LanguageModelV1StreamPart>({
async start(controller) {
let finishReason: LanguageModelV1FinishReason = "stop";
for await (const chunk of generator) {
if (chunk.content) {
controller.enqueue({ type: "text-delta", textDelta: chunk.content });
}
if (chunk.finish_reason) {
finishReason = mapFinishReason(chunk.finish_reason);
}
}
controller.enqueue({
type: "finish",
finishReason,
usage: { promptTokens: 0, completionTokens: 0 },
});
controller.close();
},
});

return {
stream: readableStream,
rawCall: { rawPrompt: messages, rawSettings: chatOptions },
warnings: [] as LanguageModelV1CallWarning[],
};
},
};
}

The factory function loads config once and caches the handle:

sekuire-provider.ts
export function createSekuireProvider(options?: SekuireProviderOptions) {
const configPath = options?.configPath;
const handleCache = new Map<string, SekuireModelHandle>();

return {
async model(agentName?: string): Promise<LanguageModelV1> {
const cacheKey = agentName || "__default__";
if (!handleCache.has(cacheKey)) {
const handle = await loadModelHandle(agentName, configPath);
handleCache.set(cacheKey, handle);
}
return createSekuireLanguageModel(handleCache.get(cacheKey)!);
},

getHandle(agentName?: string): SekuireModelHandle | undefined {
return handleCache.get(agentName || "__default__");
},
};
}
tip

The full provider source (including helper functions for config loading and policy building) is available in the TypeScript SDK examples.


Use with generateText()

index.ts
import { generateText } from "ai";
import { createSekuireProvider } from "./sekuire-provider";

const sekuire = createSekuireProvider({ configPath: "./sekuire.yml" });
const model = await sekuire.model();

const { text, usage, finishReason } = await generateText({
model,
messages: [
{ role: "system", content: "You are a concise research assistant." },
{ role: "user", content: "What are the three laws of robotics?" },
],
});

console.log(text);
console.log(`${usage.promptTokens} prompt + ${usage.completionTokens} completion tokens`);

Use with streamText()

index.ts
import { streamText } from "ai";
import { createSekuireProvider } from "./sekuire-provider";

const sekuire = createSekuireProvider({ configPath: "./sekuire.yml" });
const model = await sekuire.model();

const result = streamText({
model,
prompt: "Explain policy enforcement in one paragraph.",
});

for await (const chunk of (await result).textStream) {
process.stdout.write(chunk);
}

Use with Tool Calls

Tool definitions pass through the provider and are enforced against your policy:

index.ts
import { generateText } from "ai";
import { PolicyViolationError } from "@sekuire/sdk";
import { createSekuireProvider } from "./sekuire-provider";

const sekuire = createSekuireProvider({ configPath: "./sekuire.yml" });
const model = await sekuire.model();

try {
const result = await generateText({
model,
messages: [
{ role: "user", content: "What is the weather in San Francisco?" },
],
tools: {
get_weather: {
description: "Get current weather for a location",
parameters: {
type: "object" as const,
properties: {
location: { type: "string", description: "City name" },
},
required: ["location"],
},
},
},
});

if (result.toolCalls?.length) {
for (const call of result.toolCalls) {
console.log(`Tool call: ${call.toolName}(${JSON.stringify(call.args)})`);
}
}
} catch (err) {
if (err instanceof PolicyViolationError) {
console.log(`Blocked by policy: [${err.rule}] ${err.message}`);
}
}

Policy Enforcement in Action

Blocked model

If your policy allows gpt-4o-mini and gpt-4o but blocks gpt-3.5-turbo, any attempt to use the blocked model throws immediately:

const enforcer = sekuire.getHandle()!.enforcer!;

try {
enforcer.enforceModel("gpt-3.5-turbo");
} catch (err) {
// PolicyViolationError: [model_blocked] Model gpt-3.5-turbo is not allowed
}

Rate limits

With requests_per_minute: 10, the 11th request in a minute window is blocked:

for (let i = 1; i <= 15; i++) {
try {
enforcer.enforceRateLimit("request");
console.log(`Request ${i}: ALLOWED`);
} catch (err) {
if (err instanceof PolicyViolationError) {
console.log(`Request ${i}: BLOCKED - ${err.message}`);
}
}
}
Request 1: ALLOWED
Request 2: ALLOWED
...
Request 10: ALLOWED
Request 11: BLOCKED - Rate limit exceeded: 10 requests per minute

Next Steps