@@ -6,6 +6,10 @@
|
|||||||
|
|
||||||
- Added an optional `reasoning` field to `Usage` reporting reasoning/thinking token counts as a subset of `output`. Populated for Anthropic (`output_tokens_details.thinking_tokens`), OpenAI Responses/Codex/Azure (`output_tokens_details.reasoning_tokens`), OpenAI Completions (`completion_tokens_details.reasoning_tokens`), and Google Generative AI / Vertex (`thoughtsTokenCount`). Bedrock Converse and Mistral are not populated because those APIs do not return a reasoning token breakdown ([#6057](https://github.com/earendil-works/pi/issues/6057)).
|
- Added an optional `reasoning` field to `Usage` reporting reasoning/thinking token counts as a subset of `output`. Populated for Anthropic (`output_tokens_details.thinking_tokens`), OpenAI Responses/Codex/Azure (`output_tokens_details.reasoning_tokens`), OpenAI Completions (`completion_tokens_details.reasoning_tokens`), and Google Generative AI / Vertex (`thoughtsTokenCount`). Bedrock Converse and Mistral are not populated because those APIs do not return a reasoning token breakdown ([#6057](https://github.com/earendil-works/pi/issues/6057)).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Changed OpenAI Codex Responses SSE response-header waits to use the configured HTTP timeout instead of the previous fixed 20 second timeout, reducing false timeouts on slow connections ([#4945](https://github.com/earendil-works/pi/issues/4945)).
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed `streamSimple()` to send a context-aware max-token cap so providers that count input and output against one context window do not reject long requests ([#5595](https://github.com/earendil-works/pi/issues/5595)).
|
- Fixed `streamSimple()` to send a context-aware max-token cap so providers that count input and output against one context window do not reject long requests ([#5595](https://github.com/earendil-works/pi/issues/5595)).
|
||||||
|
|||||||
@@ -56,9 +56,6 @@ const JWT_CLAIM_PATH = "https://api.openai.com/auth" as const;
|
|||||||
const DEFAULT_MAX_RETRIES = 0;
|
const DEFAULT_MAX_RETRIES = 0;
|
||||||
const BASE_DELAY_MS = 1000;
|
const BASE_DELAY_MS = 1000;
|
||||||
const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;
|
const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;
|
||||||
// Keep a bounded pre-header timeout so zero-event Codex SSE stalls fail instead of
|
|
||||||
// leaving callers stuck on "Working..." indefinitely. See #4945.
|
|
||||||
const DEFAULT_SSE_HEADER_TIMEOUT_MS = 20_000;
|
|
||||||
const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS = 15_000;
|
const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS = 15_000;
|
||||||
const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
|
const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
|
||||||
const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
|
const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
|
||||||
@@ -179,20 +176,6 @@ function normalizeTimeoutMs(value: number | undefined): number | undefined {
|
|||||||
return Math.floor(value);
|
return Math.floor(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSSEHeaderTimeout(): { signal: AbortSignal; clear: () => void; error: () => Error | undefined } {
|
|
||||||
const controller = new AbortController();
|
|
||||||
let error: Error | undefined;
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
error = new Error(`Codex SSE response headers timed out after ${DEFAULT_SSE_HEADER_TIMEOUT_MS}ms`);
|
|
||||||
controller.abort(error);
|
|
||||||
}, DEFAULT_SSE_HEADER_TIMEOUT_MS);
|
|
||||||
return {
|
|
||||||
signal: controller.signal,
|
|
||||||
clear: () => clearTimeout(timeout),
|
|
||||||
error: () => error,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Main Stream Function
|
// Main Stream Function
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -245,7 +228,7 @@ export const stream: StreamFunction<"openai-codex-responses", OpenAICodexRespons
|
|||||||
websocketRequestId,
|
websocketRequestId,
|
||||||
);
|
);
|
||||||
const bodyJson = JSON.stringify(body);
|
const bodyJson = JSON.stringify(body);
|
||||||
const idleTimeoutMs = normalizeTimeoutMs(options?.timeoutMs);
|
const httpTimeoutMs = normalizeTimeoutMs(options?.timeoutMs);
|
||||||
const websocketConnectTimeoutMs = normalizeTimeoutMs(options?.websocketConnectTimeoutMs);
|
const websocketConnectTimeoutMs = normalizeTimeoutMs(options?.websocketConnectTimeoutMs);
|
||||||
const transport = options?.transport || "auto";
|
const transport = options?.transport || "auto";
|
||||||
const websocketDisabledForSession = transport !== "sse" && isWebSocketSseFallbackActive(options?.sessionId);
|
const websocketDisabledForSession = transport !== "sse" && isWebSocketSseFallbackActive(options?.sessionId);
|
||||||
@@ -269,7 +252,7 @@ export const stream: StreamFunction<"openai-codex-responses", OpenAICodexRespons
|
|||||||
() => {
|
() => {
|
||||||
websocketStarted = true;
|
websocketStarted = true;
|
||||||
},
|
},
|
||||||
idleTimeoutMs,
|
httpTimeoutMs,
|
||||||
websocketConnectTimeoutMs,
|
websocketConnectTimeoutMs,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
@@ -325,8 +308,9 @@ export const stream: StreamFunction<"openai-codex-responses", OpenAICodexRespons
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const headerTimeout = createSSEHeaderTimeout();
|
const headerTimeoutSignal =
|
||||||
const combinedSignal = combineAbortSignals([options?.signal, headerTimeout.signal]);
|
httpTimeoutMs !== undefined && httpTimeoutMs > 0 ? AbortSignal.timeout(httpTimeoutMs) : undefined;
|
||||||
|
const combinedSignal = combineAbortSignals([options?.signal, headerTimeoutSignal]);
|
||||||
try {
|
try {
|
||||||
response = await fetch(resolveCodexUrl(model.baseUrl), {
|
response = await fetch(resolveCodexUrl(model.baseUrl), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -335,11 +319,12 @@ export const stream: StreamFunction<"openai-codex-responses", OpenAICodexRespons
|
|||||||
signal: combinedSignal.signal,
|
signal: combinedSignal.signal,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const timeoutError = headerTimeout.error();
|
if (headerTimeoutSignal?.aborted && !options?.signal?.aborted) {
|
||||||
throw timeoutError && !options?.signal?.aborted ? timeoutError : error;
|
throw new Error(`Codex SSE response headers timed out after ${httpTimeoutMs}ms`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
combinedSignal.cleanup();
|
combinedSignal.cleanup();
|
||||||
headerTimeout.clear();
|
|
||||||
}
|
}
|
||||||
await options?.onResponse?.(
|
await options?.onResponse?.(
|
||||||
{ status: response.status, headers: headersToRecord(response.headers) },
|
{ status: response.status, headers: headersToRecord(response.headers) },
|
||||||
|
|||||||
@@ -311,8 +311,7 @@ describe("openai-codex streaming", () => {
|
|||||||
expect(result.stopReason).toBe("length");
|
expect(result.stopReason).toBe("length");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("aborts SSE fetch when response headers do not arrive", async () => {
|
it("aborts SSE fetch after the configured HTTP timeout when response headers do not arrive", async () => {
|
||||||
vi.useFakeTimers();
|
|
||||||
const token = mockToken();
|
const token = mockToken();
|
||||||
|
|
||||||
const fetchMock = vi.fn((input: string | URL, init?: RequestInit) => {
|
const fetchMock = vi.fn((input: string | URL, init?: RequestInit) => {
|
||||||
@@ -357,25 +356,15 @@ describe("openai-codex streaming", () => {
|
|||||||
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
||||||
};
|
};
|
||||||
|
|
||||||
const resultPromise = streamOpenAICodexResponses(model, context, {
|
const result = await streamOpenAICodexResponses(model, context, {
|
||||||
apiKey: token,
|
apiKey: token,
|
||||||
transport: "sse",
|
transport: "sse",
|
||||||
|
timeoutMs: 10,
|
||||||
}).result();
|
}).result();
|
||||||
let settled = false;
|
|
||||||
const observedResultPromise = resultPromise.then((result) => {
|
|
||||||
settled = true;
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
await vi.advanceTimersByTimeAsync(0);
|
|
||||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(10_000);
|
|
||||||
expect(settled).toBe(false);
|
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(10_000);
|
|
||||||
const result = await observedResultPromise;
|
|
||||||
expect(result.stopReason).toBe("error");
|
expect(result.stopReason).toBe("error");
|
||||||
expect(result.errorMessage).toBe("Codex SSE response headers timed out after 20000ms");
|
expect(result.errorMessage).toBe("Codex SSE response headers timed out after 10ms");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("aborts SSE body reads after response headers arrive", async () => {
|
it("aborts SSE body reads after response headers arrive", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user