事情的起因是一笔”莫名其妙”的扣费:我把一个 agent 工具接到自己搭的反代理上跑了几次,回头一看 askmany 的电量账单,少了 1.2。可官网明明写着这些模型是”免费”的。
为了搞清楚这 1.2 扣在哪,我把 askmany.com 的对话协议完整抓包逆向了一遍,手写了一个零依赖的 Node.js 反代理,把它的私有接口翻译成 OpenAI 标准格式,最后用官方的账单接口把计费公式整个扒了出来。结论先说:官网没骗人,基础提问费确实是 0,扣掉的是”长输入附加费”。
📑 目录h2
一、协议分析:私有接口长什么样h2
1.1 核心对话接口h3
askmany 的对话能力只暴露在一个私有接口上:
POST https://askmany.com/api/engine/sseQueryContent-Type: application/json请求体是自研格式,抓包示例如下(所有 id 都是标准的 UUIDv4):
{ "session_group_id": "66179e48-e8d7-4525-8786-c01fb01e67c2", "engine": "deepseek-flash", "query": "hi", "ask_again": 0, "new_session": 1, "recover_session": 0, "files": [], "file_ids": [], "images": [], "attached_chats": [], "enhance": 0, "image_size": "1:1", "perfect_prompt": 1, "web_search": false, "thinking_effort": "", "role": "user", "woTitle": "hi", "question_id": "336075d5-b8b5-4683-a4e6-8ddad42260b7", "turn": 0, "position": 0, "query_id": "2c8f5f4b-30dd-4349-8dda-65c7671365d7", "conv_id": "b8331808-3756-4940-bd79-f9588e22c25a", "retry": 0, "group_id": "1602c1c5-473b-4ba7-8a88-caf01270f623", "session_id": "ab6bd70c-147e-43a3-b19b-768e0c1cf246"}1.2 鉴权:JWT 藏在 Cookie 里h3
askmany 不用标准的 Authorization 请求头,而是把 JWT 放在一个名字就叫 Authorization 的 Cookie 里,值自带 Bearer 前缀并带引号,同时附带设备标识 Cookie:
Cookie: Authorization="Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; ama_uid=<uuid>X-Client: webX-Client-Version: 2.0.0Origin: https://askmany.comReferer: https://askmany.com/X-Client、Origin、Referer 这些浏览器指纹头缺一不可,服务端会校验来源。
1.3 SSE 事件流h3
响应是 text/event-stream,共三种事件,客户端逐块拼接 content 字段即可还原完整回复:
data: {"event": "thinking", "content": "> 思考中..."} ← 思考过程data: {"event": "thinking", "content": "The"}data: {"event": "resp", "content": "Hi"} ← 实际回复data: {"event": "resp", "content": "!"}data: {"event": "all_done"} ← 结束标志1.4 关键发现:会话字段全部由客户端生成h3
抓包里所有会话标识(session_group_id、conv_id、question_id、query_id、group_id、session_id)全是客户端生成的 UUIDv4,多轮上下文由服务端按 session_group_id 维持(attached_chats 为空)。这意味着反代理可以完全无状态:每次请求现场生成一组新 id、置 new_session=1,把对话历史折叠进 query 即可,不需要在代理层维护任何会话状态。
二、反代理实现(零依赖 Node.js)h2
2.1 架构h3
OpenAI 客户端 askmany-proxy(本地) askmany 上游───────────── ───────────────────── ─────────────POST /v1/chat/completions ► 协议翻译层 ─────────────────────► POST /api/engine/sseQuery(标准 OpenAI 格式) · messages → query 折叠 (私有格式 + JWT Cookie) · model → engine · 生成 UUIDv4 会话字段 · Bearer 头 → Cookie 鉴权 · 伪造浏览器指纹头 ◄── SSE 事件翻译 ────────────────── ◄── text/event-stream标准 OpenAI chunk 流 · thinking → reasoning_content thinking / resp / all_done(reasoning_content/content) · resp → content · all_done → finish_reason + [DONE]2.2 协议映射h3
请求方向:
| OpenAI 字段 | 上游字段 | 转换逻辑 |
|---|---|---|
model | engine | 直传,可配置别名映射 |
messages 全部历史 | query | 折叠为 [System]…[User]…[Assistant]… 转录文本 |
messages 最后一条 user | woTitle | 截前 30 字符作会话标题素材 |
reasoning_effort | thinking_effort | 直传 |
stream | — | 仅决定代理输出格式,上游恒为 SSE |
| 每次生成 UUIDv4 | 六个会话 id 字段 | 与抓包行为一致 |
| — | new_session=1、turn=0、perfect_prompt=1 等固定值 | 复刻抓包默认值 |
响应方向:
| 上游事件 | OpenAI 输出 |
|---|---|
thinking | delta: {content: null, reasoning_content: "…"}(DeepSeek 风格,主流客户端直接显示思考) |
resp | delta: {content: "…"} |
all_done | finish_reason: "stop" 然后 data: [DONE] |
| 上游非 200 | OpenAI 风格错误 JSON(401/502 语义透传) |
2.3 完整代码h3
零第三方依赖,Node.js 18+(用到内置 fetch 和 Web Streams),保存为 server.js 直接运行:
#!/usr/bin/env node/** * askmany-proxy — 本地 OpenAI 兼容反代理(零依赖,Node >= 18) * 配置优先级:环境变量(大写同名)> config.json > 内置默认值 */'use strict';
const http = require('http');const crypto = require('crypto');const fs = require('fs');const path = require('path');
/* ---------------- 配置 ---------------- */const fileConfig = (() => { try { return JSON.parse(fs.readFileSync(path.join(__dirname, 'config.json'), 'utf8')); } catch { return {}; }})();const cfg = (key, def) => process.env[key.toUpperCase()] ?? fileConfig[key] ?? def;
const PORT = Number(cfg('port', 3000));const HOST = String(cfg('host', '127.0.0.1'));const UPSTREAM = String(cfg('upstream', 'https://askmany.com')).replace(/\/+$/, '');const ASKMANY_TOKEN = String(cfg('askmany_token', ''));const AMA_UID = String(cfg('ama_uid', '')) || crypto.randomUUID();const PROXY_API_KEY = String(cfg('proxy_api_key', ''));const DEFAULT_MODEL = String(cfg('default_model', 'deepseek-flash'));const MODELS = String(cfg('models', 'deepseek-flash,doubao-lite,gpt-6-luna')).split(',').map(s => s.trim()).filter(Boolean);const ALIASES = (fileConfig.aliases && typeof fileConfig.aliases === 'object') ? fileConfig.aliases : {};const THINKING_MODE = String(cfg('thinking_mode', 'reasoning_content')); // reasoning_content | inline | dropconst DEBUG = cfg('debug', '') === '1' || cfg('debug', '') === true;const IDLE_TIMEOUT_MS = Number(cfg('idle_timeout_ms', 120000));
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36';
/* ---------------- 工具 ---------------- */const uuid = () => crypto.randomUUID();const nowSec = () => Math.floor(Date.now() / 1000);const approxTokens = (s) => Math.ceil(s.length / 4);
const textOf = (content) => { if (typeof content === 'string') return content; if (Array.isArray(content)) { return content.map((p) => (p && typeof p === 'object' && typeof p.text === 'string') ? p.text : '').join(''); } return '';};
function sendJSON(res, status, obj) { if (res.writableEnded || res.destroyed) return; res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(obj));}
function sendError(res, status, message, type = 'api_error') { sendJSON(res, status, { error: { message, type, param: null, code: status } });}
const log = (...a) => console.log(new Date().toISOString(), ...a);const debug = (...a) => { if (DEBUG) log('[debug]', ...a); };
/* ---------------- 上游请求 ---------------- */function upstreamHeaders(token) { const headers = { 'Content-Type': 'application/json', 'X-Client': 'web', 'X-Client-Version': '2.0.0', 'Origin': UPSTREAM, 'Referer': UPSTREAM + '/', 'User-Agent': UA, 'Accept': 'text/event-stream, application/json', }; // 抓包确认:JWT 放在名为 Authorization 的 Cookie 中,值带 Bearer 前缀 if (token) headers['Cookie'] = `Authorization="Bearer ${token}"; ama_uid=${AMA_UID}`; return headers;}
/** * OpenAI messages -> 单条 query。 * 上游多轮上下文由服务端按 session_group_id 维护, * 这里每次都 new_session=1 新建会话,把历史折叠进 prompt,代理完全无状态。 */function flattenMessages(messages) { const sys = messages .filter((m) => m.role === 'system' || m.role === 'developer') .map((m) => textOf(m.content)).join('\n').trim(); const turns = messages.filter((m) => m.role === 'user' || m.role === 'assistant'); const parts = []; if (sys) parts.push(`[System]\n${sys}`); for (const m of turns) parts.push(`[${m.role === 'user' ? 'User' : 'Assistant'}]\n${textOf(m.content)}`); return parts.join('\n\n');}
function buildPayload(engine, query, { reasoningEffort, woTitle }) { // 所有 id 均为客户端生成的 UUIDv4(与抓包一致) return { session_group_id: uuid(), engine, query, ask_again: 0, new_session: 1, recover_session: 0, files: [], file_ids: [], images: [], attached_chats: [], enhance: 0, image_size: '1:1', perfect_prompt: 1, web_search: false, banana_image_size: '1K', gpt_image_size: '1024x1024', thinking_effort: reasoningEffort || '', role: 'user', woTitle, file_lengths: [], picked: false, question_id: uuid(), turn: 0, position: 0, query_id: uuid(), conv_id: uuid(), retry: 0, group_id: uuid(), session_id: uuid(), };}
/* ---------------- /v1/chat/completions ---------------- */function openAIChunk(id, model, delta, finishReason = null) { return { id, object: 'chat.completion.chunk', created: nowSec(), model, choices: [{ index: 0, delta, finish_reason: finishReason }], };}
function clientBearer(req) { const h = req.headers.authorization || ''; return h.startsWith('Bearer ') ? h.slice(7).trim() : '';}
async function handleChatCompletions(req, res, body) { const requestedModel = body.model || DEFAULT_MODEL; const engine = ALIASES[requestedModel] || requestedModel; const stream = !!body.stream; const messages = Array.isArray(body.messages) ? body.messages : []; if (!messages.length) return sendError(res, 400, 'messages 不能为空', 'invalid_request_error');
const token = ASKMANY_TOKEN || clientBearer(req); if (!token) { return sendError(res, 401, '未配置上游凭据:请在 config.json 设置 askmany_token,或在请求中携带可透传的 Bearer Token', 'authentication_error'); }
const query = flattenMessages(messages); const lastUser = [...messages].reverse().find((m) => m.role === 'user'); const payload = buildPayload(engine, query, { reasoningEffort: body.reasoning_effort, woTitle: textOf(lastUser && lastUser.content).slice(0, 30) || 'conversation', });
const abort = new AbortController(); res.on('close', () => { if (!res.writableEnded) abort.abort(); });
log(`-> ${requestedModel}${engine !== requestedModel ? ` (via ${engine})` : ''} stream=${stream} query=${query.length}chars`); let upstream; try { upstream = await fetch(`${UPSTREAM}/api/engine/sseQuery`, { method: 'POST', headers: upstreamHeaders(token), body: JSON.stringify(payload), signal: abort.signal, }); } catch (e) { return sendError(res, 502, `上游连接失败: ${e.message}`); }
const ctype = upstream.headers.get('content-type') || ''; if (!upstream.ok || !ctype.includes('text/event-stream')) { const text = await upstream.text().catch(() => ''); debug('上游异常响应', upstream.status, text.slice(0, 300)); return sendError(res, upstream.status === 401 ? 401 : 502, `上游返回 ${upstream.status}: ${text.slice(0, 300)}`); }
const chatId = `chatcmpl-${uuid()}`; let content = ''; let reasoning = ''; let inThink = false; // inline 模式下标记 <think> 是否已开
if (stream) { res.writeHead(200, { 'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-cache, no-transform', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', }); res.write(`data: ${JSON.stringify(openAIChunk(chatId, requestedModel, { role: 'assistant', content: null }))}\n\n`); }
const emit = (delta) => res.write(`data: ${JSON.stringify(openAIChunk(chatId, requestedModel, delta))}\n\n`); const emitThinking = (chunk) => { if (THINKING_MODE === 'drop') return; if (THINKING_MODE === 'inline') { if (!inThink) { inThink = true; emit({ content: '<think>' }); } return emit({ content: chunk }); } emit({ content: null, reasoning_content: chunk }); }; const emitAnswer = (chunk) => { if (inThink) { inThink = false; emit({ content: '</think>\n' }); } emit({ content: chunk, ...(THINKING_MODE === 'reasoning_content' ? { reasoning_content: null } : {}) }); };
// 上游 SSE 逐行解析 + OpenAI chunk 输出 const reader = upstream.body.getReader(); const decoder = new TextDecoder(); let buf = ''; let idleTimer = setTimeout(() => abort.abort(), IDLE_TIMEOUT_MS);
const usage = () => ({ prompt_tokens: approxTokens(query), completion_tokens: approxTokens(content), total_tokens: approxTokens(query) + approxTokens(content), });
const finish = () => { clearTimeout(idleTimer); if (res.writableEnded || res.destroyed) return; if (stream) { res.write(`data: ${JSON.stringify(openAIChunk(chatId, requestedModel, {}, 'stop'))}\n\n`); if (body.stream_options && body.stream_options.include_usage) { res.write(`data: ${JSON.stringify({ id: chatId, object: 'chat.completion.chunk', created: nowSec(), model: requestedModel, choices: [], usage: usage(), })}\n\n`); } res.write('data: [DONE]\n\n'); res.end(); } else { const message = { role: 'assistant', content }; if (reasoning && THINKING_MODE === 'reasoning_content') message.reasoning_content = reasoning; sendJSON(res, 200, { id: chatId, object: 'chat.completion', created: nowSec(), model: requestedModel, choices: [{ index: 0, message, finish_reason: 'stop' }], usage: usage(), }); } log(`<- done content=${content.length}chars reasoning=${reasoning.length}chars`); };
try { for (;;) { const { done, value } = await reader.read(); if (done) break; clearTimeout(idleTimer); idleTimer = setTimeout(() => abort.abort(), IDLE_TIMEOUT_MS); buf += decoder.decode(value, { stream: true }); let nl; while ((nl = buf.indexOf('\n')) >= 0) { const line = buf.slice(0, nl).replace(/\r$/, ''); buf = buf.slice(nl + 1); if (!line.startsWith('data:')) continue; const dataStr = line.slice(5).trim(); if (!dataStr || dataStr === '[DONE]') continue; let evt; try { evt = JSON.parse(dataStr); } catch { debug('非 JSON data:', dataStr.slice(0, 200)); continue; } switch (evt.event) { case 'thinking': reasoning += evt.content || ''; if (stream) emitThinking(evt.content || ''); break; case 'resp': content += evt.content || ''; if (stream) emitAnswer(evt.content || ''); break; case 'all_done': break; case 'error': log('上游错误事件:', dataStr.slice(0, 300)); if (stream) emit({ content: `\n[上游错误: ${evt.message || dataStr.slice(0, 200)}]` }); else content += `\n[上游错误: ${evt.message || dataStr.slice(0, 200)}]`; break; default: debug('忽略事件:', evt.event); } } } } catch (e) { clearTimeout(idleTimer); if (res.writableEnded || res.destroyed) return; if (abort.signal.aborted) { // 客户端主动断开或空闲超时 log('连接中止:', e.message); try { res.end(); } catch {} return; } const note = `\n[askmany-proxy: 上游流中断 ${e.message}]`; if (stream) { if (content || reasoning) emit({ content: note }); else return sendError(res, 502, `上游流中断: ${e.message}`); } else if (!content) { return sendError(res, 502, `上游流中断: ${e.message}`); } else { content += note; } } finish();}
/* ---------------- 其他路由 ---------------- */function handleModels(res) { const all = [...new Set([...MODELS, DEFAULT_MODEL, ...Object.keys(ALIASES)])]; sendJSON(res, 200, { object: 'list', data: all.map((id) => ({ id, object: 'model', created: 0, owned_by: 'askmany-proxy' })), });}
/* ---------------- HTTP 服务 ---------------- */const server = http.createServer((req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, X-Api-Key'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); if (req.method === 'OPTIONS') { res.writeHead(204); return res.end(); }
const pathname = new URL(req.url, 'http://localhost').pathname.replace(/\/+$/, ''); const route = pathname.replace(/^\/v1/, '') || '/'; // /v1/xxx 与 /xxx 等价
if (route === '/health') return sendJSON(res, 200, { ok: true, upstream: UPSTREAM, models: MODELS });
if (PROXY_API_KEY) { const auth = req.headers.authorization || ''; if (auth !== `Bearer ${PROXY_API_KEY}`) { return sendError(res, 401, '无效的 API Key', 'authentication_error'); } }
if (req.method === 'GET' && route === '/models') return handleModels(res);
if (req.method === 'POST' && route === '/chat/completions') { let raw = ''; let oversized = false; req.on('data', (c) => { raw += c; if (raw.length > 20 * 1024 * 1024) { oversized = true; req.destroy(); } }); req.on('end', () => { if (oversized) return; let body; try { body = raw ? JSON.parse(raw) : {}; } catch { return sendError(res, 400, '请求体不是合法 JSON', 'invalid_request_error'); } handleChatCompletions(req, res, body) .catch((e) => sendError(res, 500, `代理内部错误: ${e.message}`)); }); return; }
sendError(res, 404, `未知路由: ${req.method} ${pathname}(可用: GET /v1/models, POST /v1/chat/completions)`);});
server.listen(PORT, HOST, () => { log(`askmany-proxy 已启动: http://${HOST}:${PORT}/v1`); log(`上游: ${UPSTREAM} | 默认模型: ${DEFAULT_MODEL} | thinking_mode: ${THINKING_MODE}`); log(`上游凭据: ${ASKMANY_TOKEN ? '已从配置加载' : '未配置(将透传客户端 Bearer Token)'}`); log(`鉴权: ${PROXY_API_KEY ? '已开启(proxy_api_key)' : '未开启(仅本机访问)'}`);});
process.on('uncaughtException', (e) => log('[uncaught]', e.message));process.on('unhandledRejection', (e) => log('[unhandled]', e && (e.message || e)));2.4 配置文件h3
config.json(JWT 是账号凭据,注意保密,环境变量大写同名可覆盖任意配置):
{ "port": 3000, "host": "127.0.0.1", "upstream": "https://askmany.com", "askmany_token": "eyJ...(从浏览器登录后的 Authorization Cookie 整体复制,含 Bearer 前缀)", "ama_uid": "(同上,抓 ama_uid Cookie)", "default_model": "deepseek-flash", "models": "deepseek-flash,doubao-lite,gpt-6-luna", "aliases": {}, "thinking_mode": "reasoning_content", "proxy_api_key": "", "debug": "1"}几个关键配置:thinking_mode 控制思考过程的输出方式(reasoning_content 独立字段 / inline 用 <think> 标签包进正文 / drop 丢弃);aliases 可以做模型别名映射,比如把 gpt-4o-mini 映射到某个实际引擎;proxy_api_key 设置后代理自身需要鉴权,挂公网时务必开启。
2.5 使用h3
任何 OpenAI 客户端,base_url 填 http://127.0.0.1:3000/v1,API Key 随意填:
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:3000/v1", api_key="anything")resp = client.chat.completions.create( model="deepseek-flash", messages=[{"role": "user", "content": "你好"}], stream=True,)for chunk in resp: delta = chunk.choices[0].delta if getattr(delta, "reasoning_content", None): # 思考过程 print(delta.reasoning_content, end="", flush=True) if delta.content: print(delta.content, end="", flush=True)Cherry Studio、ChatBox、Open WebUI 等 GUI 工具同理,供应商类型选 OpenAI 即可。
三、实测效果h2
| 测试项 | 结果 |
|---|---|
| 流式对话 | 41 个标准 chunk,思考与正文分离正确,finish_reason:stop + [DONE] 正常 |
| 非流式对话 | 标准 chat.completion JSON,含 usage 字段 |
| 多轮上下文 | system + 3 轮历史折叠后,模型正确答出前文中的暗号 |
| 上游计费影响 | 上述短输入调用全部扣费为 0 |
两个踩坑记录:
- Windows 终端直接 curl 传中文会乱码——命令行按 GBK 编码传参,上游收到的是乱码,模型还会反过来吐槽”用户输入乱码”。解决办法:把请求体写成 UTF-8 的 JSON 文件,用
--data-binary @body.json发送。 - 多轮上下文的代价:无状态折叠意味着历史全部计入输入长度。如果业务系统提示词很大(agent 框架动辄几万字符),会触发上游的长输入附加费——这正是下一部分的主角。
四、官方扣费机制全解析h2
4.1 电量体系:三个池子h3
askmany 用”电量”(battery)作为全站统一的用量计量单位。账户接口 POST /api/pay/accountInfo 返回三个独立电量池:
remain_free_battery:免费电量,实测上限max_free_battery = 50,新用户注册一次性赠送 50;remain_vip_battery:会员电量,VIP 套餐专属池;remain_extend_battery:扩展电量,对应充值购买的延长包。
配套字段 enable_battery_cost = 1 表示该账号已启用电量计费;long_input_battery_cost_notice = true 表示”长输入额外消耗电量”的提示开关已开(前端有个确认弹窗,确认一次不再弹)。实测中 remain_requests 与免费电量数值完全同步,可视为余额镜像。免费电量的上限是 50,用完即止。
4.2 双层计费:基础费 + 附加费h3
官方计费配置由 POST /api/pay/batteryCosts 下发(无需请求体),每个引擎有两个价格参数:
battery_cost:基础提问费,一次调用的固定起步价;delta_query_battery_cost:附加单价,按次叠加的增量费率。
官方说明页的原话是:“提问免费AI时,电池消耗最低为0;提问付费AI时,电池消耗最低为1。” 这与计费表完全对应:battery_cost = 0 的引擎就是官网宣传的”免费模型”(如 deepseek-flash、gpt-6-luna、Doubao-lite);battery_cost ≥ 1 的则是付费模型。模型分四类计价:思考模型类(主力聊天/推理)、联网搜索类、传统对话类(GPT-4o-mini、GLM 等旧款)、绘画类(按张计费,起步价最高)。
于是单次调用费用的通用结构就是:
单次扣费 =
battery_cost(基础费,随模型固定) + 附加费(随输入规模等条件浮动,费率取该模型的delta_query_battery_cost)
免费模型(基础费 0)并非永远 0 扣费——附加费部分照收,这就是”官网标免费却被扣了 1.2”的根源。
4.3 全模型计费总表(2026-09-28 实抓)h3
思考模型类(节选,完整表可通过 batteryCosts 接口获取):
| 引擎 | 展示名 | 基础费 | 附加单价 |
|---|---|---|---|
| deepseek-flash | DeepSeek-V4.1-Flash | 0 | 0.2 |
| deepseek-v4-flash | DeepSeek-V4-Flash | 0 | 0.2 |
| gemini-3.1-flash | Gemini-3.1-Flash | 0 | 0.2 |
| gpt-6-luna | GPT-6-Luna | 0 | 0.84 |
| gpt-5.6-luna | GPT-5.6-luna | 0 | 0.84 |
| Doubao-lite | 豆包 2.0 | 0 | 1.1 |
| deepseek-v4-pro | DeepSeek-V4-Pro | 1 | 0.6 |
| claude-sonnet-5 | Claude-Sonnet-5 | 3 | 2.4(限时优惠到 2026.8.31) |
| claude-sonnet-4-6 | Claude-Sonnet-4.6 | 4 | 1.2 |
| gpt-5.5 | GPT-5.5 | 4 | 4.2 |
| gemini-3.1-pro-preview | Gemini-3.1-Pro | 4 | 2 |
| gemini-3.8-flash | Gemini-3.8-Flash | 2 | 1.5 |
| gpt-6-sol | GPT-6-Sol | 2 | 2.1 |
| claude-opus-5-5 | Claude-Opus-5.5 | 8 | 4.2 |
| claude-opus-5 | Claude-Opus-5 | 10 | 4.2 |
| gemini-3.1-pro-stable | 尊享版 | 13.5 | 2 |
| gpt-6-astra | GPT-6-Astra | 20 | 8.4 |
| claude-fable-5-1 | Claude-Fable-5.1 | 20 | 8.4 |
传统对话类里还藏着一批零基础费模型:gpt-5-nano(附加单价仅 0.02)、gpt-4.1-nano(0.025)、gemini-2.5-flash(0)、gpt-5-mini、gpt-4o-mini、glm-4-air(均 0.1)。绘画类则是电量黑洞:gpt-image-2 基础费 12、Nano Banana 2 是 33、Nano Banana Pro 高达 65。
4.4 长输入附加费:1.2 的完整证据链h3
通过 POST /api/engine/userBatteryCost(请求体 {"page": 1})拉取消耗流水,每条记录包含 question_id、完整的 title(即发送的 query 原文)、时间戳和扣费额。把全部 12 条账单和输入长度做相关:
| 输入长度(字符) | 扣费 | 说明 |
|---|---|---|
| 2 ~ 23 | 0 | 短输入(含全部反代理测试与抓包中的 “hi”) |
| 99(system + 3 轮折叠) | 0 | 反代理多轮测试,带 900 字符思考输出 |
| 42,174 / 42,236 / 42,446 | 各 1.2 | 同一份 agent 大系统提示词,三次调用 |
三个确定性结论:
- 思考过程不单独计费——产生几百字符 reasoning 的调用扣费仍为 0;
- 多轮折叠的历史本身不是计费因子——计费只看输入规模档位,不看请求形态;
- 长输入按档收费——约 4.2 万字符的三笔全部扣 1.2,而 1.2 = 0.2 × 6,0.2 恰好就是
deepseek-flash的附加单价。
据此反推出计费公式:
长输入附加费 =
delta_query_battery_cost× 档数,档数 ≈ceil(输入规模 / 单位阈值)
由 42,236 ÷ 6 ≈ 7,039 反推,单位阈值在 7~8 千字符量级(若按 8,000 字符一档,ceil(42236/8000) = 6,与账单完全吻合)。免费阈值和计费单位(按字符还是 token)从单一样本定不死,如需精确标定,可以发 2K/5K/8K/12K 字符的受控请求观察扣费跳变点,每次试探成本约 0.2 电量。
顺带一提:gemini-2.5-pro-free 在计费表里被官方注明”无法支持长输入和长思考,可能会不稳定或长期不可用”——进一步佐证”长输入”是平台级的独立计费维度。
4.5 计费演算h3
以免费账号(余额 50)为例:
- 普通聊天:
deepseek-flash,输入 9 字符 → 基础费 0 + 附加费 0 → 扣 0。账单实测一致,短输入对话近乎零成本。 - 长输入 agent:
deepseek-flash,输入 42,236 字符 → 0 + 0.2 × 6 → 扣 1.2。账单实测一致,剩余电量约够 38 次同规模调用。 - 付费模型:
claude-opus-5,短输入 → 基础费 10 → 扣 10,一次调用烧掉免费电量的五分之一。这就是官方文案里”最低为1”的含义——基础费只是下限,附加费另算。 - 余额核验:50 − 3 × 1.2 = 46.4,与
accountInfo.remain_free_battery完全吻合,账实相符,没有隐藏扣费项。
4.6 电量怎么回血:没有签到这件事h3
实测结论:web 端不存在每日签到/打卡/看广告回血机制。依据有三:前端全部 45 个 JS 包里提取出完整的 110 个 API 端点,没有任何签到类动作接口;全量 JS 里搜”签到/打卡/赚电量”零命中;账号的获取流水(userBatteryEarn)全部历史只有一条”新用户注册 +50”。
所以免费电量的现实来源只有三个:注册赠送(一次性 50)、邀请好友(邀请接口返回专属链接,奖励结构里有累计电量字段,证明邀请成功会送电量)、付费充值。桌面或手机 APP 不排除有 web 端没有的活动,但 web 侧无法验证。
4.7 对账 API 清单h3
这几个接口只需要 JWT Cookie 就能调用,全部只读、不产生扣费,接进代理就能实现自动化运营(余额预检、定期对账、异常扣费审计):
| 接口 | 用途 |
|---|---|
POST /api/pay/batteryCosts | 全模型计费表(基础费 + 附加单价) |
POST /api/pay/accountInfo | 余额三池、上限、计费开关 |
POST /api/engine/userBatteryCost | 消耗流水(含每笔完整 query 与扣费额) |
POST /api/engine/userBatteryEarn | 获取流水 |
POST /api/engine/questionBatteryDetails | 单次问答的逐引擎扣费明细 |
4.8 局限与变量h3
三点必须说明:其一,计费规则本身在 A/B 实验中——账户接口返回 in_battert_cost_v3_experiment 标记位,平台正在迭代”电量成本 v3”方案,本文数值仅代表 2026-09-28 的实测快照;其二,长输入公式是从 3 笔同源样本反推的最简模型,阈值细节需受控实验标定;其三,站方存在迁移迹象(指向新域名的迁移横幅),未来接口路径可能变化。
五、写在最后h2
整个事情串起来其实是一条很顺的线:抓包逆出协议 → 无状态反代理跑通 OpenAI 标准格式 → 账单对不上号 → 用官方流水接口逆向出”基础费 + 长输入附加费”的双层计费结构。最终回答了开头的问题:官网的”免费”是真的(基础费 0),扣的 1.2 是把几万字符的 agent 系统提示词喂进去触发的长输入附加费——正常网页聊天永远到不了那个量级,所以官方敢标免费。
给想折腾的同学三条建议:
- 把大提示词场景当成唯一耗电源来管:agent 类工具接反代理前,先估一下系统提示词的体量;纯短对话用免费模型基本无消耗;
- 长输入选低附加单价的模型:
gpt-5-nano(0.02)、gpt-4.1-nano(0.025)比deepseek-flash(0.2)便宜一个数量级; - 仅自用:反代理消耗的是绑定账号的真实额度,别公开服务, JWT 别提交到仓库,最好配余额预检防止打空。
Comments