Vercel AI SDK
Call Knobase beside streamText and keep the model answer unchanged.
This is host-application sample code. The Knobase repository does not depend on the `ai` package. The example uses the current public AI SDK streamText / UIMessage surface.
What the host may do
- Call Knobase from the server independently from streamText.
- Preserve the normal answer stream.
- Attach a structured offer as typed application data or UIMessage metadata/data parts.
- Let the existing app decide whether it has an appropriate native UI surface.
Do not
- Put offer content into the model system prompt.
- Ask the model to produce sponsored text.
- Re-feed a sponsored offer tool result to the model as answer content.
- Add a required card component.
Independent decide beside streamText
import { streamText } from "ai";
type CommercialAttachment = {
disclosure: string;
advertiser: string;
link: string;
title?: string;
description?: string;
cta?: string;
};
async function decideOffer(message: string): Promise<CommercialAttachment | undefined> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 400);
try {
const response = await fetch("https://knobase.com/v1/offers/decide", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.KNOBASE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
message,
locale: "ko",
audience: { age_band: "18_plus", contextual_offer_consent: true },
native_surface: { clickable: true, disclosure_capable: true },
}),
signal: controller.signal,
});
if (!response.ok) return undefined;
const decision = await response.json();
if (!decision.serve) return undefined;
return {
disclosure: decision.disclosure,
advertiser: decision.advertiser,
link: decision.link,
title: decision.title,
description: decision.description,
cta: decision.cta,
};
} catch {
return undefined;
} finally {
clearTimeout(timer);
}
}
export async function POST(req: Request) {
const { messages } = await req.json();
const current = messages.at(-1)?.content ?? "";
const [result, commercialAttachment] = await Promise.all([
streamText({
model: "openai/gpt-4.1-mini",
messages,
// Do not put offer copy in the system prompt.
}),
decideOffer(typeof current === "string" ? current : ""),
]);
// Return the model stream unchanged. Attach the offer as separate
// application data or UIMessage metadata — never as answer text.
return result.toUIMessageStreamResponse({
messageMetadata: {
commercialAttachment,
},
});
}