功能(Grok):优化 GrokConverter 与 GrokApiService 中的使用量追踪及估算功能
在 GrokConverter 中新增方法,用于详细提取与估算使用量,包含上游使用量处理及降级机制。 在 GrokApiService 中添加功能,当上游令牌字段缺失时自动附加使用量估算数据。 优化多响应来源的使用量数据合并逻辑,确保追踪精准。 基于环境变量实现使用量统计日志记录,提升可监控性。
This commit is contained in:
parent
51c0af2f15
commit
952b20c9b7
2 changed files with 315 additions and 9 deletions
|
|
@ -5,8 +5,10 @@
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import logger from '../../utils/logger.js';
|
import logger from '../../utils/logger.js';
|
||||||
|
import { countTextTokens } from '../../utils/token-utils.js';
|
||||||
import { BaseConverter } from '../BaseConverter.js';
|
import { BaseConverter } from '../BaseConverter.js';
|
||||||
import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
|
import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
|
||||||
|
import { ConverterFactory } from '../ConverterFactory.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grok转换器类
|
* Grok转换器类
|
||||||
|
|
@ -21,6 +23,8 @@ export class GrokConverter extends BaseConverter {
|
||||||
super('grok');
|
super('grok');
|
||||||
// 用于跟踪每个请求的状态
|
// 用于跟踪每个请求的状态
|
||||||
this.requestStates = new Map();
|
this.requestStates = new Map();
|
||||||
|
/** @type {Map<string, boolean>} 流式 Claude 转换是否已发送 message_start(按 streamRequestId) */
|
||||||
|
this._claudeMsgStartSent = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -111,12 +115,183 @@ export class GrokConverter extends BaseConverter {
|
||||||
requestBaseUrl: "",
|
requestBaseUrl: "",
|
||||||
uuid: null,
|
uuid: null,
|
||||||
seen_images: new Set(), // 用于去重已输出的图片
|
seen_images: new Set(), // 用于去重已输出的图片
|
||||||
pending_text_buffer: "" // 用于处理流式输出中被截断的 URL
|
pending_text_buffer: "", // 用于处理流式输出中被截断的 URL
|
||||||
|
usageAcc: null, // 流式过程中最后一次解析到的上游用量(末包常为合成 isDone 无用量)
|
||||||
|
usageEstimatePayload: null // grok-core 注入的 prompt/tools 文本,用于本地估算
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return this.requestStates.get(requestId);
|
return this.requestStates.get(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_nTok(v) {
|
||||||
|
const n = Number(v);
|
||||||
|
return Number.isFinite(n) && n >= 0 ? Math.trunc(n) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_packOpenAIUsage(prompt, completion, total) {
|
||||||
|
const pt = this._nTok(prompt);
|
||||||
|
const ct = this._nTok(completion);
|
||||||
|
let tt = this._nTok(total);
|
||||||
|
if (!tt && (pt || ct)) tt = pt + ct;
|
||||||
|
if (!pt && !ct && !tt) return null;
|
||||||
|
return { prompt_tokens: pt, completion_tokens: ct, total_tokens: tt || pt + ct };
|
||||||
|
}
|
||||||
|
|
||||||
|
_usageFromUsageLike(u) {
|
||||||
|
if (!u || typeof u !== "object") return null;
|
||||||
|
return this._packOpenAIUsage(
|
||||||
|
u.prompt_tokens ?? u.input_tokens ?? u.promptTokens ?? u.inputTokens
|
||||||
|
?? u.prompt_token_count ?? u.input_token_count,
|
||||||
|
u.completion_tokens ?? u.output_tokens ?? u.completionTokens ?? u.outputTokens
|
||||||
|
?? u.completion_token_count ?? u.output_token_count,
|
||||||
|
u.total_tokens ?? u.totalTokens ?? u.total_token_count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_usageFromLlmInfoLike(li) {
|
||||||
|
if (!li || typeof li !== "object") return null;
|
||||||
|
return this._packOpenAIUsage(
|
||||||
|
li.inputTokens ?? li.promptTokens ?? li.prompt_tokens ?? li.input_tokens
|
||||||
|
?? li.prompt_token_count ?? li.input_token_count,
|
||||||
|
li.outputTokens ?? li.completionTokens ?? li.completion_tokens ?? li.output_tokens
|
||||||
|
?? li.completion_token_count ?? li.output_token_count,
|
||||||
|
li.totalTokens ?? li.total_tokens ?? li.total_token_count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_usageRank(u) {
|
||||||
|
return u ? (u.total_tokens || u.prompt_tokens + u.completion_tokens) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_usageFromRecord(node) {
|
||||||
|
if (!node || typeof node !== "object") return null;
|
||||||
|
return this._usageFromUsageLike(node.usage)
|
||||||
|
|| this._usageFromUsageLike(node.tokenUsage)
|
||||||
|
|| this._usageFromLlmInfoLike(node.llmInfo)
|
||||||
|
|| this._usageFromLlmInfoLike(node.llm_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
_bestUsageFromNodes(nodes) {
|
||||||
|
let best = null;
|
||||||
|
for (const node of nodes) {
|
||||||
|
const u = this._usageFromRecord(node);
|
||||||
|
if (u && this._usageRank(u) >= this._usageRank(best)) best = u;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
_preferHigherUsage(a, b) {
|
||||||
|
if (this._usageRank(b) > this._usageRank(a)) return b || a;
|
||||||
|
return a || b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上游无有效 usage 时,用 Claude tokenizer 估算(与 Grok/xAI 官方计费可能不一致,仅作展示/配额参考)
|
||||||
|
*/
|
||||||
|
_fillUsageWithEstimateIfNeeded(upstream, payload, completionText) {
|
||||||
|
if (process.env.GROK_DISABLE_USAGE_ESTIMATE === '1' || /^true$/i.test(process.env.GROK_DISABLE_USAGE_ESTIMATE || '')) {
|
||||||
|
return upstream && typeof upstream === 'object'
|
||||||
|
? upstream
|
||||||
|
: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
||||||
|
}
|
||||||
|
const u = upstream && typeof upstream === 'object'
|
||||||
|
? upstream
|
||||||
|
: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
||||||
|
if (this._usageRank(u) > 0) {
|
||||||
|
return {
|
||||||
|
prompt_tokens: u.prompt_tokens,
|
||||||
|
completion_tokens: u.completion_tokens,
|
||||||
|
total_tokens: u.total_tokens || u.prompt_tokens + u.completion_tokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const promptStr = `${payload?.promptText ?? ''}${payload?.toolsJson ?? ''}`;
|
||||||
|
const pt = countTextTokens(promptStr);
|
||||||
|
const ct = countTextTokens(completionText || '');
|
||||||
|
return {
|
||||||
|
prompt_tokens: pt,
|
||||||
|
completion_tokens: ct,
|
||||||
|
total_tokens: pt + ct,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在整块 JSON 内深度查找类 usage 对象(Grok 上游字段位置不固定时兜底)
|
||||||
|
*/
|
||||||
|
_deepFindUsage(obj, depth = 0, maxDepth = 6) {
|
||||||
|
if (!obj || typeof obj !== "object" || depth > maxDepth) return null;
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
let best = null;
|
||||||
|
const lim = Math.min(obj.length, 80);
|
||||||
|
for (let i = 0; i < lim; i++) {
|
||||||
|
const u = this._deepFindUsage(obj[i], depth + 1, maxDepth);
|
||||||
|
best = this._preferHigherUsage(best, u);
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
const direct = this._usageFromUsageLike(obj) || this._usageFromLlmInfoLike(obj);
|
||||||
|
if (direct) return direct;
|
||||||
|
let best = null;
|
||||||
|
const keys = Object.keys(obj);
|
||||||
|
const lim = Math.min(keys.length, 80);
|
||||||
|
for (let i = 0; i < lim; i++) {
|
||||||
|
const v = obj[keys[i]];
|
||||||
|
if (v == null || typeof v !== "object") continue;
|
||||||
|
const u = this._deepFindUsage(v, depth + 1, maxDepth);
|
||||||
|
best = this._preferHigherUsage(best, u);
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Grok app-chat 流式块解析用量(兼容 result 层、response、modelResponse.metadata.llm_info 等)
|
||||||
|
*/
|
||||||
|
_extractGrokUsageFromChunk(grokChunk, resp) {
|
||||||
|
const nodes = [];
|
||||||
|
if (grokChunk?.result) nodes.push(grokChunk.result);
|
||||||
|
if (resp) nodes.push(resp);
|
||||||
|
if (resp?.modelResponse) {
|
||||||
|
nodes.push(resp.modelResponse);
|
||||||
|
const md = resp.modelResponse.metadata;
|
||||||
|
if (md) {
|
||||||
|
nodes.push(md);
|
||||||
|
if (md.llm_info) nodes.push(md.llm_info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const shallow = this._bestUsageFromNodes(nodes);
|
||||||
|
const deep = this._deepFindUsage(grokChunk, 0, 6);
|
||||||
|
return this._preferHigherUsage(shallow, deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从非流式聚合结果解析用量
|
||||||
|
*/
|
||||||
|
_extractGrokUsageFromCollected(grokResponse) {
|
||||||
|
const nodes = [grokResponse, grokResponse?.modelResponse];
|
||||||
|
if (grokResponse?.usage) nodes.push({ usage: grokResponse.usage });
|
||||||
|
const md = grokResponse?.modelResponse?.metadata;
|
||||||
|
if (md) {
|
||||||
|
nodes.push(md);
|
||||||
|
if (md.llm_info) nodes.push(md.llm_info);
|
||||||
|
}
|
||||||
|
if (grokResponse?.llmInfo) nodes.push(grokResponse.llmInfo);
|
||||||
|
const shallow = this._bestUsageFromNodes(nodes);
|
||||||
|
const deep = this._deepFindUsage(grokResponse, 0, 6);
|
||||||
|
return this._preferHigherUsage(shallow, deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部署后验证用量:环境变量 GROK_LOG_USAGE=1(或 true)时,每次完成响应打一行 info,默认关闭。
|
||||||
|
*/
|
||||||
|
_maybeLogGrokUsage(kind, model, responseId, usage) {
|
||||||
|
const flag = process.env.GROK_LOG_USAGE;
|
||||||
|
if (flag !== '1' && !/^true$/i.test(String(flag || ''))) return;
|
||||||
|
if (!usage) return;
|
||||||
|
logger.info(
|
||||||
|
`[Grok usage] ${kind} model=${model ?? '?'} id=${responseId ?? '?'} ` +
|
||||||
|
`in=${usage.prompt_tokens} out=${usage.completion_tokens} total=${usage.total_tokens}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建工具系统提示词 (build_tool_prompt)
|
* 构建工具系统提示词 (build_tool_prompt)
|
||||||
*/
|
*/
|
||||||
|
|
@ -268,6 +443,12 @@ export class GrokConverter extends BaseConverter {
|
||||||
return this.toOpenAIResponsesResponse(data, model);
|
return this.toOpenAIResponsesResponse(data, model);
|
||||||
case MODEL_PROTOCOL_PREFIX.CODEX:
|
case MODEL_PROTOCOL_PREFIX.CODEX:
|
||||||
return this.toCodexResponse(data, model);
|
return this.toCodexResponse(data, model);
|
||||||
|
case MODEL_PROTOCOL_PREFIX.CLAUDE: {
|
||||||
|
const openaiRes = this.toOpenAIResponse(data, model);
|
||||||
|
if (!openaiRes) return data;
|
||||||
|
const openaiConverter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI);
|
||||||
|
return openaiConverter.toClaudeResponse(openaiRes, model);
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
@ -276,7 +457,7 @@ export class GrokConverter extends BaseConverter {
|
||||||
/**
|
/**
|
||||||
* 转换流式响应块
|
* 转换流式响应块
|
||||||
*/
|
*/
|
||||||
convertStreamChunk(chunk, targetProtocol, model) {
|
convertStreamChunk(chunk, targetProtocol, model, requestId) {
|
||||||
switch (targetProtocol) {
|
switch (targetProtocol) {
|
||||||
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
||||||
return this.toOpenAIStreamChunk(chunk, model);
|
return this.toOpenAIStreamChunk(chunk, model);
|
||||||
|
|
@ -286,6 +467,48 @@ export class GrokConverter extends BaseConverter {
|
||||||
return this.toOpenAIResponsesStreamChunk(chunk, model);
|
return this.toOpenAIResponsesStreamChunk(chunk, model);
|
||||||
case MODEL_PROTOCOL_PREFIX.CODEX:
|
case MODEL_PROTOCOL_PREFIX.CODEX:
|
||||||
return this.toCodexStreamChunk(chunk, model);
|
return this.toCodexStreamChunk(chunk, model);
|
||||||
|
case MODEL_PROTOCOL_PREFIX.CLAUDE: {
|
||||||
|
const openaiPieces = this.toOpenAIStreamChunk(chunk, model);
|
||||||
|
if (!openaiPieces) return null;
|
||||||
|
const key = requestId || '_';
|
||||||
|
const openaiConverter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.OPENAI);
|
||||||
|
const pieces = Array.isArray(openaiPieces) ? openaiPieces : [openaiPieces];
|
||||||
|
const out = [];
|
||||||
|
for (const p of pieces) {
|
||||||
|
const events = openaiConverter.toClaudeStreamChunk(p, model);
|
||||||
|
if (!events) continue;
|
||||||
|
const arr = Array.isArray(events) ? events : [events];
|
||||||
|
for (const ev of arr) {
|
||||||
|
if (!this._claudeMsgStartSent.get(key)) {
|
||||||
|
this._claudeMsgStartSent.set(key, true);
|
||||||
|
const msgId = `msg_${String(p.id || uuidv4()).replace(/^chatcmpl-/, '')}`;
|
||||||
|
out.push({
|
||||||
|
type: 'message_start',
|
||||||
|
message: {
|
||||||
|
id: msgId,
|
||||||
|
type: 'message',
|
||||||
|
role: 'assistant',
|
||||||
|
content: [],
|
||||||
|
model: model || p.model || 'unknown',
|
||||||
|
stop_reason: null,
|
||||||
|
stop_sequence: null,
|
||||||
|
usage: {
|
||||||
|
input_tokens: 0,
|
||||||
|
output_tokens: 0,
|
||||||
|
cache_creation_input_tokens: 0,
|
||||||
|
cache_read_input_tokens: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
out.push(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chunk?.result?.response?.isDone) {
|
||||||
|
this._claudeMsgStartSent.delete(key);
|
||||||
|
}
|
||||||
|
return out.length === 0 ? null : (out.length === 1 ? out[0] : out);
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return chunk;
|
return chunk;
|
||||||
}
|
}
|
||||||
|
|
@ -555,8 +778,20 @@ export class GrokConverter extends BaseConverter {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析工具调用
|
// 解析工具调用
|
||||||
|
const contentForTokenCount = content;
|
||||||
const { text, toolCalls } = this.parseToolCalls(content);
|
const { text, toolCalls } = this.parseToolCalls(content);
|
||||||
|
|
||||||
|
let usage = this._extractGrokUsageFromCollected(grokResponse) || {
|
||||||
|
prompt_tokens: 0,
|
||||||
|
completion_tokens: 0,
|
||||||
|
total_tokens: 0,
|
||||||
|
};
|
||||||
|
usage = this._fillUsageWithEstimateIfNeeded(
|
||||||
|
usage,
|
||||||
|
grokResponse._grokUsageEstimatePayload,
|
||||||
|
contentForTokenCount
|
||||||
|
);
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
id: responseId,
|
id: responseId,
|
||||||
object: "chat.completion",
|
object: "chat.completion",
|
||||||
|
|
@ -571,17 +806,14 @@ export class GrokConverter extends BaseConverter {
|
||||||
},
|
},
|
||||||
finish_reason: toolCalls ? "tool_calls" : "stop",
|
finish_reason: toolCalls ? "tool_calls" : "stop",
|
||||||
}],
|
}],
|
||||||
usage: {
|
usage,
|
||||||
prompt_tokens: 0,
|
|
||||||
completion_tokens: 0,
|
|
||||||
total_tokens: 0,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (toolCalls) {
|
if (toolCalls) {
|
||||||
result.choices[0].message.tool_calls = toolCalls;
|
result.choices[0].message.tool_calls = toolCalls;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._maybeLogGrokUsage('unary', model, result.id, result.usage);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -619,6 +851,15 @@ export class GrokConverter extends BaseConverter {
|
||||||
state.rollout_id = String(resp.rolloutId);
|
state.rollout_id = String(resp.rolloutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const usageHere = this._extractGrokUsageFromChunk(grokChunk, resp);
|
||||||
|
if (usageHere && this._usageRank(usageHere) >= this._usageRank(state.usageAcc)) {
|
||||||
|
state.usageAcc = usageHere;
|
||||||
|
}
|
||||||
|
const est = grokChunk.result?._grokUsageEstimatePayload;
|
||||||
|
if (est && !state.usageEstimatePayload) {
|
||||||
|
state.usageEstimatePayload = est;
|
||||||
|
}
|
||||||
|
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
|
|
||||||
// 0. 发送角色信息(仅第一次)
|
// 0. 发送角色信息(仅第一次)
|
||||||
|
|
@ -657,6 +898,17 @@ export class GrokConverter extends BaseConverter {
|
||||||
// 处理 buffer 中的工具调用
|
// 处理 buffer 中的工具调用
|
||||||
const { text, toolCalls } = this.parseToolCalls(state.content_buffer);
|
const { text, toolCalls } = this.parseToolCalls(state.content_buffer);
|
||||||
|
|
||||||
|
let terminalUsage = state.usageAcc || usageHere || {
|
||||||
|
prompt_tokens: 0,
|
||||||
|
completion_tokens: 0,
|
||||||
|
total_tokens: 0
|
||||||
|
};
|
||||||
|
terminalUsage = this._fillUsageWithEstimateIfNeeded(
|
||||||
|
terminalUsage,
|
||||||
|
state.usageEstimatePayload,
|
||||||
|
state.content_buffer || ''
|
||||||
|
);
|
||||||
|
this._maybeLogGrokUsage('stream', model, responseId, terminalUsage);
|
||||||
if (toolCalls) {
|
if (toolCalls) {
|
||||||
chunks.push({
|
chunks.push({
|
||||||
id: responseId,
|
id: responseId,
|
||||||
|
|
@ -664,6 +916,7 @@ export class GrokConverter extends BaseConverter {
|
||||||
created: Math.floor(Date.now() / 1000),
|
created: Math.floor(Date.now() / 1000),
|
||||||
model: model,
|
model: model,
|
||||||
system_fingerprint: state.fingerprint,
|
system_fingerprint: state.fingerprint,
|
||||||
|
usage: terminalUsage,
|
||||||
choices: [{
|
choices: [{
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: {
|
delta: {
|
||||||
|
|
@ -680,6 +933,7 @@ export class GrokConverter extends BaseConverter {
|
||||||
created: Math.floor(Date.now() / 1000),
|
created: Math.floor(Date.now() / 1000),
|
||||||
model: model,
|
model: model,
|
||||||
system_fingerprint: state.fingerprint,
|
system_fingerprint: state.fingerprint,
|
||||||
|
usage: terminalUsage,
|
||||||
choices: [{
|
choices: [{
|
||||||
index: 0,
|
index: 0,
|
||||||
delta: { content: finalContent || null },
|
delta: { content: finalContent || null },
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,15 @@ function normalizeGrokModelId(modelId) {
|
||||||
return isGrokNsfwModel(modelId) ? modelId.slice(0, -5) : modelId;
|
return isGrokNsfwModel(modelId) ? modelId.slice(0, -5) : modelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 供 GrokConverter 在上游无 token 字段时用 Claude tokenizer 估算(非 Grok 官方计费) */
|
||||||
|
function attachGrokUsageEstimatePayload(collected, requestBody) {
|
||||||
|
if (!collected || !requestBody) return;
|
||||||
|
const promptText = requestBody.message || "";
|
||||||
|
const toolsJson = requestBody.tools && Array.isArray(requestBody.tools) && requestBody.tools.length
|
||||||
|
? JSON.stringify(requestBody.tools) : "";
|
||||||
|
collected._grokUsageEstimatePayload = { promptText, toolsJson };
|
||||||
|
}
|
||||||
|
|
||||||
export class GrokApiService {
|
export class GrokApiService {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
@ -722,8 +731,17 @@ export class GrokApiService {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
const resp = chunk.result?.response;
|
const res = chunk.result;
|
||||||
|
if (res?.usage && typeof res.usage === 'object') {
|
||||||
|
if (!collected.usage) collected.usage = {};
|
||||||
|
Object.assign(collected.usage, res.usage);
|
||||||
|
}
|
||||||
|
const resp = res?.response;
|
||||||
if (!resp) continue;
|
if (!resp) continue;
|
||||||
|
if (resp.usage && typeof resp.usage === 'object') {
|
||||||
|
if (!collected.usage) collected.usage = {};
|
||||||
|
Object.assign(collected.usage, resp.usage);
|
||||||
|
}
|
||||||
|
|
||||||
// 增加原始输出日志以排查多图生成问题
|
// 增加原始输出日志以排查多图生成问题
|
||||||
if (resp.cardAttachment || resp.streamingImageGenerationResponse || resp.modelResponse?.cardAttachmentsJson) {
|
if (resp.cardAttachment || resp.streamingImageGenerationResponse || resp.modelResponse?.cardAttachmentsJson) {
|
||||||
|
|
@ -756,6 +774,15 @@ export class GrokApiService {
|
||||||
} else {
|
} else {
|
||||||
// 合并 modelResponse 中的消息
|
// 合并 modelResponse 中的消息
|
||||||
if (mr.message) collected.modelResponse.message = mr.message;
|
if (mr.message) collected.modelResponse.message = mr.message;
|
||||||
|
if (mr.metadata) {
|
||||||
|
const prev = collected.modelResponse.metadata || {};
|
||||||
|
const next = mr.metadata;
|
||||||
|
const merged = { ...prev, ...next };
|
||||||
|
if (prev.llm_info && next.llm_info && typeof prev.llm_info === 'object' && typeof next.llm_info === 'object') {
|
||||||
|
merged.llm_info = { ...prev.llm_info, ...next.llm_info };
|
||||||
|
}
|
||||||
|
collected.modelResponse.metadata = merged;
|
||||||
|
}
|
||||||
// 合并 cardAttachmentsJson (如果存在且未预过滤,但此处通常已由流预处理)
|
// 合并 cardAttachmentsJson (如果存在且未预过滤,但此处通常已由流预处理)
|
||||||
if (Array.isArray(mr.cardAttachmentsJson)) {
|
if (Array.isArray(mr.cardAttachmentsJson)) {
|
||||||
if (!collected.modelResponse.cardAttachmentsJson) collected.modelResponse.cardAttachmentsJson = [];
|
if (!collected.modelResponse.cardAttachmentsJson) collected.modelResponse.cardAttachmentsJson = [];
|
||||||
|
|
@ -825,10 +852,12 @@ export class GrokApiService {
|
||||||
// 如果已经采集到了图片或视频,则不抛出异常,而是返回已有的结果
|
// 如果已经采集到了图片或视频,则不抛出异常,而是返回已有的结果
|
||||||
if (collected.cardAttachments.length > 0 || collected.generatedImageUrls.length > 0 || collected.finalVideoUrl) {
|
if (collected.cardAttachments.length > 0 || collected.generatedImageUrls.length > 0 || collected.finalVideoUrl) {
|
||||||
logger.warn(`[Grok] Error during collection, but partial results exist. Returning what we have: ${error.message}`);
|
logger.warn(`[Grok] Error during collection, but partial results exist. Returning what we have: ${error.message}`);
|
||||||
|
attachGrokUsageEstimatePayload(collected, requestBody);
|
||||||
return collected;
|
return collected;
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
attachGrokUsageEstimatePayload(collected, requestBody);
|
||||||
return collected;
|
return collected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -994,6 +1023,7 @@ export class GrokApiService {
|
||||||
logger.warn(`[Grok Video Skip] isVideo is TRUE but NO postId found to create share link.`);
|
logger.warn(`[Grok Video Skip] isVideo is TRUE but NO postId found to create share link.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachGrokUsageEstimatePayload(collected, requestBody);
|
||||||
return collected;
|
return collected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1112,7 +1142,8 @@ export class GrokApiService {
|
||||||
if (collected.cardAttachments.length === 0) {
|
if (collected.cardAttachments.length === 0) {
|
||||||
throw new Error("WebSocket generation returned no images");
|
throw new Error("WebSocket generation returned no images");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachGrokUsageEstimatePayload(collected, requestBody);
|
||||||
return collected;
|
return collected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1308,6 +1339,7 @@ export class GrokApiService {
|
||||||
});
|
});
|
||||||
const rl = readline.createInterface({ input: response.data, terminal: false });
|
const rl = readline.createInterface({ input: response.data, terminal: false });
|
||||||
let lastResponseId = payload.responseMetadata?.requestModelDetails?.modelId || "final";
|
let lastResponseId = payload.responseMetadata?.requestModelDetails?.modelId || "final";
|
||||||
|
let grokStreamUsagePayloadAttached = false;
|
||||||
|
|
||||||
for await (const line of rl) {
|
for await (const line of rl) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
|
|
@ -1316,6 +1348,14 @@ export class GrokApiService {
|
||||||
if (dataStr === '[DONE]') break;
|
if (dataStr === '[DONE]') break;
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(dataStr);
|
const json = JSON.parse(dataStr);
|
||||||
|
if (json.result && requestBody && !grokStreamUsagePayloadAttached) {
|
||||||
|
json.result._grokUsageEstimatePayload = {
|
||||||
|
promptText: requestBody.message || "",
|
||||||
|
toolsJson: requestBody.tools && Array.isArray(requestBody.tools) && requestBody.tools.length
|
||||||
|
? JSON.stringify(requestBody.tools) : ""
|
||||||
|
};
|
||||||
|
grokStreamUsagePayloadAttached = true;
|
||||||
|
}
|
||||||
if (json.result?.response) {
|
if (json.result?.response) {
|
||||||
const resp = json.result.response;
|
const resp = json.result.response;
|
||||||
resp._requestBaseUrl = reqBaseUrl;
|
resp._requestBaseUrl = reqBaseUrl;
|
||||||
|
|
@ -1392,9 +1432,21 @@ export class GrokApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hasYieldedData = true;
|
hasYieldedData = true;
|
||||||
|
if (process.env.GROK_LOG_LAST_CHUNK === '1' || /^true$/i.test(process.env.GROK_LOG_LAST_CHUNK || '')) {
|
||||||
|
this._grokLastStreamJsonForDebug = json;
|
||||||
|
}
|
||||||
yield json;
|
yield json;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
if ((process.env.GROK_LOG_LAST_CHUNK === '1' || /^true$/i.test(process.env.GROK_LOG_LAST_CHUNK || '')) && this._grokLastStreamJsonForDebug) {
|
||||||
|
try {
|
||||||
|
const s = JSON.stringify(this._grokLastStreamJsonForDebug);
|
||||||
|
logger.info(`[Grok] Last SSE chunk before synthetic isDone (truncated 4000): ${s.slice(0, 4000)}`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`[Grok] Could not stringify last chunk: ${err.message}`);
|
||||||
|
}
|
||||||
|
this._grokLastStreamJsonForDebug = null;
|
||||||
|
}
|
||||||
yield { result: { response: { isDone: true, responseId: lastResponseId, _requestBaseUrl: reqBaseUrl, _uuid: this.uuid } } };
|
yield { result: { response: { isDone: true, responseId: lastResponseId, _requestBaseUrl: reqBaseUrl, _uuid: this.uuid } } };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const { status, errorCode, errorMessage, isNetworkError } = this.classifyApiError(error);
|
const { status, errorCode, errorMessage, isNetworkError } = this.classifyApiError(error);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue