Vercel AI SDK
streamText 옆에서 Knobase를 호출하고 모델 답변은 그대로 둡니다.
호스트 앱 샘플입니다. Knobase 저장소는 `ai` 패키지에 의존하지 않습니다. 예제는 현재 공개 AI SDK의 streamText / UIMessage 표면을 사용합니다.
호스트가 할 수 있는 일
- streamText와 별도로 서버에서 Knobase를 호출합니다.
- 일반 답변 스트림을 유지합니다.
- 구조화된 오퍼를 앱 데이터 또는 UIMessage 메타데이터/data part로 붙입니다.
- 적절한 네이티브 UI 표면이 있는지는 기존 앱이 판단합니다.
하지 말 것
- 오퍼 내용을 모델 시스템 프롬프트에 넣지 마세요.
- 모델에게 후원 문구를 만들라고 하지 마세요.
- 후원 오퍼 도구 결과를 답변 내용으로 다시 넣지 마세요.
- 필수 카드 컴포넌트를 추가하지 마세요.
streamText와 독립 decide
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,
},
});
}