refactor(*): 代码质量 - 优化前后端实现并完善协议与画布验证
This commit is contained in:
@@ -1,301 +1,218 @@
|
||||
import axios from "axios";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import { consumeJsonEvents } from "@/lib/json-event-stream";
|
||||
import type {
|
||||
ChatCompletionPayload,
|
||||
ChatCompletionStreamState,
|
||||
GeminiPart,
|
||||
GeminiPayload,
|
||||
GeminiStreamState,
|
||||
ImageApiResponse,
|
||||
ResponseApiOutputItem,
|
||||
ResponseApiPayload,
|
||||
ResponseStreamState,
|
||||
ResponseToolCall,
|
||||
ToolResponseResult,
|
||||
ChatCompletionPayload, ChatCompletionStreamState, GeminiPart, GeminiPayload, GeminiStreamState,
|
||||
ImageApiResponse, ResponseApiPayload, ResponseStreamState, ResponseToolCall, ToolResponseResult,
|
||||
} from "@/services/api/image-contracts";
|
||||
|
||||
function resolveImageDataUrl(item: Record<string, unknown>) {
|
||||
if (typeof item.b64_json === "string" && item.b64_json) return `data:image/png;base64,${item.b64_json}`;
|
||||
if (typeof item.url === "string" && item.url) return item.url;
|
||||
return null;
|
||||
type TextSink = (text: string) => void;
|
||||
type TextState = { text: string; reasoning: string };
|
||||
|
||||
function record(value: unknown): Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
export function parseImagePayload(payload: ImageApiResponse) {
|
||||
if (typeof payload.code === "number" && payload.code !== 0) throw new Error(payload.msg || "请求失败");
|
||||
const images =
|
||||
payload.data
|
||||
?.map(resolveImageDataUrl)
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.map((dataUrl) => ({ id: nanoid(), dataUrl })) || [];
|
||||
if (images.length === 0) throw new Error("接口没有返回图片");
|
||||
return images;
|
||||
}
|
||||
|
||||
export function readAxiosError(error: unknown, fallback: string) {
|
||||
if (axios.isCancel(error)) return "请求已取消";
|
||||
if (axios.isAxiosError<{ error?: { message?: string }; msg?: string; code?: number }>(error)) {
|
||||
const responseData = error.response?.data;
|
||||
return responseData?.msg || responseData?.error?.message || readStatusError(error.response?.status, fallback);
|
||||
}
|
||||
if (error instanceof DOMException && error.name === "AbortError") return "请求已取消";
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
}
|
||||
|
||||
export function readStatusError(status: number | undefined, fallback: string) {
|
||||
if (status === 401 || status === 403) return "鉴权失败,请检查 API Key、套餐权限或模型权限";
|
||||
if (status === 429) return "请求被限流或额度不足,请稍后重试";
|
||||
return status ? `${fallback}:${status}` : fallback;
|
||||
}
|
||||
|
||||
export function parseChatCompletionPayload(payload: ChatCompletionPayload): ToolResponseResult {
|
||||
if (typeof payload.code === "number" && payload.code !== 0) throw new Error(payload.msg || "请求失败");
|
||||
if (payload.error?.message) throw new Error(payload.error.message);
|
||||
const message = payload.choices?.[0]?.message;
|
||||
const toolCalls = (message?.tool_calls || [])
|
||||
.map((call) => ({
|
||||
id: call.id || "",
|
||||
type: "function" as const,
|
||||
function: { name: call.function?.name || "", arguments: call.function?.arguments || "{}" },
|
||||
}))
|
||||
.filter((call) => call.id && call.function.name);
|
||||
return { content: message?.content || "", toolCalls, ...(message?.reasoning_content ? { reasoning: message.reasoning_content } : {}) };
|
||||
}
|
||||
|
||||
export function parseToolResponse(payload: ResponseApiPayload): ToolResponseResult {
|
||||
const output = payload.output || [];
|
||||
const content =
|
||||
payload.output_text ||
|
||||
output
|
||||
.flatMap((item) => (item.type === "message" ? item.content || [] : []))
|
||||
.map((item) => item.text || "")
|
||||
.join("");
|
||||
const toolCalls = output
|
||||
.filter((item): item is Extract<ResponseApiOutputItem, { type?: "function_call" }> => item.type === "function_call")
|
||||
.map((item) => ({
|
||||
id: item.call_id || item.id || "",
|
||||
type: "function" as const,
|
||||
function: { name: item.name || "", arguments: item.arguments || "{}" },
|
||||
}))
|
||||
.filter((item) => item.id && item.function.name);
|
||||
return { content, toolCalls };
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function stringValue(value: unknown) {
|
||||
function text(value: unknown): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
export function responseErrorMessage(value: unknown) {
|
||||
if (!isRecord(value)) return "";
|
||||
const error = isRecord(value.error) ? value.error : undefined;
|
||||
const response = isRecord(value.response) ? value.response : undefined;
|
||||
const responseError = response && isRecord(response.error) ? response.error : undefined;
|
||||
return stringValue(value.msg) || stringValue(error?.message) || stringValue(responseError?.message);
|
||||
}
|
||||
|
||||
export function validateResponsePayload(payload: ResponseApiPayload) {
|
||||
function assertEnvelope(payload: { code?: number; msg?: string }) {
|
||||
if (typeof payload.code === "number" && payload.code !== 0) throw new Error(payload.msg || "请求失败");
|
||||
if (payload.error?.message) throw new Error(payload.error.message);
|
||||
}
|
||||
|
||||
export function validateGeminiPayload(payload: GeminiPayload) {
|
||||
if (payload.error?.message) throw new Error(payload.error.message);
|
||||
if (payload.promptFeedback?.blockReason) throw new Error(`Gemini 拒绝了本次请求:${payload.promptFeedback.blockReason}`);
|
||||
function imagesFrom(sources: Iterable<string | undefined | null>, emptyMessage: string) {
|
||||
const images: { id: string; dataUrl: string }[] = [];
|
||||
for (const dataUrl of sources) if (dataUrl) images.push({ id: nanoid(), dataUrl });
|
||||
if (!images.length) throw new Error(emptyMessage);
|
||||
return images;
|
||||
}
|
||||
|
||||
export async function readFetchError(response: Response, fallback: string) {
|
||||
const text = await response.text();
|
||||
if (!text) return readStatusError(response.status, fallback);
|
||||
try {
|
||||
return responseErrorMessage(JSON.parse(text)) || readStatusError(response.status, fallback);
|
||||
} catch {
|
||||
return text.slice(0, 300) || readStatusError(response.status, fallback);
|
||||
}
|
||||
function addTool(result: ToolResponseResult, id: string | undefined, name: string | undefined, args: string | undefined, signature?: string) {
|
||||
if (!id || !name) return;
|
||||
const call: ResponseToolCall = { type: "function", id, function: { name, arguments: args || "{}" } };
|
||||
if (signature) call.thoughtSignature = signature;
|
||||
result.toolCalls.push(call);
|
||||
}
|
||||
|
||||
export async function readJsonPayload<T>(response: Response, fallback: string): Promise<T> {
|
||||
const text = await response.text();
|
||||
try {
|
||||
return JSON.parse(text) as T;
|
||||
} catch {
|
||||
if (/^\s*(?:<!doctype|<html)/i.test(text)) throw new Error("后端代理返回了前端网页,请检查 VITE_CANVAS_BACKEND_URL 和反向代理配置");
|
||||
throw new Error(`${fallback}:接口没有返回有效 JSON`);
|
||||
}
|
||||
function* geminiParts(payload: GeminiPayload): Generator<GeminiPart> {
|
||||
for (const candidate of payload.candidates ?? []) yield* candidate.content?.parts ?? [];
|
||||
}
|
||||
|
||||
function parseEventBlock(block: string) {
|
||||
const data = block
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.startsWith("data:"))
|
||||
.map((line) => line.slice(5).replace(/^ /, ""))
|
||||
.join("\n")
|
||||
.trim();
|
||||
return data && data !== "[DONE]" ? data : undefined;
|
||||
export function parseImagePayload(payload: ImageApiResponse) {
|
||||
assertEnvelope(payload);
|
||||
function* sources() {
|
||||
for (const item of payload.data ?? []) {
|
||||
const encoded = text(item.b64_json);
|
||||
yield encoded ? `data:image/png;base64,${encoded}` : text(item.url);
|
||||
}
|
||||
}
|
||||
return imagesFrom(sources(), "接口没有返回图片");
|
||||
}
|
||||
|
||||
function consumeResponseStreamBlock(block: string, state: ResponseStreamState, onDelta?: (text: string) => void, onReasoning?: (text: string) => void) {
|
||||
const data = parseEventBlock(block);
|
||||
if (!data) return;
|
||||
const event = JSON.parse(data) as Record<string, unknown>;
|
||||
const type = stringValue(event.type);
|
||||
const errorMessage = responseErrorMessage(event);
|
||||
if (errorMessage) state.error = errorMessage;
|
||||
if (type === "response.output_text.delta" && typeof event.delta === "string") {
|
||||
state.text += event.delta;
|
||||
onDelta?.(state.text);
|
||||
}
|
||||
if (type === "response.output_text.done" && !state.text && typeof event.text === "string") {
|
||||
state.text = event.text;
|
||||
onDelta?.(state.text);
|
||||
}
|
||||
if (["response.reasoning.delta", "response.reasoning_text.delta", "response.reasoning_summary_text.delta"].includes(type) && typeof event.delta === "string") {
|
||||
state.reasoning += event.delta;
|
||||
onReasoning?.(state.reasoning);
|
||||
}
|
||||
if (["response.reasoning.done", "response.reasoning_text.done", "response.reasoning_summary_text.done"].includes(type) && !state.reasoning && typeof event.text === "string") {
|
||||
state.reasoning = event.text;
|
||||
onReasoning?.(state.reasoning);
|
||||
}
|
||||
if (type === "response.completed" && isRecord(event.response)) state.payload = event.response as ResponseApiPayload;
|
||||
else if (Array.isArray(event.output)) state.payload = event as ResponseApiPayload;
|
||||
export function parseChatCompletionPayload(payload: ChatCompletionPayload): ToolResponseResult {
|
||||
validateResponsePayload(payload);
|
||||
const message = payload.choices?.[0]?.message;
|
||||
const result: ToolResponseResult = { content: message?.content || "", toolCalls: [] };
|
||||
if (message?.reasoning_content) result.reasoning = message.reasoning_content;
|
||||
for (const call of message?.tool_calls ?? []) addTool(result, call.id, call.function?.name, call.function?.arguments);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function consumeResponseStreamText(state: ResponseStreamState, text: string, onDelta?: (text: string) => void, onReasoning?: (text: string) => void, flush = false) {
|
||||
state.buffer += text;
|
||||
for (;;) {
|
||||
const match = state.buffer.match(/\r?\n\r?\n/);
|
||||
if (!match) break;
|
||||
const index = match.index ?? 0;
|
||||
consumeResponseStreamBlock(state.buffer.slice(0, index), state, onDelta, onReasoning);
|
||||
state.buffer = state.buffer.slice(index + match[0].length);
|
||||
}
|
||||
if (flush && state.buffer.trim()) {
|
||||
consumeResponseStreamBlock(state.buffer, state, onDelta, onReasoning);
|
||||
state.buffer = "";
|
||||
}
|
||||
}
|
||||
|
||||
function consumeChatCompletionStreamBlock(block: string, state: ChatCompletionStreamState, onDelta?: (text: string) => void, onReasoning?: (text: string) => void) {
|
||||
const data = parseEventBlock(block);
|
||||
if (!data) return;
|
||||
const event = JSON.parse(data) as Record<string, unknown>;
|
||||
const errorMessage = responseErrorMessage(event);
|
||||
if (errorMessage) state.error = errorMessage;
|
||||
const choices = Array.isArray(event.choices) ? event.choices : [];
|
||||
const choice = isRecord(choices[0]) ? choices[0] : undefined;
|
||||
const delta = choice && isRecord(choice.delta) ? choice.delta : undefined;
|
||||
if (!delta) return;
|
||||
if (typeof delta.content === "string") {
|
||||
state.text += delta.content;
|
||||
onDelta?.(state.text);
|
||||
}
|
||||
const reasoningDelta = stringValue(delta.reasoning_content) || stringValue(delta.reasoning) || stringValue(delta.reasoning_text);
|
||||
if (reasoningDelta) {
|
||||
state.reasoning += reasoningDelta;
|
||||
onReasoning?.(state.reasoning);
|
||||
}
|
||||
const chunks = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
||||
chunks.forEach((value, fallbackIndex) => {
|
||||
if (!isRecord(value)) return;
|
||||
const callIndex = typeof value.index === "number" ? value.index : fallbackIndex;
|
||||
const current = state.toolCalls.get(callIndex) || { id: "", name: "", arguments: "" };
|
||||
const fn = isRecord(value.function) ? value.function : undefined;
|
||||
state.toolCalls.set(callIndex, {
|
||||
id: stringValue(value.id) || current.id,
|
||||
name: stringValue(fn?.name) || current.name,
|
||||
arguments: current.arguments + stringValue(fn?.arguments),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function consumeChatCompletionStreamText(state: ChatCompletionStreamState, text: string, onDelta?: (text: string) => void, onReasoning?: (text: string) => void, flush = false) {
|
||||
state.buffer += text;
|
||||
for (;;) {
|
||||
const match = state.buffer.match(/\r?\n\r?\n/);
|
||||
if (!match) break;
|
||||
const index = match.index ?? 0;
|
||||
consumeChatCompletionStreamBlock(state.buffer.slice(0, index), state, onDelta, onReasoning);
|
||||
state.buffer = state.buffer.slice(index + match[0].length);
|
||||
}
|
||||
if (flush && state.buffer.trim()) {
|
||||
consumeChatCompletionStreamBlock(state.buffer, state, onDelta, onReasoning);
|
||||
state.buffer = "";
|
||||
}
|
||||
}
|
||||
|
||||
function consumeGeminiStreamBlock(block: string, state: GeminiStreamState, onDelta?: (text: string) => void, onReasoning?: (text: string) => void) {
|
||||
const data = parseEventBlock(block);
|
||||
if (!data) return;
|
||||
const result = parseGeminiToolResponse(JSON.parse(data) as GeminiPayload);
|
||||
if (result.reasoning) {
|
||||
state.reasoning += result.reasoning;
|
||||
onReasoning?.(state.reasoning);
|
||||
}
|
||||
if (result.content) {
|
||||
state.text += result.content;
|
||||
onDelta?.(state.text);
|
||||
}
|
||||
state.toolCalls.push(...result.toolCalls);
|
||||
}
|
||||
|
||||
export function consumeGeminiStreamText(state: GeminiStreamState, text: string, onDelta?: (text: string) => void, onReasoning?: (text: string) => void, flush = false) {
|
||||
state.buffer += text;
|
||||
for (;;) {
|
||||
const match = state.buffer.match(/\r?\n\r?\n/);
|
||||
if (!match) break;
|
||||
const index = match.index ?? 0;
|
||||
consumeGeminiStreamBlock(state.buffer.slice(0, index), state, onDelta, onReasoning);
|
||||
state.buffer = state.buffer.slice(index + match[0].length);
|
||||
}
|
||||
if (flush && state.buffer.trim()) {
|
||||
consumeGeminiStreamBlock(state.buffer, state, onDelta, onReasoning);
|
||||
state.buffer = "";
|
||||
export function parseToolResponse(payload: ResponseApiPayload): ToolResponseResult {
|
||||
const result: ToolResponseResult = { content: "", toolCalls: [] };
|
||||
for (const item of payload.output ?? []) {
|
||||
switch (item.type) {
|
||||
case "message":
|
||||
for (const part of item.content ?? []) result.content += part.text || "";
|
||||
break;
|
||||
case "function_call":
|
||||
addTool(result, item.call_id || item.id, item.name, item.arguments);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (payload.output_text) result.content = payload.output_text;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseGeminiToolResponse(payload: GeminiPayload): ToolResponseResult {
|
||||
validateGeminiPayload(payload);
|
||||
const parts = payload.candidates?.flatMap((candidate) => candidate.content?.parts || []) || [];
|
||||
const content = parts
|
||||
.filter((part) => !part.thought)
|
||||
.map((part) => part.text || "")
|
||||
.join("");
|
||||
const reasoning = parts
|
||||
.filter((part) => part.thought)
|
||||
.map((part) => part.text || "")
|
||||
.join("");
|
||||
const toolCalls = parts
|
||||
.map((part) => part.functionCall)
|
||||
.filter((call): call is NonNullable<GeminiPart["functionCall"]> => Boolean(call?.name))
|
||||
.map((call) => {
|
||||
const part = parts.find((item) => item.functionCall === call);
|
||||
const thoughtSignature = part?.thoughtSignature || part?.thought_signature;
|
||||
return {
|
||||
id: call.id || nanoid(),
|
||||
type: "function" as const,
|
||||
function: { name: call.name || "", arguments: JSON.stringify(call.args || {}) },
|
||||
...(thoughtSignature ? { thoughtSignature } : {}),
|
||||
};
|
||||
});
|
||||
return { content, toolCalls, ...(reasoning ? { reasoning } : {}) };
|
||||
const result: ToolResponseResult = { content: "", toolCalls: [] };
|
||||
let reasoning = "";
|
||||
for (const part of geminiParts(payload)) {
|
||||
if (part.thought) reasoning += part.text || "";
|
||||
else result.content += part.text || "";
|
||||
const call = part.functionCall;
|
||||
if (call?.name) addTool(result, call.id || nanoid(), call.name, JSON.stringify(call.args || {}), part.thoughtSignature || part.thought_signature);
|
||||
}
|
||||
if (reasoning) result.reasoning = reasoning;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseGeminiImagePayload(payload: GeminiPayload) {
|
||||
validateGeminiPayload(payload);
|
||||
const images =
|
||||
payload.candidates
|
||||
?.flatMap((candidate) => candidate.content?.parts || [])
|
||||
.map((part) => {
|
||||
const inlineData = part.inlineData || (part.inline_data ? { mimeType: part.inline_data.mimeType || part.inline_data.mime_type, data: part.inline_data.data } : undefined);
|
||||
if (inlineData?.data) return `data:${inlineData.mimeType || "image/png"};base64,${inlineData.data}`;
|
||||
return part.fileData?.fileUri || null;
|
||||
})
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.map((dataUrl) => ({ id: nanoid(), dataUrl })) || [];
|
||||
if (!images.length) throw new Error("Gemini 接口没有返回图片");
|
||||
return images;
|
||||
function* sources() {
|
||||
for (const part of geminiParts(payload)) {
|
||||
const inline = part.inlineData ?? part.inline_data;
|
||||
const mime = part.inlineData ? part.inlineData.mimeType : part.inline_data?.mimeType || part.inline_data?.mime_type;
|
||||
yield inline?.data ? `data:${mime || "image/png"};base64,${inline.data}` : part.fileData?.fileUri;
|
||||
}
|
||||
}
|
||||
return imagesFrom(sources(), "Gemini 接口没有返回图片");
|
||||
}
|
||||
|
||||
export function responseErrorMessage(value: unknown) {
|
||||
const payload = record(value);
|
||||
return text(payload.msg) || text(record(payload.error).message) || text(record(record(payload.response).error).message);
|
||||
}
|
||||
|
||||
export function validateResponsePayload(payload: ResponseApiPayload) {
|
||||
assertEnvelope(payload);
|
||||
if (payload.error?.message) throw new Error(payload.error.message);
|
||||
}
|
||||
|
||||
export function validateGeminiPayload(payload: GeminiPayload) {
|
||||
const failure = payload.error?.message || (payload.promptFeedback?.blockReason ? `Gemini 拒绝了本次请求:${payload.promptFeedback.blockReason}` : "");
|
||||
if (failure) throw new Error(failure);
|
||||
}
|
||||
|
||||
export function readStatusError(status: number | undefined, fallback: string) {
|
||||
const known: Record<number, string> = {
|
||||
401: "鉴权失败,请检查 API Key、套餐权限或模型权限",
|
||||
403: "鉴权失败,请检查 API Key、套餐权限或模型权限",
|
||||
429: "请求被限流或额度不足,请稍后重试",
|
||||
};
|
||||
return status ? known[status] || `${fallback}:${status}` : fallback;
|
||||
}
|
||||
|
||||
export function readAxiosError(error: unknown, fallback: string) {
|
||||
if (axios.isCancel(error) || (error instanceof DOMException && error.name === "AbortError")) return "请求已取消";
|
||||
if (!axios.isAxiosError(error)) return error instanceof Error ? error.message : fallback;
|
||||
const payload = record(error.response?.data);
|
||||
return text(payload.msg) || text(record(payload.error).message) || readStatusError(error.response?.status, fallback);
|
||||
}
|
||||
|
||||
export async function readFetchError(response: Response, fallback: string) {
|
||||
const body = await response.text();
|
||||
const status = readStatusError(response.status, fallback);
|
||||
if (!body) return status;
|
||||
let payload: unknown;
|
||||
try { payload = JSON.parse(body); } catch { return body.slice(0, 300) || status; }
|
||||
return responseErrorMessage(payload) || status;
|
||||
}
|
||||
|
||||
export async function readJsonPayload<T>(response: Response, fallback: string): Promise<T> {
|
||||
const body = await response.text();
|
||||
try { return JSON.parse(body) as T; } catch {
|
||||
const detail = /^\s*(?:<!doctype|<html)/i.test(body)
|
||||
? "后端代理返回了前端网页,请检查 VITE_CANVAS_BACKEND_URL 和反向代理配置"
|
||||
: `${fallback}:接口没有返回有效 JSON`;
|
||||
throw new Error(detail);
|
||||
}
|
||||
}
|
||||
|
||||
function updateText(state: TextState, field: keyof TextState, value: unknown, sink: TextSink | undefined, final = false) {
|
||||
if (typeof value !== "string" || (final && state[field])) return;
|
||||
state[field] = final ? value : state[field] + value;
|
||||
sink?.(state[field]);
|
||||
}
|
||||
|
||||
const responseChannels: Record<string, keyof TextState> = {
|
||||
"response.output_text": "text",
|
||||
"response.reasoning": "reasoning",
|
||||
"response.reasoning_text": "reasoning",
|
||||
"response.reasoning_summary_text": "reasoning",
|
||||
};
|
||||
|
||||
export function consumeResponseStreamText(state: ResponseStreamState, chunk: string, onDelta?: TextSink, onReasoning?: TextSink, flush = false) {
|
||||
consumeJsonEvents(state, chunk, (payload) => {
|
||||
const event = record(payload);
|
||||
const failure = responseErrorMessage(event);
|
||||
if (failure) state.error = failure;
|
||||
const type = text(event.type);
|
||||
const separator = type.lastIndexOf(".");
|
||||
const field = Object.hasOwn(responseChannels, type.slice(0, separator)) ? responseChannels[type.slice(0, separator)] : undefined;
|
||||
const action = type.slice(separator + 1);
|
||||
if (field && (action === "delta" || action === "done")) {
|
||||
updateText(state, field, action === "done" ? event.text : event.delta, field === "text" ? onDelta : onReasoning, action === "done");
|
||||
}
|
||||
if (type === "response.completed" && event.response !== null && typeof event.response === "object" && !Array.isArray(event.response)) {
|
||||
state.payload = event.response as ResponseApiPayload;
|
||||
} else if (Array.isArray(event.output)) state.payload = event as ResponseApiPayload;
|
||||
}, flush);
|
||||
}
|
||||
|
||||
export function consumeChatCompletionStreamText(state: ChatCompletionStreamState, chunk: string, onDelta?: TextSink, onReasoning?: TextSink, flush = false) {
|
||||
consumeJsonEvents(state, chunk, (payload) => {
|
||||
const event = record(payload);
|
||||
const failure = responseErrorMessage(event);
|
||||
if (failure) state.error = failure;
|
||||
const first = Array.isArray(event.choices) ? record(event.choices[0]) : {};
|
||||
const delta = record(first.delta);
|
||||
updateText(state, "text", delta.content, onDelta);
|
||||
const reasoning = text(delta.reasoning_content) || text(delta.reasoning) || text(delta.reasoning_text);
|
||||
if (reasoning) updateText(state, "reasoning", reasoning, onReasoning);
|
||||
if (!Array.isArray(delta.tool_calls)) return;
|
||||
for (const [fallback, raw] of delta.tool_calls.entries()) {
|
||||
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
|
||||
const call = record(raw);
|
||||
const index = typeof call.index === "number" ? call.index : fallback;
|
||||
const previous = state.toolCalls.get(index);
|
||||
const fn = record(call.function);
|
||||
state.toolCalls.set(index, {
|
||||
id: text(call.id) || previous?.id || "",
|
||||
name: text(fn.name) || previous?.name || "",
|
||||
arguments: (previous?.arguments || "") + text(fn.arguments),
|
||||
});
|
||||
}
|
||||
}, flush);
|
||||
}
|
||||
|
||||
export function consumeGeminiStreamText(state: GeminiStreamState, chunk: string, onDelta?: TextSink, onReasoning?: TextSink, flush = false) {
|
||||
consumeJsonEvents(state, chunk, (payload) => {
|
||||
const result = parseGeminiToolResponse(payload as GeminiPayload);
|
||||
if (result.reasoning) updateText(state, "reasoning", result.reasoning, onReasoning);
|
||||
if (result.content) updateText(state, "text", result.content, onDelta);
|
||||
state.toolCalls.push(...result.toolCalls);
|
||||
}, flush);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user