直接看数字

← 回到全部文章

先用1美元试跑:Next.js 把模型回答逐段送到浏览器

01 / 1美元是测试预算,不是请求次数承诺

先给自己设一个1美元的试验预算,只做一件事:输入一句话,让回答逐段出现在网页上。暂时不做登录、数据库、聊天历史或自动重试。流式传输能让用户先看到已经生成的内容,但不会自动减少模型生成的 token,更不代表价格更低。

本例使用 Next.js App Router 的 Route Handler,也就是 app/api 下的服务端接口,不是旧版 pages/api。链路只有三段:浏览器请求自己的 /api/chat,服务端请求模型,再把收到的文字块传回浏览器。密钥始终留在服务器。

02 / 先装一个依赖,把密钥留在服务端

在已有的 Next.js 项目目录执行:npm install openai。CheapRelay 提供按量付费的 OpenAI 兼容接口,没有月费和订阅;账单按实际 token 计算,也包含缓存读写。需要测试密钥时,可以注册 CheapRelay,再从控制台确认余额、模型标识和当前单价。

在项目根目录的 .env.local 中分别写入下面三行。AI_MODEL 必须填写控制台提供的准确 API 模型标识,不要直接猜展示名称对应的字符串。可以先查看 DeepSeek V4 Flash 的价格,再决定是否使用。

CHEAPRELAY_API_KEY=替换为你的密钥

AI_BASE_URL=https://api.cheapapirelay.com/v1

AI_MODEL=替换为控制台中的模型标识

不要给密钥变量添加 NEXT_PUBLIC_ 前缀,也不要提交 .env.local。修改环境变量后重启开发服务器。这里使用 /v1,因为安装的是 OpenAI SDK。

03 / 用一个 POST 路由把模型流转换成文字流

新建 app/api/chat/route.ts,粘贴下面的代码。代码为了便于直接复制,写成单行;编辑器可以自动格式化。输入最多接受200个字符,输出暂时限制为128个 token。字符数不等于 token 数,这只是两道粗粒度护栏。

import OpenAI from 'openai'; export const runtime = 'nodejs'; const client = new OpenAI({ apiKey: process.env.CHEAPRELAY_API_KEY, baseURL: process.env.AI_BASE_URL, maxRetries: 0 }); export async function POST(req: Request) { let body; try { body = await req.json(); } catch { return new Response('请求必须是 JSON', { status: 400 }); } const prompt = body?.prompt; if (typeof prompt !== 'string' || !prompt.trim() || prompt.length > 200) return new Response('请输入1至200个字符', { status: 400 }); let upstream; try { upstream = await client.chat.completions.create({ model: process.env.AI_MODEL!, messages: [{ role: 'user', content: prompt }], stream: true, max_tokens: 128 }, { signal: req.signal }); } catch { return new Response('模型请求失败,请检查服务端日志和配置', { status: 502 }); } const encoder = new TextEncoder(); const stream = new ReadableStream({ async start(controller) { try { for await (const chunk of upstream) { const text = chunk.choices[0]?.delta?.content; if (text) controller.enqueue(encoder.encode(text)); } controller.close(); } catch (error) { try { controller.error(error); } catch {} } }, cancel() { upstream.controller.abort(); } }); return new Response(stream, { headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-cache, no-transform' } }); }

上游通过 SDK 解析流式事件,下游只传纯文本,因此浏览器不必自己解析事件协议。maxRetries: 0 关闭 SDK 自动重试,方便第一次排查费用和错误。max_tokens 的支持情况仍需以所选模型文档为准;若接口拒绝该参数,先核对文档,不要连续重发碰运气。

04 / 浏览器逐块读取,不等整段回答结束

把下面代码放入 app/page.tsx。它使用 fetch 而不是 EventSource,因为这里需要发送 POST 请求。TextDecoder 的流式模式可以处理跨网络分块的中文字符。

'use client'; import { useState } from 'react'; export default function Page() { const [prompt, setPrompt] = useState('用三句话解释流式输出'); const [answer, setAnswer] = useState(''); const [busy, setBusy] = useState(false); async function send() { setBusy(true); setAnswer(''); try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt }) }); if (!response.ok) throw new Error(await response.text()); if (!response.body) throw new Error('浏览器未收到响应流'); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { value, done } = await reader.read(); if (done) break; const text = decoder.decode(value, { stream: true }); setAnswer(previous => previous + text); } const tail = decoder.decode(); if (tail) setAnswer(previous => previous + tail); } catch (error) { setAnswer(previous => previous + '\n请求未完成:' + (error instanceof Error ? error.message : '未知错误')); } finally { setBusy(false); } } return (<main><textarea value={prompt} maxLength={200} onChange={event => setPrompt(event.target.value)} /><button disabled={busy || !prompt.trim()} onClick={send}>{busy ? '接收中' : '发送一次'}</button><pre>{answer}</pre></main>); }

执行 npm run dev,打开本地页面,只发送一次。回答应逐段追加,但一块不一定对应一个字或一个 token。代理、压缩和部署平台都可能缓冲内容;如果本地能流式显示,部署后却一次出现,应检查平台的流式支持与执行时限。

05 / 用实际账单决定是否继续花这1美元

测试前后查看用量记录,不要把余额显示的小数位当成精确的单次成本。若拿到了输入、输出 token 数,单次费用可按“输入 token × 输入单价+输出 token × 输出单价”计算,并按报价单位换算;涉及缓存时,还要纳入对应读写费用。

  • ■ 128是输出上限,不是整次请求的总 token 上限,也不是固定收费金额。
  • ■ 1美元只是人工预算;如果没有设置可执行的消费限制,程序不会在预算用完时自动停止。
  • ■ 发送按钮只阻止当前页面重复点击,不能防止别人直接调用接口。公开部署前必须补上鉴权、服务端限流和用量限制。
  • ■ 断开连接后的取消只是尽力而为,已经生成或计费的内容不会因此退回。

第一版先验证三件事:页面逐段显示、浏览器拿不到密钥、账单能对应这次请求。通过后再增加功能,比先搭完整聊天产品更适合小预算。

注册 价格自己试,注册就送试用额度。

顺手再看

日期货架语言标题
2026-10-28协议细节ZH每月固定费 0 元:别让 SSE 解析错误浪费 token
从字节解码、跨块换行到事件拼接,逐步拆解 SSE 解析。先用本地测试覆盖边界,再接入真实 AI 流,减少小预算项目的无效重试。
2026-10-28协议细节ZH先把输出限到128个令牌:低预算读懂Anthropic Messages API
从OpenAI聊天接口迁移,先弄清system位置、内容块、请求头与令牌用量。用128个输出令牌做小测试,避免把格式错误和重复上下文变成账单。
2026-10-29少花一点ZH100次调用0.18美元:先算token,再选接口
预算很小时,Responses API和Chat Completions该选哪个?用一张假设账单,讲清输入、输出、缓存、历史记录与推理token的成本区别。
2026-10-29模型比价ZH¥0月费,1000张收据先做小额分类账
用MiniMax M3和Qwen3.7 Plus做收据分类:从标签设计、抽样测试到真实令牌账单,适合预算很小的初学者。
2026-10-29少花一点ZH把输出上限从500降到150:小预算提示词入门
从一条客服提示词开始,练习明确任务、限制输出、精简上下文和小批量测试;用可核对的词元账单,找到真正省钱的改法。
2026-10-29省钱用编码 AgentZH先定10元预算:用Cline接入便宜中转API
从安装Cline、填写中转地址和模型ID,到完成第一次小改动,用10元试用预算讲清配置、费用核对与停止条件。