功能(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 logger from '../../utils/logger.js';
|
||||
import { countTextTokens } from '../../utils/token-utils.js';
|
||||
import { BaseConverter } from '../BaseConverter.js';
|
||||
import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
|
||||
import { ConverterFactory } from '../ConverterFactory.js';
|
||||
|
||||
/**
|
||||
* Grok转换器类
|
||||
|
|
@ -21,6 +23,8 @@ export class GrokConverter extends BaseConverter {
|
|||
super('grok');
|
||||
// 用于跟踪每个请求的状态
|
||||
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: "",
|
||||
uuid: null,
|
||||
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);
|
||||
}
|
||||
|
||||
_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)
|
||||
*/
|
||||
|
|
@ -268,6 +443,12 @@ export class GrokConverter extends BaseConverter {
|
|||
return this.toOpenAIResponsesResponse(data, model);
|
||||
case MODEL_PROTOCOL_PREFIX.CODEX:
|
||||
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:
|
||||
return data;
|
||||
}
|
||||
|
|
@ -276,7 +457,7 @@ export class GrokConverter extends BaseConverter {
|
|||
/**
|
||||
* 转换流式响应块
|
||||
*/
|
||||
convertStreamChunk(chunk, targetProtocol, model) {
|
||||
convertStreamChunk(chunk, targetProtocol, model, requestId) {
|
||||
switch (targetProtocol) {
|
||||
case MODEL_PROTOCOL_PREFIX.OPENAI:
|
||||
return this.toOpenAIStreamChunk(chunk, model);
|
||||
|
|
@ -286,6 +467,48 @@ export class GrokConverter extends BaseConverter {
|
|||
return this.toOpenAIResponsesStreamChunk(chunk, model);
|
||||
case MODEL_PROTOCOL_PREFIX.CODEX:
|
||||
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:
|
||||
return chunk;
|
||||
}
|
||||
|
|
@ -555,8 +778,20 @@ export class GrokConverter extends BaseConverter {
|
|||
}
|
||||
|
||||
// 解析工具调用
|
||||
const contentForTokenCount = 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 = {
|
||||
id: responseId,
|
||||
object: "chat.completion",
|
||||
|
|
@ -571,17 +806,14 @@ export class GrokConverter extends BaseConverter {
|
|||
},
|
||||
finish_reason: toolCalls ? "tool_calls" : "stop",
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
},
|
||||
usage,
|
||||
};
|
||||
|
||||
if (toolCalls) {
|
||||
result.choices[0].message.tool_calls = toolCalls;
|
||||
}
|
||||
|
||||
this._maybeLogGrokUsage('unary', model, result.id, result.usage);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -619,6 +851,15 @@ export class GrokConverter extends BaseConverter {
|
|||
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 = [];
|
||||
|
||||
// 0. 发送角色信息(仅第一次)
|
||||
|
|
@ -657,6 +898,17 @@ export class GrokConverter extends BaseConverter {
|
|||
// 处理 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) {
|
||||
chunks.push({
|
||||
id: responseId,
|
||||
|
|
@ -664,6 +916,7 @@ export class GrokConverter extends BaseConverter {
|
|||
created: Math.floor(Date.now() / 1000),
|
||||
model: model,
|
||||
system_fingerprint: state.fingerprint,
|
||||
usage: terminalUsage,
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: {
|
||||
|
|
@ -680,6 +933,7 @@ export class GrokConverter extends BaseConverter {
|
|||
created: Math.floor(Date.now() / 1000),
|
||||
model: model,
|
||||
system_fingerprint: state.fingerprint,
|
||||
usage: terminalUsage,
|
||||
choices: [{
|
||||
index: 0,
|
||||
delta: { content: finalContent || null },
|
||||
|
|
|
|||
|
|
@ -67,6 +67,15 @@ function normalizeGrokModelId(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 {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
|
|
@ -722,8 +731,17 @@ export class GrokApiService {
|
|||
|
||||
try {
|
||||
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.usage && typeof resp.usage === 'object') {
|
||||
if (!collected.usage) collected.usage = {};
|
||||
Object.assign(collected.usage, resp.usage);
|
||||
}
|
||||
|
||||
// 增加原始输出日志以排查多图生成问题
|
||||
if (resp.cardAttachment || resp.streamingImageGenerationResponse || resp.modelResponse?.cardAttachmentsJson) {
|
||||
|
|
@ -756,6 +774,15 @@ export class GrokApiService {
|
|||
} else {
|
||||
// 合并 modelResponse 中的消息
|
||||
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 (如果存在且未预过滤,但此处通常已由流预处理)
|
||||
if (Array.isArray(mr.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) {
|
||||
logger.warn(`[Grok] Error during collection, but partial results exist. Returning what we have: ${error.message}`);
|
||||
attachGrokUsageEstimatePayload(collected, requestBody);
|
||||
return collected;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
attachGrokUsageEstimatePayload(collected, requestBody);
|
||||
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.`);
|
||||
}
|
||||
|
||||
attachGrokUsageEstimatePayload(collected, requestBody);
|
||||
return collected;
|
||||
}
|
||||
|
||||
|
|
@ -1112,7 +1142,8 @@ export class GrokApiService {
|
|||
if (collected.cardAttachments.length === 0) {
|
||||
throw new Error("WebSocket generation returned no images");
|
||||
}
|
||||
|
||||
|
||||
attachGrokUsageEstimatePayload(collected, requestBody);
|
||||
return collected;
|
||||
}
|
||||
|
||||
|
|
@ -1308,6 +1339,7 @@ export class GrokApiService {
|
|||
});
|
||||
const rl = readline.createInterface({ input: response.data, terminal: false });
|
||||
let lastResponseId = payload.responseMetadata?.requestModelDetails?.modelId || "final";
|
||||
let grokStreamUsagePayloadAttached = false;
|
||||
|
||||
for await (const line of rl) {
|
||||
const trimmed = line.trim();
|
||||
|
|
@ -1316,6 +1348,14 @@ export class GrokApiService {
|
|||
if (dataStr === '[DONE]') break;
|
||||
try {
|
||||
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) {
|
||||
const resp = json.result.response;
|
||||
resp._requestBaseUrl = reqBaseUrl;
|
||||
|
|
@ -1392,9 +1432,21 @@ export class GrokApiService {
|
|||
}
|
||||
}
|
||||
hasYieldedData = true;
|
||||
if (process.env.GROK_LOG_LAST_CHUNK === '1' || /^true$/i.test(process.env.GROK_LOG_LAST_CHUNK || '')) {
|
||||
this._grokLastStreamJsonForDebug = json;
|
||||
}
|
||||
yield json;
|
||||
} 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 } } };
|
||||
} catch (error) {
|
||||
const { status, errorCode, errorMessage, isNetworkError } = this.classifyApiError(error);
|
||||
|
|
|
|||
Loading…
Reference in a new issue