diff --git a/.vscode/settings.json b/.vscode/settings.json index 9d429ef11f..bf5c483fc3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,5 +13,20 @@ // Surface invisible (zero-width) and ambiguous/homoglyph Unicode in the // editor. Pairs with the code-qa.yml invisible-chars CI check. "editor.unicodeHighlight.invisibleCharacters": true, - "editor.unicodeHighlight.ambiguousCharacters": true + "editor.unicodeHighlight.ambiguousCharacters": true, + "workbench.colorCustomizations": { + "editor.selectionBackground": "#135564ef", + "editor.selectionHighlightBackground": "#13556496", + "activityBar.background": "#A02C18", + "titleBar.activeBackground": "#DE3F24", + "titleBar.activeForeground": "#FEF9F9", + "titleBar.inactiveBackground": "#A02C18", + "titleBar.inactiveForeground": "#FEF9F9", + "statusBar.background": "#A02C18", + "statusBar.foreground": "#FEF9F9", + "statusBar.debuggingBackground": "#A02C18", + "statusBar.debuggingForeground": "#FEF9F9", + "statusBar.noFolderBackground": "#A02C18", + "statusBar.noFolderForeground": "#FEF9F9" + } } diff --git a/src/api/providers/__tests__/kimi-code.spec.ts b/src/api/providers/__tests__/kimi-code.spec.ts index fe229910b8..edaa5d5ba7 100644 --- a/src/api/providers/__tests__/kimi-code.spec.ts +++ b/src/api/providers/__tests__/kimi-code.spec.ts @@ -10,6 +10,16 @@ const { mockGetAccessToken, mockForceRefreshAccessToken, mockGetModels } = vi.ho mockGetModels: vi.fn(), })) +vi.mock("undici", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + fetch: vi.fn().mockImplementation(async (url: RequestInfo | URL, init?: RequestInit) => { + return globalThis.fetch(url, init) + }), + } +}) + vi.mock("../../../integrations/kimi-code/oauth", () => ({ kimiCodeOAuthManager: { getAccessToken: mockGetAccessToken, diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 04b12f233d..8195242f47 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -1,6 +1,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI, { AzureOpenAI } from "openai" import axios from "axios" +import { Agent, fetch as undiciFetch, Dispatcher } from "undici" import { type ModelInfo, @@ -52,34 +53,79 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl ...(this.options.openAiHeaders || {}), } + function resolveTimeoutMs(configuredMs: number | undefined): number { + if (configuredMs === undefined || configuredMs === 0) { + return 0 + // return 60 * 60 * 1000; + } + return configuredMs + } + + const timeoutMs = resolveTimeoutMs(this.timeoutMs) + + // VS Code bundles its own undici with a 5-minute `bodyTimeout` default. + // For streaming LLM requests, that default terminates the connection. + // We bypass the VS Code-bundled undici by injecting our own Agent-backed + // fetch into the OpenAI SDK. + const agent = new Agent({ + headersTimeout: timeoutMs, + bodyTimeout: timeoutMs, + keepAliveTimeout: timeoutMs, + keepAliveMaxTimeout: timeoutMs, + connect: { + timeout: Math.min(timeoutMs, 60_000), + }, + }) + + interface UndiciRequestInit extends RequestInit { + dispatcher?: Dispatcher + } + + const customFetch: typeof fetch = (url, init) => { + const undiciInit = { ...init, dispatcher: agent } as UndiciRequestInit + return globalThis.fetch(url, undiciInit as RequestInit) + // const fetchImpl = undiciFetch as unknown as ( + // url: RequestInfo | URL, + // init: UndiciRequestInit, + // ) => Promise + + // return fetchImpl(url, undiciInit) + } + + const timeoutConfig = { + timeout: timeoutMs, + } + if (isAzureAiInference) { - // Azure AI Inference Service (e.g., for DeepSeek) uses a different path structure this.client = new OpenAI({ baseURL, apiKey, defaultHeaders: headers, defaultQuery: { "api-version": this.options.azureApiVersion || "2024-05-01-preview" }, - timeout: this.timeoutMs, + ...timeoutConfig, }) } else if (isAzureOpenAi) { // Azure API shape slightly differs from the core API shape: // https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai + const azureBaseURL = `${baseURL.replace(/\/openai\/?$/i, "").replace(/\/$/, "")}/openai` this.client = new AzureOpenAI({ baseURL: azureBaseURL, apiKey, apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion, defaultHeaders: headers, - timeout: this.timeoutMs, + ...timeoutConfig, }) } else { this.client = new OpenAI({ baseURL, apiKey, defaultHeaders: headers, - timeout: this.timeoutMs, + ...timeoutConfig, }) } + + ;(this.client as unknown as { fetch: typeof fetch }).fetch = customFetch } override async *createMessage(