25 mins
4958 words
Loading views
askmany 反代理实战:把私有 SSE 接口翻译成 OpenAI 标准格式,顺便扒清它的扣费规则

官网标"免费"的模型为什么被扣了 1.2 电量?本文从抓包逆向 askmany.com 的私有对话协议开始,手写一个零依赖的 Node.js 反代理,把它的 SSE 接口翻译成 OpenAI /v1/chat/completions 标准格式,再用官方账单接口完整还原扣费公式:基础费 + 长输入附加费。

事情的起因是一笔”莫名其妙”的扣费:我把一个 agent 工具接到自己搭的反代理上跑了几次,回头一看 askmany 的电量账单,少了 1.2。可官网明明写着这些模型是”免费”的。

为了搞清楚这 1.2 扣在哪,我把 askmany.com 的对话协议完整抓包逆向了一遍,手写了一个零依赖的 Node.js 反代理,把它的私有接口翻译成 OpenAI 标准格式,最后用官方的账单接口把计费公式整个扒了出来。结论先说:官网没骗人,基础提问费确实是 0,扣掉的是”长输入附加费”。

📑 目录h2


一、协议分析:私有接口长什么样h2

1.1 核心对话接口h3

askmany 的对话能力只暴露在一个私有接口上:

POST https://askmany.com/api/engine/sseQuery
Content-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"
}

askmany 不用标准的 Authorization 请求头,而是把 JWT 放在一个名字就叫 Authorization 的 Cookie 里,值自带 Bearer 前缀并带引号,同时附带设备标识 Cookie:

Cookie: Authorization="Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; ama_uid=<uuid>
X-Client: web
X-Client-Version: 2.0.0
Origin: https://askmany.com
Referer: 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 字段上游字段转换逻辑
modelengine直传,可配置别名映射
messages 全部历史query折叠为 [System]…[User]…[Assistant]… 转录文本
messages 最后一条 userwoTitle截前 30 字符作会话标题素材
reasoning_effortthinking_effort直传
stream—仅决定代理输出格式,上游恒为 SSE
每次生成 UUIDv4六个会话 id 字段与抓包行为一致
—new_session=1、turn=0、perfect_prompt=1 等固定值复刻抓包默认值

响应方向:

上游事件OpenAI 输出
thinkingdelta: {content: null, reasoning_content: "…"}(DeepSeek 风格,主流客户端直接显示思考)
respdelta: {content: "…"}
all_donefinish_reason: "stop" 然后 data: [DONE]
上游非 200OpenAI 风格错误 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 | drop
const 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

两个踩坑记录:

  1. Windows 终端直接 curl 传中文会乱码——命令行按 GBK 编码传参,上游收到的是乱码,模型还会反过来吐槽”用户输入乱码”。解决办法:把请求体写成 UTF-8 的 JSON 文件,用 --data-binary @body.json 发送。
  2. 多轮上下文的代价:无状态折叠意味着历史全部计入输入长度。如果业务系统提示词很大(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-flashDeepSeek-V4.1-Flash00.2
deepseek-v4-flashDeepSeek-V4-Flash00.2
gemini-3.1-flashGemini-3.1-Flash00.2
gpt-6-lunaGPT-6-Luna00.84
gpt-5.6-lunaGPT-5.6-luna00.84
Doubao-lite豆包 2.001.1
deepseek-v4-proDeepSeek-V4-Pro10.6
claude-sonnet-5Claude-Sonnet-532.4(限时优惠到 2026.8.31)
claude-sonnet-4-6Claude-Sonnet-4.641.2
gpt-5.5GPT-5.544.2
gemini-3.1-pro-previewGemini-3.1-Pro42
gemini-3.8-flashGemini-3.8-Flash21.5
gpt-6-solGPT-6-Sol22.1
claude-opus-5-5Claude-Opus-5.584.2
claude-opus-5Claude-Opus-5104.2
gemini-3.1-pro-stable尊享版13.52
gpt-6-astraGPT-6-Astra208.4
claude-fable-5-1Claude-Fable-5.1208.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 ~ 230短输入(含全部反代理测试与抓包中的 “hi”)
99(system + 3 轮折叠)0反代理多轮测试,带 900 字符思考输出
42,174 / 42,236 / 42,446各 1.2同一份 agent 大系统提示词,三次调用

三个确定性结论:

  1. 思考过程不单独计费——产生几百字符 reasoning 的调用扣费仍为 0;
  2. 多轮折叠的历史本身不是计费因子——计费只看输入规模档位,不看请求形态;
  3. 长输入按档收费——约 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 系统提示词喂进去触发的长输入附加费——正常网页聊天永远到不了那个量级,所以官方敢标免费。

给想折腾的同学三条建议:

  1. 把大提示词场景当成唯一耗电源来管:agent 类工具接反代理前,先估一下系统提示词的体量;纯短对话用免费模型基本无消耗;
  2. 长输入选低附加单价的模型:gpt-5-nano(0.02)、gpt-4.1-nano(0.025)比 deepseek-flash(0.2)便宜一个数量级;
  3. 仅自用:反代理消耗的是绑定账号的真实额度,别公开服务, JWT 别提交到仓库,最好配余额预检防止打空。

Author: 夜猫子Ai手记
Post: askmany 反代理实战:把私有 SSE 接口翻译成 OpenAI 标准格式,顺便扒清它的扣费规则

Comments