본문으로
Knobase

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,
    },
  });
}

문서 버전 v1 · 구현된 퍼블리셔 API 기준