



























@@ -25,6 +25,7 @@ import {
2525resolveTimerTimeoutMs,
2626clampTimerTimeoutMs,
2727} from "@openclaw/normalization-core/number-coercion";
28+import { createSseByteGuard } from "../../agents/streaming-byte-guard.js";
2829import { stripSystemPromptCacheBoundary } from "../../agents/system-prompt-cache-boundary.js";
2930import { getEnvApiKey } from "../env-api-keys.js";
3031import { clampThinkingLevel } from "../model-utils.js";
@@ -66,6 +67,8 @@ const RETRY_AFTER_HTTP_DATE_RE =
6667/^(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), \d{2}-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{2} \d{2}:\d{2}:\d{2} GMT|(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [ \d]\d \d{2}:\d{2}:\d{2} \d{4})$/;
6768const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "opencode"]);
6869const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
70+const OPENAI_CHATGPT_RESPONSES_ERROR_BODY_MAX_BYTES = 16 * 1024;
71+const OPENAI_CHATGPT_RESPONSES_SUCCESS_BODY_MAX_BYTES = 16 * 1024 * 1024;
69727073const CODEX_RESPONSE_STATUSES = new Set<CodexResponseStatus>([
7174"completed",
@@ -339,7 +342,7 @@ export const streamOpenAICodexResponses: StreamFunction<
339342break;
340343}
341344342-const errorText = await response.text();
345+const errorText = await readChatGptResponsesErrorTextLimited(response);
343346if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
344347let delayMs = BASE_DELAY_MS * 2 ** attempt;
345348@@ -722,12 +725,23 @@ async function* parseSSE(response: Response): AsyncGenerator<Record<string, unkn
722725}
723726724727const reader = response.body.getReader();
728+// Cap the streaming 200 success-body read at 16 MiB, mirroring the
729+// non-streaming `readProviderJsonResponse` cap so a hostile or
730+// malfunctioning ChatGPT Responses endpoint cannot exhaust memory by
731+// streaming an unbounded SSE body.
732+const guard = createSseByteGuard(reader, {
733+maxBytes: OPENAI_CHATGPT_RESPONSES_SUCCESS_BODY_MAX_BYTES,
734+onOverflow: ({ size, maxBytes }) =>
735+new Error(
736+`OpenAI ChatGPT Responses success body exceeded ${maxBytes} bytes (received ${size})`,
737+),
738+});
725739const decoder = new TextDecoder();
726740let buffer = "";
727741728742try {
729743while (true) {
730-const { done, value } = await reader.read();
744+const { done, value } = await guard.read();
731745if (done) {
732746break;
733747}
@@ -760,14 +774,18 @@ async function* parseSSE(response: Response): AsyncGenerator<Record<string, unkn
760774}
761775} finally {
762776try {
763-await reader.cancel();
777+await guard.cancel();
764778} catch {}
765779try {
766780reader.releaseLock();
767781} catch {}
768782}
769783}
770784785+// Test-only re-export of the bounded SSE parser. Mirrors
786+// `parseAnthropicSseBodyForTest` / `iterateSseMessagesForTest` patterns.
787+export const parseSSEForTest = parseSSE;
788+771789// ============================================================================
772790// WebSocket Parsing
773791// ============================================================================
@@ -1521,10 +1539,57 @@ async function processWebSocketStream(
15211539// Error Handling
15221540// ============================================================================
152315411542+async function readChatGptResponsesErrorTextLimited(response: Response): Promise<string> {
1543+const reader = response.body?.getReader();
1544+if (!reader) {
1545+return "";
1546+}
1547+1548+const decoder = new TextDecoder();
1549+let total = 0;
1550+let text = "";
1551+let reachedLimit = false;
1552+1553+try {
1554+while (true) {
1555+const { value, done } = await reader.read();
1556+if (done) {
1557+break;
1558+}
1559+if (!value || value.byteLength === 0) {
1560+continue;
1561+}
1562+const remaining = OPENAI_CHATGPT_RESPONSES_ERROR_BODY_MAX_BYTES - total;
1563+if (remaining <= 0) {
1564+reachedLimit = true;
1565+break;
1566+}
1567+const chunk = value.byteLength > remaining ? value.subarray(0, remaining) : value;
1568+total += chunk.byteLength;
1569+text += decoder.decode(chunk, { stream: true });
1570+if (total >= OPENAI_CHATGPT_RESPONSES_ERROR_BODY_MAX_BYTES) {
1571+reachedLimit = true;
1572+break;
1573+}
1574+}
1575+text += decoder.decode();
1576+} finally {
1577+if (reachedLimit) {
1578+// This provider module is browser-safe, so keep error-body capping on Web APIs.
1579+await reader.cancel().catch(() => {});
1580+}
1581+try {
1582+reader.releaseLock();
1583+} catch {}
1584+}
1585+1586+return text;
1587+}
1588+15241589async function parseErrorResponse(
15251590response: Response,
15261591): Promise<{ message: string; friendlyMessage?: string }> {
1527-const raw = await response.text();
1592+const raw = await readChatGptResponsesErrorTextLimited(response);
15281593let message = raw || response.statusText || "Request failed";
15291594let friendlyMessage: string | undefined;
15301595此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。