Last reviewed: 2026-08-05

Direct answer

The cleanest CometAPI Vercel AI SDK setup is a server-only Next.js Route Handler that uses the CometAPI provider, calls streamText, and returns an AI SDK UI message stream. The browser calls your local route; it never receives the CometAPI key.

The official CometAPI provider repository documents the @cometapi/ai-sdk-provider package, its default cometapi provider instance, and streaming support. Install that package with the AI SDK:

npm install ai @cometapi/ai-sdk-provider

Set the server environment variable named COMETAPI_KEY through .env.local during local development or through the deployment platform’s protected environment settings. This article intentionally omits a sample credential assignment. Restart the development server after changing local environment settings.

Keep the model ID separately configurable:

COMETAPI_MODEL_ID=your-model-id

Replace your-model-id with an available CometAPI model ID before testing. The CometAPI SDK configuration guide identifies an invalid model ID as a common integration error.

Create app/api/completion/route.ts:

import { cometapi } from '@cometapi/ai-sdk-provider';
import {
  createUIMessageStreamResponse,
  streamText,
  toUIMessageStream,
} from 'ai';

const MODEL_ID = process.env.COMETAPI_MODEL_ID ?? 'your-model-id';
const MAX_PROMPT_CHARS = 4_000;

function readPrompt(body: unknown): string {
  if (typeof body !== 'object' || body === null) return '';

  const value = (body as Record<string, unknown>).prompt;
  return typeof value === 'string' ? value.trim() : '';
}

export async function POST(request: Request) {
  const requestId = crypto.randomUUID();
  const startedAt = Date.now();

  if (!process.env.COMETAPI_KEY) {
    console.error('cometapi_route_rejected', {
      request_id: requestId,
      route: '/api/completion',
      method: 'POST',
      model_id: MODEL_ID,
      outcome: 'missing_server_configuration',
      status: 500,
      elapsed_ms: Date.now() - startedAt,
    });

    return Response.json(
      {
        error: 'Server configuration is incomplete.',
        request_id: requestId,
      },
      { status: 500 },
    );
  }

  let body: unknown;

  try {
    body = await request.json();
  } catch {
    console.warn('cometapi_route_rejected', {
      request_id: requestId,
      route: '/api/completion',
      method: 'POST',
      model_id: MODEL_ID,
      outcome: 'invalid_json',
      status: 400,
      elapsed_ms: Date.now() - startedAt,
    });

    return Response.json(
      { error: 'Request body must be valid JSON.', request_id: requestId },
      { status: 400 },
    );
  }

  const prompt = readPrompt(body);

  if (!prompt || prompt.length > MAX_PROMPT_CHARS) {
    console.warn('cometapi_route_rejected', {
      request_id: requestId,
      route: '/api/completion',
      method: 'POST',
      model_id: MODEL_ID,
      outcome: 'invalid_prompt',
      status: 400,
      prompt_chars: prompt.length,
      elapsed_ms: Date.now() - startedAt,
    });

    return Response.json(
      {
        error: 'Prompt must contain between 1 and 4000 characters.',
        request_id: requestId,
      },
      { status: 400 },
    );
  }

  try {
    const result = streamText({
      model: cometapi(MODEL_ID),
      system: 'Answer clearly and concisely.',
      prompt,
    });

    console.info('cometapi_stream_created', {
      request_id: requestId,
      route: '/api/completion',
      method: 'POST',
      model_id: MODEL_ID,
      outcome: 'stream_created',
      status: 200,
      prompt_chars: prompt.length,
      elapsed_ms: Date.now() - startedAt,
    });

    return createUIMessageStreamResponse({
      stream: toUIMessageStream({ stream: result.stream }),
    });
  } catch (error) {
    console.error('cometapi_stream_start_failed', {
      request_id: requestId,
      route: '/api/completion',
      method: 'POST',
      model_id: MODEL_ID,
      outcome: 'stream_start_failed',
      status: 502,
      error_name: error instanceof Error ? error.name : 'UnknownError',
      elapsed_ms: Date.now() - startedAt,
    });

    return Response.json(
      { error: 'Unable to start the response stream.', request_id: requestId },
      { status: 502 },
    );
  }
}

The 4,000-character ceiling is an application rule in this example, not a documented CometAPI limit. Adjust it deliberately for your product. The final catch covers failures raised while constructing the stream. It cannot guarantee a JSON error after streaming headers have already been sent, so late stream failures require separate client handling and platform observation.

Who this is for

This tutorial is for TypeScript developers using the Next.js App Router who want a small, inspectable server boundary for streamed model output. You should already have a Next.js application, server-side CometAPI access configured, and a model ID selected for your test.

It is deliberately narrower than a complete multi-turn chat product. The route accepts one prompt, streams one response, and establishes boundaries that a larger chat UI can reuse. If the project is public or shared, first keep CometAPI keys out of the client and repository .

Key takeaways

  • Keep COMETAPI_KEY on the server and let the browser call a same-origin Next.js route.
  • Use the default cometapi provider with streamText, then convert the result to the UI stream shape expected by the AI SDK client.
  • Treat model IDs, request payloads, and stream response format as explicit contracts.
  • Log identifiers and outcomes, not prompts, completions, headers, request bodies, or environment values.
  • Test failures before release because errors can occur either before streaming starts or after a response has begun.

Sources checked

  • The CometAPI Provider for Vercel AI SDK documents installation, the default provider instance, the COMETAPI_KEY environment variable, model selection, and streamText usage.
  • The CometAPI OpenAI SDK guide confirms the API-key, compatible base-URL, and model-ID configuration contract and lists common configuration errors.
  • Vercel’s Next.js Stream Text cookbook shows a POST Route Handler calling streamText and converting the result into a UI message stream for the client.
  • The Next.js Route Handlers guide confirms that handlers live in route.js or route.ts files under app, support POST, and use the Web Request and Response APIs.

These four refetched public pages are the factual source set for this implementation. The example adds local validation and sanitized logging as application-level safeguards; it does not present those choices as provider limits.

Contract details to verify

Provider and model contract

The dedicated package’s default provider reads COMETAPI_KEY from the server environment and accepts a model ID when the language model instance is selected. Keep the model ID configurable so you can change it without editing the route. Before deployment, use the model catalog verification checklist and replace the placeholder with an ID that is currently available to your account.

Do not silently fall back to another model in this first route. A visible configuration failure is easier to diagnose than a successful response from an unintended model. Add fallback behavior only after defining which failures are retryable and how the UI will disclose a model change.

Route and request contract

The file location determines the route. Under the App Router, app/api/completion/route.ts defines the completion handler without adding a separate Pages Router API route. The example exports POST because the client sends user input. A request must contain valid JSON with a nonempty string in prompt.

The handler trims whitespace and rejects empty or oversized prompts before contacting the provider. That produces a stable local 400 response and avoids sending a request the application already considers invalid. If you later accept message arrays, define and validate every permitted role and content field instead of passing an unchecked browser payload through to the provider.

Streaming response contract

The Vercel cookbook converts the stream returned by streamText into a UI message stream. Preserve that response format when the client uses the matching AI SDK hook. Returning ordinary JSON on the success path or buffering the whole model result would change the client contract and remove incremental rendering.

Streaming also divides failures into two phases. Before response headers are sent, the handler can return a normal JSON error with a useful status. After streaming begins, the server may no longer be able to replace the response with a clean JSON body. The client therefore needs a visible failure state, and operators need server or platform evidence for a stream that starts but does not finish.

Sanitized logging contract

The example records only fields that help correlate and classify a request:

FieldPurpose
eventStable name for filtering route events
request_idRuntime correlation value
route and methodIdentifies the handler contract
model_idShows which configured model was requested
outcome and statusSeparates validation, setup, and stream creation results
prompt_charsCaptures request size without storing the prompt
elapsed_msMeasures time to rejection or stream creation
error_nameClassifies an immediate exception without copying its message

Do not log environment contents, authorization data, raw headers, the full request body, prompt text, generated text, or an unreviewed upstream error body. A successful stream_created event means the response stream was constructed; it does not prove that every chunk reached the browser.

Happy-path operator workflow

  1. Set the required server environment and a verified model ID, then restart the Next.js process.
  2. Submit a short, non-sensitive prompt through the application client.
  3. Confirm the route returns a successful streaming response and the interface updates incrementally.
  4. Find one cometapi_stream_created event with the expected route, method, and model ID.
  5. Confirm the log contains prompt_chars but not prompt text, generated text, headers, or environment values.
  6. Repeat once after deployment so the production runtime, environment configuration, and streaming path are all exercised.

Error-path operator workflow

  1. Send an empty prompt or malformed JSON and confirm the route returns 400 without contacting the provider.
  2. In a controlled local test, temporarily remove COMETAPI_KEY from the process environment. Confirm the route returns the generic 500 response and logs missing_server_configuration without logging environment contents.
  3. Set COMETAPI_MODEL_ID to not-a-real-model in a controlled test. Observe whether the provider failure arrives before or after the stream begins; do not assume it will use the same response shape as local validation.
  4. Record the request time, route, configured model ID, outcome, status if available, and whether any chunks reached the client.
  5. Restore the valid configuration and rerun the happy path. Do not close the incident until streaming succeeds and logs remain sanitized.

Failure modes

The server environment is incomplete or rejected. The local guard returns a generic 500 when the required variable is absent. If a request reaches CometAPI with unusable credentials, the CometAPI guide lists 401 as a configuration symptom. Check the server environment and deployment scope without printing sensitive values.

The model ID is invalid or unavailable. The CometAPI guide explicitly identifies an invalid model ID as a common error. Verify the catalog rather than guessing an alias. Keep the attempted model ID in sanitized logs because it is necessary for diagnosis.

The route is in the wrong directory. Next.js Route Handlers are available under app and use a route.js or route.ts convention. A route file cannot occupy the same segment as a page file. If the browser receives a framework 404, confirm the file path before changing provider code.

The request uses the wrong method. Next.js supports POST Route Handlers, while unsupported methods receive 405. Confirm that the client sends POST and that another route file has not taken over the same segment.

The request body is malformed or outside the local limit. This example returns 400 for invalid JSON, an empty prompt, or more than 4,000 prompt characters. Keep this failure separate from provider errors so operators know the request never left the application.

The stream starts and then stops. A client disconnect, runtime interruption, or later provider failure can occur after the response begins. The pre-stream catch cannot rewrite a response whose headers have already been sent. Give the UI a visible failed state, retain sanitized platform logs, and consider the client timeout pattern before production use.

A generic compatible client targets the wrong base URL. This tutorial uses the dedicated CometAPI provider. If you replace it with a generic OpenAI-compatible SDK, the CometAPI guide says to configure the CometAPI base URL and include its /v1 suffix. Do not mix both client configurations in one route while diagnosing a failure.

Logs become a second data leak. Raw prompts, completions, headers, bodies, and upstream error payloads may contain sensitive user or configuration data. Log the allowlisted fields above and send a generic error message to the browser.

FAQ

Why use the dedicated CometAPI provider instead of a generic client?

The dedicated package exposes CometAPI as an AI SDK provider, so the route can pass its model object directly to streamText. That keeps the example aligned with the Vercel streaming helpers. CometAPI also documents a generic OpenAI-compatible setup, but that is a different client configuration and should not be combined casually with this one.

Is this a complete multi-turn chat implementation?

No. It is a minimal single-turn streaming route. That is useful because it verifies environment setup, model selection, route placement, payload validation, and stream transport with a small surface area. Add conversation history only after defining a validated message schema and deciding how much context the server should accept.

Does the CometAPI key ever need to reach the browser?

No. In this pattern, the browser calls the local Next.js route, and the default provider reads the server environment. Do not place the key in client code, browser storage, query parameters, or a public environment variable.

Why not log the prompt when debugging?

Prompt text is not necessary to identify most configuration and routing failures. Start with request ID, route, model ID, size, status, outcome, and elapsed time. Use a controlled non-sensitive reproduction if content inspection becomes essential, and keep that material out of routine logs.

What should I do when the client receives no text?

First determine whether the request was rejected locally, failed before stream creation, or started streaming and then stopped. Check the browser response status, the cometapi_route_rejected and cometapi_stream_start_failed events, and the presence of cometapi_stream_created. Then verify the model ID and server configuration from the documented contract.

Can I change models without rewriting the route?

Yes. Keep the model ID in server configuration as shown, verify that the replacement ID is available, restart or redeploy as required by your platform, and repeat both the happy path and controlled error checks. Do not assume two models share identical behavior merely because they use the same route.

Reader next step

Implement the route with a verified model ID, connect your existing AI SDK client to the completion endpoint, and run one happy-path request plus the malformed-body, missing-configuration, and invalid-model checks. Confirm that the interface streams incrementally and that every log entry stays within the sanitized field list.

Before merging, review the CometAPI model catalog checks and client-side timeout handling . Those checks turn the minimal route into a better-defined integration boundary without hiding provider or streaming failures.