diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/providers/azure-openai-responses.ts index f836fcfb8..7d1bfef85 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -20,7 +20,13 @@ import { } from "../types"; import { AssistantMessageEventStream } from "../utils/event-stream"; import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector"; -import { createFirstEventWatchdog, getOpenAIStreamIdleTimeoutMs, getStreamFirstEventTimeoutMs, iterateWithIdleTimeout, markFirstStreamEvent } from "../utils/idle-iterator"; +import { + createFirstEventWatchdog, + getOpenAIStreamIdleTimeoutMs, + getStreamFirstEventTimeoutMs, + iterateWithIdleTimeout, + markFirstStreamEvent, +} from "../utils/idle-iterator"; import { mapToOpenAIResponsesToolChoice } from "../utils/tool-choice"; import { supportsDeveloperRole } from "./openai-responses"; import { @@ -121,9 +127,8 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses" ? AbortSignal.any([options.signal, requestAbortController.signal]) : requestAbortController.signal; const idleTimeoutMs = getOpenAIStreamIdleTimeoutMs(); - const firstEventWatchdog = createFirstEventWatchdog( - getStreamFirstEventTimeoutMs(idleTimeoutMs), - () => requestAbortController.abort(), + const firstEventWatchdog = createFirstEventWatchdog(getStreamFirstEventTimeoutMs(idleTimeoutMs), () => + requestAbortController.abort(), ); options?.onPayload?.(params); rawRequestDump = { @@ -138,14 +143,11 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses" stream.push({ type: "start", partial: output }); await processResponsesStream( - iterateWithIdleTimeout( - markFirstStreamEvent(openaiStream, firstEventWatchdog), - { - idleTimeoutMs, - errorMessage: "Azure OpenAI responses stream stalled while waiting for the next event", - onIdle: () => requestAbortController.abort(), - }, - ), + iterateWithIdleTimeout(markFirstStreamEvent(openaiStream, firstEventWatchdog), { + idleTimeoutMs, + errorMessage: "Azure OpenAI responses stream stalled while waiting for the next event", + onIdle: () => requestAbortController.abort(), + }), output, stream, model, diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 480e2a951..a3bc8a1a5 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -30,7 +30,13 @@ import { } from "../utils"; import { AssistantMessageEventStream } from "../utils/event-stream"; import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector"; -import { createFirstEventWatchdog, getOpenAIStreamIdleTimeoutMs, getStreamFirstEventTimeoutMs, iterateWithIdleTimeout, markFirstStreamEvent } from "../utils/idle-iterator"; +import { + createFirstEventWatchdog, + getOpenAIStreamIdleTimeoutMs, + getStreamFirstEventTimeoutMs, + iterateWithIdleTimeout, + markFirstStreamEvent, +} from "../utils/idle-iterator"; import { adaptSchemaForStrict, NO_STRICT } from "../utils/schema"; import { mapToOpenAIResponsesToolChoice } from "../utils/tool-choice"; import { @@ -173,9 +179,8 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = ( ? AbortSignal.any([options.signal, requestAbortController.signal]) : requestAbortController.signal; const idleTimeoutMs = getOpenAIStreamIdleTimeoutMs(); - const firstEventWatchdog = createFirstEventWatchdog( - getStreamFirstEventTimeoutMs(idleTimeoutMs), - () => requestAbortController.abort(), + const firstEventWatchdog = createFirstEventWatchdog(getStreamFirstEventTimeoutMs(idleTimeoutMs), () => + requestAbortController.abort(), ); options?.onPayload?.(params); rawRequestDump = { @@ -192,14 +197,11 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = ( const nativeOutputItems: Array> = []; await processResponsesStream( - iterateWithIdleTimeout( - markFirstStreamEvent(openaiStream, firstEventWatchdog), - { - idleTimeoutMs, - errorMessage: "OpenAI responses stream stalled while waiting for the next event", - onIdle: () => requestAbortController.abort(), - }, - ), + iterateWithIdleTimeout(markFirstStreamEvent(openaiStream, firstEventWatchdog), { + idleTimeoutMs, + errorMessage: "OpenAI responses stream stalled while waiting for the next event", + onIdle: () => requestAbortController.abort(), + }), output, stream, model, diff --git a/packages/ai/src/utils/idle-iterator.ts b/packages/ai/src/utils/idle-iterator.ts index c86bc8394..cd52b4abb 100644 --- a/packages/ai/src/utils/idle-iterator.ts +++ b/packages/ai/src/utils/idle-iterator.ts @@ -26,7 +26,10 @@ export function getOpenAIStreamIdleTimeoutMs(): number | undefined { * Set `PI_STREAM_FIRST_EVENT_TIMEOUT_MS=0` to disable the watchdog. */ export function getStreamFirstEventTimeoutMs(idleTimeoutMs?: number): number | undefined { - const fallback = Math.min(DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS, idleTimeoutMs ?? DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS); + const fallback = Math.min( + DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS, + idleTimeoutMs ?? DEFAULT_STREAM_FIRST_EVENT_TIMEOUT_MS, + ); return normalizeIdleTimeoutMs($env.PI_STREAM_FIRST_EVENT_TIMEOUT_MS, fallback); } @@ -39,10 +42,7 @@ export interface FirstEventWatchdog { * Starts a watchdog that aborts a request if no first stream event arrives in time. * Call `markFirstEventReceived()` as soon as the first event is observed. */ -export function createFirstEventWatchdog( - timeoutMs: number | undefined, - onTimeout: () => void, -): FirstEventWatchdog { +export function createFirstEventWatchdog(timeoutMs: number | undefined, onTimeout: () => void): FirstEventWatchdog { let timer: NodeJS.Timeout | undefined; if (timeoutMs !== undefined && timeoutMs > 0) { timer = setTimeout(() => { @@ -105,7 +105,10 @@ export async function* iterateWithIdleTimeout( options: IdleTimeoutIteratorOptions, ): AsyncGenerator { const firstItemTimeoutMs = options.firstItemTimeoutMs ?? options.idleTimeoutMs; - if ((firstItemTimeoutMs === undefined || firstItemTimeoutMs <= 0) && (options.idleTimeoutMs === undefined || options.idleTimeoutMs <= 0)) { + if ( + (firstItemTimeoutMs === undefined || firstItemTimeoutMs <= 0) && + (options.idleTimeoutMs === undefined || options.idleTimeoutMs <= 0) + ) { for await (const item of iterable) { yield item; } @@ -152,7 +155,9 @@ export async function* iterateWithIdleTimeout( if (returnPromise) { void returnPromise.catch(() => {}); } - throw new Error(sawFirstItem ? options.errorMessage : (options.firstItemErrorMessage ?? options.errorMessage)); + throw new Error( + sawFirstItem ? options.errorMessage : (options.firstItemErrorMessage ?? options.errorMessage), + ); } if (outcome.kind === "error") { throw outcome.error; diff --git a/packages/ai/src/utils/oauth/vercel-ai-gateway.ts b/packages/ai/src/utils/oauth/vercel-ai-gateway.ts index 0ecfe6234..3163c4869 100644 --- a/packages/ai/src/utils/oauth/vercel-ai-gateway.ts +++ b/packages/ai/src/utils/oauth/vercel-ai-gateway.ts @@ -11,8 +11,7 @@ import type { OAuthController } from "./types"; -const AUTH_URL = - "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway%2Fapi-keys&title=AI+Gateway+API+Keys"; +const AUTH_URL = "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai-gateway%2Fapi-keys&title=AI+Gateway+API+Keys"; /** * Login to Vercel AI Gateway. @@ -20,32 +19,29 @@ const AUTH_URL = * Opens browser to Vercel AI Gateway docs and prompts for an API key. * Returns the API key directly (not OAuthCredentials - this isn't OAuth). */ -export async function loginVercelAiGateway( - options: OAuthController, -): Promise { - if (!options.onPrompt) { - throw new Error("Vercel AI Gateway login requires onPrompt callback"); - } +export async function loginVercelAiGateway(options: OAuthController): Promise { + if (!options.onPrompt) { + throw new Error("Vercel AI Gateway login requires onPrompt callback"); + } - options.onAuth?.({ - url: AUTH_URL, - instructions: - "Copy your Vercel AI Gateway API key from the Vercel dashboard", - }); + options.onAuth?.({ + url: AUTH_URL, + instructions: "Copy your Vercel AI Gateway API key from the Vercel dashboard", + }); - const apiKey = await options.onPrompt({ - message: "Paste your Vercel AI Gateway API key", - placeholder: "vck_...", - }); + const apiKey = await options.onPrompt({ + message: "Paste your Vercel AI Gateway API key", + placeholder: "vck_...", + }); - if (options.signal?.aborted) { - throw new Error("Login cancelled"); - } + if (options.signal?.aborted) { + throw new Error("Login cancelled"); + } - const trimmed = apiKey.trim(); - if (!trimmed) { - throw new Error("API key is required"); - } + const trimmed = apiKey.trim(); + if (!trimmed) { + throw new Error("API key is required"); + } - return trimmed; + return trimmed; } diff --git a/packages/ai/test/anthropic-retry.test.ts b/packages/ai/test/anthropic-retry.test.ts index 7a7482b8a..3c3a8e129 100644 --- a/packages/ai/test/anthropic-retry.test.ts +++ b/packages/ai/test/anthropic-retry.test.ts @@ -19,7 +19,9 @@ describe("isProviderRetryableError", () => { }); it("retries first-event timeout errors", () => { - expect(isProviderRetryableError(new Error("Anthropic stream timed out while waiting for the first event"))).toBe(true); + expect(isProviderRetryableError(new Error("Anthropic stream timed out while waiting for the first event"))).toBe( + true, + ); }); it("does not retry non-transient validation errors", () => { diff --git a/packages/ai/test/anthropic-stream-timeout.test.ts b/packages/ai/test/anthropic-stream-timeout.test.ts index 177820e70..c54012c92 100644 --- a/packages/ai/test/anthropic-stream-timeout.test.ts +++ b/packages/ai/test/anthropic-stream-timeout.test.ts @@ -100,9 +100,9 @@ describe("anthropic first-event timeout retries", () => { vi.spyOn(Messages.prototype, "stream").mockImplementation((_body, requestOptions) => { attempt += 1; const signal = (requestOptions as { signal?: AbortSignal } | undefined)?.signal; - return (attempt === 1 - ? createHangingAnthropicStream(signal) - : createSuccessfulAnthropicStream("retry recovered")) as never; + return ( + attempt === 1 ? createHangingAnthropicStream(signal) : createSuccessfulAnthropicStream("retry recovered") + ) as never; }); const result = await streamAnthropic(model, context, { apiKey: "sk-ant-test" }).result(); diff --git a/packages/coding-agent/src/config/model-registry.ts b/packages/coding-agent/src/config/model-registry.ts index d53ed3244..9099c51e2 100644 --- a/packages/coding-agent/src/config/model-registry.ts +++ b/packages/coding-agent/src/config/model-registry.ts @@ -31,7 +31,7 @@ import { type ConfigError, ConfigFile } from "../config"; import { parseModelString } from "../config/model-resolver"; import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme"; import type { AuthStorage, OAuthCredential } from "../session/auth-storage"; -import { settings, type Settings } from "./settings"; +import { type Settings, settings } from "./settings"; export const kNoAuth = "N/A"; diff --git a/packages/coding-agent/src/patch/shared.ts b/packages/coding-agent/src/patch/shared.ts index 0178629e8..1030c8117 100644 --- a/packages/coding-agent/src/patch/shared.ts +++ b/packages/coding-agent/src/patch/shared.ts @@ -21,7 +21,7 @@ import { shortenPath, truncateDiffByHunk, } from "../tools/render-utils"; -import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui"; +import { Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui"; import type { HashlineToolEdit } from "./index"; import type { DiffError, DiffResult, Operation } from "./types"; @@ -242,7 +242,9 @@ function wrapEditRendererLine(line: string, width: number): string[] { const continuationPrefix = `${" ".repeat(Math.max(0, prefixWidth - 1))}|`; const wrappedContent = wrapTextWithAnsi(content, contentWidth); - return wrappedContent.map((segment, index) => `${startAnsi}${index === 0 ? prefix : continuationPrefix}${segment}\x1b[39m`); + return wrappedContent.map( + (segment, index) => `${startAnsi}${index === 0 ? prefix : continuationPrefix}${segment}\x1b[39m`, + ); } export const editToolRenderer = { @@ -379,7 +381,8 @@ export const editToolRenderer = { ); } - const lines = width > 0 ? text.split("\n").flatMap(line => wrapEditRendererLine(line, width)) : text.split("\n"); + const lines = + width > 0 ? text.split("\n").flatMap(line => wrapEditRendererLine(line, width)) : text.split("\n"); cached = { key, lines }; return lines; }, diff --git a/packages/coding-agent/src/utils/title-generator.ts b/packages/coding-agent/src/utils/title-generator.ts index 4b986640e..43743ee64 100644 --- a/packages/coding-agent/src/utils/title-generator.ts +++ b/packages/coding-agent/src/utils/title-generator.ts @@ -28,7 +28,6 @@ function getTitleModel( const availableModels = registry.getAvailable(); if (availableModels.length === 0) return undefined; - const matchPreferences = { usageOrder: settings.getStorage()?.getModelUsageOrder() }; const titleModel = resolveRoleSelection(["commit", "smol"], settings, availableModels); if (titleModel) { return { model: titleModel.model, thinkingLevel: titleModel.thinkingLevel };