


















@@ -19,7 +19,35 @@ if (!Number.isFinite(maxBodyBytes) || maxBodyBytes <= 0) {
1919throw new Error(`invalid OPENCLAW_OPENAI_CHAT_TOOLS_MAX_BODY_BYTES: ${maxBodyBytes}`);
2020}
212122-async function readBoundedResponseText(response, byteLimit) {
22+function cancelReaderSoon(reader) {
23+void Promise.resolve()
24+.then(() => reader.cancel())
25+.catch(() => undefined);
26+}
27+28+async function readResponseChunk(reader, timeoutPromise, markCanceled) {
29+const readPromise = reader.read();
30+if (!timeoutPromise) {
31+return await readPromise;
32+}
33+34+let waitingForRead = true;
35+const timeoutReadPromise = timeoutPromise.catch((error) => {
36+if (waitingForRead) {
37+markCanceled();
38+cancelReaderSoon(reader);
39+}
40+throw error;
41+});
42+43+try {
44+return await Promise.race([readPromise, timeoutReadPromise]);
45+} finally {
46+waitingForRead = false;
47+}
48+}
49+50+async function readBoundedResponseText(response, byteLimit, timeoutPromise) {
2351const contentLength = response.headers?.get?.("content-length");
2452if (contentLength && /^\d+$/u.test(contentLength)) {
2553const parsedContentLength = Number(contentLength);
@@ -35,67 +63,88 @@ async function readBoundedResponseText(response, byteLimit) {
3563}
3664const chunks = [];
3765let totalBytes = 0;
38-for (;;) {
39-const { done, value } = await reader.read();
40-if (done) {
41-break;
66+let canceled = false;
67+try {
68+for (;;) {
69+const { done, value } = await readResponseChunk(reader, timeoutPromise, () => {
70+canceled = true;
71+});
72+if (done) {
73+break;
74+}
75+totalBytes += value.byteLength;
76+if (totalBytes > byteLimit) {
77+canceled = true;
78+await reader.cancel();
79+throw new Error(`chat completions response body exceeded ${byteLimit} bytes`);
80+}
81+chunks.push(Buffer.from(value));
4282}
43-totalBytes += value.byteLength;
44-if (totalBytes > byteLimit) {
45-await reader.cancel();
46-throw new Error(`chat completions response body exceeded ${byteLimit} bytes`);
83+} finally {
84+if (!canceled) {
85+reader.releaseLock();
4786}
48-chunks.push(Buffer.from(value));
4987}
5088return Buffer.concat(chunks, totalBytes).toString("utf8");
5189}
52905391const controller = new AbortController();
54-const timeout = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
92+const timeoutError = new Error(`chat completions request timed out after ${timeoutSeconds}s`);
93+let timeout;
94+const timeoutPromise = new Promise((_, reject) => {
95+timeout = setTimeout(() => {
96+controller.abort(timeoutError);
97+reject(timeoutError);
98+}, timeoutSeconds * 1000);
99+timeout.unref?.();
100+});
55101const started = Date.now();
56102let response;
57103let text;
58104try {
59-response = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
60-method: "POST",
61-headers: {
62-authorization: `Bearer ${token}`,
63-"content-type": "application/json",
64-"x-openclaw-model": backendModel,
65-},
66-body: JSON.stringify({
67-model: "openclaw",
68-stream: false,
69-messages: [
70-{
71-role: "user",
72-content:
73-"Use the get_weather tool exactly once for Paris, France. Return the tool call only.",
74-},
75-],
76-tool_choice: "auto",
77-tools: [
78-{
79-type: "function",
80-function: {
81-name: "get_weather",
82-description: "Return weather for a city.",
83-strict: true,
84-parameters: {
85-type: "object",
86-additionalProperties: false,
87-properties: {
88-city: { type: "string", description: "City and country." },
105+response = await Promise.race([
106+fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
107+method: "POST",
108+headers: {
109+authorization: `Bearer ${token}`,
110+"content-type": "application/json",
111+"x-openclaw-model": backendModel,
112+},
113+body: JSON.stringify({
114+model: "openclaw",
115+stream: false,
116+messages: [
117+{
118+role: "user",
119+content:
120+"Use the get_weather tool exactly once for Paris, France. Return the tool call only.",
121+},
122+],
123+tool_choice: "auto",
124+tools: [
125+{
126+type: "function",
127+function: {
128+name: "get_weather",
129+description: "Return weather for a city.",
130+strict: true,
131+parameters: {
132+type: "object",
133+additionalProperties: false,
134+properties: {
135+city: { type: "string", description: "City and country." },
136+},
137+required: ["city"],
89138},
90-required: ["city"],
91139},
92140},
93-},
94-],
141+],
142+}),
143+signal: controller.signal,
95144}),
96-signal: controller.signal,
97-});
98-text = await readBoundedResponseText(response, maxBodyBytes);
145+timeoutPromise,
146+]);
147+text = await readBoundedResponseText(response, maxBodyBytes, timeoutPromise);
99148} finally {
100149clearTimeout(timeout);
101150}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。