Skip to main content

11. Vercel AI SDK Wrapper

Purpose

agentid-vercel-sdk is the official AgentID wrapper for the Vercel AI SDK.

Use it when the application already relies on:

  • generateText()
  • streamText()
  • @ai-sdk/openai
  • @ai-sdk/anthropic

The goal is to keep the client callsite unchanged while still enforcing AgentID guardrails and telemetry.

import { openai } from "@ai-sdk/openai";
import { generateText } from "ai";
import { withAgentId } from "agentid-vercel-sdk";

const secureModel = withAgentId(openai("gpt-4o"), {
systemId: process.env.AGENTID_SYSTEM_ID!,
apiKey: process.env.AGENTID_API_KEY!,
});

const result = await generateText({
model: secureModel,
prompt: "Write a short refund confirmation.",
});

The important integration rule is that secureModel must be the model passed to the real generateText() or streamText() call. Sending raw messages to openai("gpt-4o") and logging a masked copy to AgentID afterwards only creates a masked audit trail. It does not protect the LLM.

For chat and agent apps, route the complete messages array through the wrapped model, including previous user messages, assistant messages, tool results, retrieval context, and the latest user message. Protecting only the latest input field is not enough because the provider receives the full context.

Protect the full history still means one model invocation, not many guard calls. Build the exact messages array for the provider call, protect that array once, run one pre-flight decision for that provider call, then send the protected history to the provider.

Current V1 scope note: full-history protection here refers to the provider payload after local masking/protection. The wrapper's pre-flight guard request is currently derived from the last user message text plus supported inline attachments on that last user turn, not the exact full multi-turn history. If you need pre-flight evaluation over the assembled history today, use an explicit guard -> protected provider call -> ingest flow for that route.

When SDK-side masking is enabled, withAgentId(...) protects text parts across the full Vercel AI SDK prompt history before provider dispatch. Telemetry is marked with full_history_protected=true, messages_count, and protected_messages_count so operators can verify that the integration is not just logging a masked copy.

Runtime Flow

The wrapper keeps the same backend-first contract as agentid-sdk:

  1. extract the user prompt from the Vercel AI SDK request
  2. call AgentID /guard
  3. block early when guard denies
  4. optionally apply transformed_input before provider execution
  5. call the wrapped provider model
  6. persist /ingest telemetry
  7. finalize SDK timing with /ingest/finalize

This means denied prompts are blocked before the provider is billed.

Enterprise Rollout Gate

The biggest rollout risk is not usually a weak prompt-injection classifier. It is a provider path that never uses withAgentId(...).

Do not call a Vercel AI SDK deployment enterprise-ready until all of these are true:

  1. every server-side generateText() / streamText() call that reaches a model uses a withAgentId(...) wrapped model
  2. no route performs a parallel raw provider call outside the wrapped path
  3. the wrapped call carries the exact full message history that reaches the provider
  4. manual fallbacks use guard -> protected provider call -> ingest, not raw provider call -> masked log
  5. Activity confirms wrapper coverage with full_history_protected=true or equivalent manual metadata

Token, Cost, and ROI Telemetry

The wrapper forwards Vercel AI SDK usage metadata to AgentID completion telemetry when the provider exposes usage. This is what powers:

  • token usage KPIs
  • cost_usd
  • Total Spend
  • cost charts
  • projected savings and ROI graphs

Do not strip, replace, or ignore the wrapped result before it is observed by the wrapper. For streams, make sure the stream is consumed or closed normally so the final telemetry branch can complete.

If cost or ROI is empty after integration, verify the Activity detail row has:

  • event_type: complete
  • Model set to the actual provider model, not Not applicable
  • non-zero input_tokens and output_tokens
  • cost_usd populated for priced models
  • system business context configured in Settings (human_hourly_rate and human_time_per_task_min) for ROI

Manual History Protection Helper

Prefer withAgentId(...). If a custom route cannot use the wrapper yet, protect the complete messages array before calling the provider:

  1. Build the exact messages array for this provider call.
  2. Run protectMessageHistory(...) once on that full array.
  3. If you are doing manual guard telemetry, issue one guard() call for that provider call.
  4. Send protectedHistory.messages to the provider.

Do not iterate over old messages and call guard() once per message. That creates multiple guard events for one LLM request and can look like duplicate or stale conversation logging.

import { protectMessageHistory } from "agentid-vercel-sdk";

const protectedHistory = protectMessageHistory(body.messages, {
pii: true,
secrets: true,
});

const result = streamText({
model: getCustomModel(),
messages: protectedHistory.messages,
});

Manual integrations should include full_history_protected=true, messages_count, protected_messages_count, and transformed_prompt_text_parts_count in ingest metadata. Otherwise the dashboard treats the row as an unverified manual integration.

Failure Behavior

Default behavior is backend-first and fail-open unless the effective system policy says otherwise.

  • /guard is the primary authority
  • clientFastFail is optional and disabled by default
  • strictMode or failureMode: "fail_close" activates fail-close behavior
  • fail-open dependency fallback keeps local deterministic PII and secret masking enabled in agentid-sdk@0.1.40+
  • if backend guard is temporarily unreachable and the effective mode is fail-close, the wrapper can apply the same local deterministic fallback contract as agentid-sdk

This keeps the wrapper aligned with agentid-sdk instead of inventing a separate security model.

Edge Runtime Compatibility

The wrapper is designed for Vercel AI SDK and Edge-safe server runtimes.

  • no fs
  • no native Node crypto dependency in the wrapper path
  • streaming telemetry is observed on a forked ReadableStream branch so the response stream stays non-blocking for the caller

Provider Coverage

The package is provider-agnostic at the Vercel AI SDK layer, but this repo validates concrete provider behavior with integration tests for:

  • @ai-sdk/openai non-stream
  • @ai-sdk/openai stream
  • @ai-sdk/anthropic non-stream
  • @ai-sdk/anthropic stream

Per-request Overrides

Request-scoped identity can be passed through providerOptions.agentid.

const result = await generateText({
model: secureModel,
prompt: "Summarize this customer ticket.",
providerOptions: {
agentid: {
userId: "customer-123",
requestIdentity: {
tenantId: "acme",
sessionId: "sess-42",
},
expectedLanguages: ["en"],
clientEventId: "11111111-1111-4111-8111-111111111111",
},
},
});

Supported request-level overrides:

  • userId
  • requestIdentity
  • expectedLanguages
  • clientEventId
  • telemetry

Workflow Timeline Behavior

When telemetry.workflowRunId is provided, the dashboard groups matching rows into a workflow timeline. The guarded prompt still remains visible as a standalone prompt/guard Activity row for forensic inspection.

Recommended model:

  • use workflowRunId to group the whole run
  • use workflowStepName / workflowStepId to identify the current step
  • use event-specific clientEventId only for a single guarded provider call or operation event
  • log surrounding tool/delivery/inbox events with AgentID.logOperation() or the workflow trail helper

This preserves prompt-level auditability while still showing the operational sequence around the LLM call.

Masking and Secret Coverage

Masking follows the base agentid-sdk runtime configuration. With SDK-side masking enabled, prompt text can be rewritten before /guard, provider dispatch, and /ingest. Output text from the wrapped result or wrapped stream is also protected before it is returned to the app caller and before telemetry is stored.

This only applies to calls made through withAgentId(...). A raw provider call in the same route can still leak PII even if the dashboard later shows a masked AgentID row.

Regression coverage includes:

  • multiline PEM, certificate, and PGP private key blocks
  • natural-language password disclosures such as my Password is Passwordk123
  • environment-style assignments such as DB_PASSWORD=...
  • suffix-safe secret values such as values ending in #
  • base64-like secret values with = / == padding
  • security-question answers after answer is, is, or localized equivalents

Operational Notes

  • The wrapper expects a text user prompt. File-only prompt payloads are not a supported V1 surface and currently raise AgentIdPromptExtractionError.
  • Pre-flight guard extraction is centered on the last user message text plus supported inline attachments on that last user turn, not the exact full provider prompt history.
  • The wrapper delegates capability fetch, retry behavior, guard correlation, and finalize semantics to agentid-sdk.
  • If you are not using Vercel AI SDK, prefer the Node.js / TypeScript SDK (agentid-sdk) or call /guard + /ingest explicitly.

Package QA

Before publishing the npm package from this monorepo, run:

npm run audit:all
npm run qa:production-gate

The gate includes the package-local audit, provider integration tests, typecheck, build, and the root Next production build.