fix(ai,coding-agent): expose provider timeout/retry controls closes #3627
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
- Fixed DeepSeek V4 session replay 400 errors by adding `thinkingFormat: "deepseek"` (sends `thinking: { type }` + `reasoning_effort`), a `reasoningEffortMap`, and `requiresReasoningContentOnAssistantMessages` compat that injects empty `reasoning_content` on all replayed assistant messages when reasoning is enabled ([#3636](https://github.com/badlogic/pi-mono/issues/3636))
|
- Fixed DeepSeek V4 session replay 400 errors by adding `thinkingFormat: "deepseek"` (sends `thinking: { type }` + `reasoning_effort`), a `reasoningEffortMap`, and `requiresReasoningContentOnAssistantMessages` compat that injects empty `reasoning_content` on all replayed assistant messages when reasoning is enabled ([#3636](https://github.com/badlogic/pi-mono/issues/3636))
|
||||||
- Fixed GPT-5.5 generated context window metadata to use the observed 272k limit.
|
- Fixed GPT-5.5 generated context window metadata to use the observed 272k limit.
|
||||||
|
- Fixed provider request controls to expose `timeoutMs` and `maxRetries` in stream options and forward them through OpenAI/Azure/Anthropic request options, preventing unconfigurable SDK timeout/retry defaults on long-running local inference requests ([#3627](https://github.com/badlogic/pi-mono/issues/3627))
|
||||||
|
|
||||||
## [0.70.0] - 2026-04-23
|
## [0.70.0] - 2026-04-23
|
||||||
|
|
||||||
|
|||||||
@@ -457,7 +457,14 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti
|
|||||||
params = nextParams as MessageCreateParamsStreaming;
|
params = nextParams as MessageCreateParamsStreaming;
|
||||||
}
|
}
|
||||||
const response = await client.messages
|
const response = await client.messages
|
||||||
.create({ ...params, stream: true }, { signal: options?.signal })
|
.create(
|
||||||
|
{ ...params, stream: true },
|
||||||
|
{
|
||||||
|
signal: options?.signal,
|
||||||
|
timeout: options?.timeoutMs,
|
||||||
|
maxRetries: options?.maxRetries,
|
||||||
|
},
|
||||||
|
)
|
||||||
.asResponse();
|
.asResponse();
|
||||||
await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
||||||
stream.push({ type: "start", partial: output });
|
stream.push({ type: "start", partial: output });
|
||||||
|
|||||||
@@ -92,7 +92,11 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
|
|||||||
params = nextParams as ResponseCreateParamsStreaming;
|
params = nextParams as ResponseCreateParamsStreaming;
|
||||||
}
|
}
|
||||||
const { data: openaiStream, response } = await client.responses
|
const { data: openaiStream, response } = await client.responses
|
||||||
.create(params, options?.signal ? { signal: options.signal } : undefined)
|
.create(params, {
|
||||||
|
signal: options?.signal,
|
||||||
|
timeout: options?.timeoutMs,
|
||||||
|
maxRetries: options?.maxRetries,
|
||||||
|
})
|
||||||
.withResponse();
|
.withResponse();
|
||||||
await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
||||||
stream.push({ type: "start", partial: output });
|
stream.push({ type: "start", partial: output });
|
||||||
|
|||||||
@@ -145,7 +145,11 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
|
|||||||
params = nextParams as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming;
|
params = nextParams as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming;
|
||||||
}
|
}
|
||||||
const { data: openaiStream, response } = await client.chat.completions
|
const { data: openaiStream, response } = await client.chat.completions
|
||||||
.create(params, { signal: options?.signal })
|
.create(params, {
|
||||||
|
signal: options?.signal,
|
||||||
|
timeout: options?.timeoutMs,
|
||||||
|
maxRetries: options?.maxRetries,
|
||||||
|
})
|
||||||
.withResponse();
|
.withResponse();
|
||||||
await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
||||||
stream.push({ type: "start", partial: output });
|
stream.push({ type: "start", partial: output });
|
||||||
|
|||||||
@@ -99,7 +99,11 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIRes
|
|||||||
params = nextParams as ResponseCreateParamsStreaming;
|
params = nextParams as ResponseCreateParamsStreaming;
|
||||||
}
|
}
|
||||||
const { data: openaiStream, response } = await client.responses
|
const { data: openaiStream, response } = await client.responses
|
||||||
.create(params, options?.signal ? { signal: options.signal } : undefined)
|
.create(params, {
|
||||||
|
signal: options?.signal,
|
||||||
|
timeout: options?.timeoutMs,
|
||||||
|
maxRetries: options?.maxRetries,
|
||||||
|
})
|
||||||
.withResponse();
|
.withResponse();
|
||||||
await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
||||||
stream.push({ type: "start", partial: output });
|
stream.push({ type: "start", partial: output });
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export function buildBaseOptions(model: Model<Api>, options?: SimpleStreamOption
|
|||||||
headers: options?.headers,
|
headers: options?.headers,
|
||||||
onPayload: options?.onPayload,
|
onPayload: options?.onPayload,
|
||||||
onResponse: options?.onResponse,
|
onResponse: options?.onResponse,
|
||||||
|
timeoutMs: options?.timeoutMs,
|
||||||
|
maxRetries: options?.maxRetries,
|
||||||
maxRetryDelayMs: options?.maxRetryDelayMs,
|
maxRetryDelayMs: options?.maxRetryDelayMs,
|
||||||
metadata: options?.metadata,
|
metadata: options?.metadata,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -101,6 +101,16 @@ export interface StreamOptions {
|
|||||||
* Not supported by all providers (e.g., AWS Bedrock uses SDK auth).
|
* Not supported by all providers (e.g., AWS Bedrock uses SDK auth).
|
||||||
*/
|
*/
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* HTTP request timeout in milliseconds for providers/SDKs that support it.
|
||||||
|
* For example, OpenAI and Anthropic SDK clients default to 10 minutes.
|
||||||
|
*/
|
||||||
|
timeoutMs?: number;
|
||||||
|
/**
|
||||||
|
* Maximum retry attempts for providers/SDKs that support client-side retries.
|
||||||
|
* For example, OpenAI and Anthropic SDK clients default to 2.
|
||||||
|
*/
|
||||||
|
maxRetries?: number;
|
||||||
/**
|
/**
|
||||||
* Maximum delay in milliseconds to wait for a retry when the server requests a long wait.
|
* Maximum delay in milliseconds to wait for a retry when the server requests a long wait.
|
||||||
* If the server's requested delay exceeds this value, the request fails immediately
|
* If the server's requested delay exceeds this value, the request fails immediately
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
- Fixed `/copy` to avoid unbounded OSC 52 writes and clipboard races that could break terminal rendering or panic the native clipboard addon ([#3639](https://github.com/badlogic/pi-mono/issues/3639))
|
- Fixed `/copy` to avoid unbounded OSC 52 writes and clipboard races that could break terminal rendering or panic the native clipboard addon ([#3639](https://github.com/badlogic/pi-mono/issues/3639))
|
||||||
- Fixed extension flag docs to show `pi.getFlag()` using registered flag names without the CLI `--` prefix ([#3614](https://github.com/badlogic/pi-mono/issues/3614))
|
- Fixed extension flag docs to show `pi.getFlag()` using registered flag names without the CLI `--` prefix ([#3614](https://github.com/badlogic/pi-mono/issues/3614))
|
||||||
|
- Fixed provider retry/timeout settings wiring by adding `retry.provider.{timeoutMs,maxRetries,maxRetryDelayMs}`, migrating legacy `retry.maxDelayMs`, and forwarding provider controls into `streamSimple` request options ([#3627](https://github.com/badlogic/pi-mono/issues/3627))
|
||||||
|
|
||||||
## [0.70.0] - 2026-04-23
|
## [0.70.0] - 2026-04-23
|
||||||
|
|
||||||
|
|||||||
@@ -77,12 +77,14 @@ Edit directly or use `/settings` for common options.
|
|||||||
|
|
||||||
| Setting | Type | Default | Description |
|
| Setting | Type | Default | Description |
|
||||||
|---------|------|---------|-------------|
|
|---------|------|---------|-------------|
|
||||||
| `retry.enabled` | boolean | `true` | Enable automatic retry on transient errors |
|
| `retry.enabled` | boolean | `true` | Enable automatic agent-level retry on transient errors |
|
||||||
| `retry.maxRetries` | number | `3` | Maximum retry attempts |
|
| `retry.maxRetries` | number | `3` | Maximum agent-level retry attempts |
|
||||||
| `retry.baseDelayMs` | number | `2000` | Base delay for exponential backoff (2s, 4s, 8s) |
|
| `retry.baseDelayMs` | number | `2000` | Base delay for agent-level exponential backoff (2s, 4s, 8s) |
|
||||||
| `retry.maxDelayMs` | number | `60000` | Max server-requested delay before failing (60s) |
|
| `retry.provider.timeoutMs` | number | SDK default | Provider/SDK request timeout in milliseconds |
|
||||||
|
| `retry.provider.maxRetries` | number | SDK default | Provider/SDK retry attempts |
|
||||||
|
| `retry.provider.maxRetryDelayMs` | number | `60000` | Max server-requested delay before failing (60s) |
|
||||||
|
|
||||||
When a provider requests a retry delay longer than `maxDelayMs` (e.g., Google's "quota will reset after 5h"), the request fails immediately with an informative error instead of waiting silently. Set to `0` to disable the cap.
|
When a provider requests a retry delay longer than `retry.provider.maxRetryDelayMs` (e.g., Google's "quota will reset after 5h"), the request fails immediately with an informative error instead of waiting silently. Set to `0` to disable the cap.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -90,7 +92,11 @@ When a provider requests a retry delay longer than `maxDelayMs` (e.g., Google's
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"maxRetries": 3,
|
"maxRetries": 3,
|
||||||
"baseDelayMs": 2000,
|
"baseDelayMs": 2000,
|
||||||
"maxDelayMs": 60000
|
"provider": {
|
||||||
|
"timeoutMs": 3600000,
|
||||||
|
"maxRetries": 0,
|
||||||
|
"maxRetryDelayMs": 60000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -315,10 +315,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|||||||
if (!auth.ok) {
|
if (!auth.ok) {
|
||||||
throw new Error(auth.error);
|
throw new Error(auth.error);
|
||||||
}
|
}
|
||||||
|
const providerRetrySettings = settingsManager.getProviderRetrySettings();
|
||||||
const openRouterAttributionHeaders = getOpenRouterAttributionHeaders(model, settingsManager);
|
const openRouterAttributionHeaders = getOpenRouterAttributionHeaders(model, settingsManager);
|
||||||
return streamSimple(model, context, {
|
return streamSimple(model, context, {
|
||||||
...options,
|
...options,
|
||||||
apiKey: auth.apiKey,
|
apiKey: auth.apiKey,
|
||||||
|
timeoutMs: options?.timeoutMs ?? providerRetrySettings.timeoutMs,
|
||||||
|
maxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,
|
||||||
|
maxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,
|
||||||
headers:
|
headers:
|
||||||
openRouterAttributionHeaders || auth.headers || options?.headers
|
openRouterAttributionHeaders || auth.headers || options?.headers
|
||||||
? { ...openRouterAttributionHeaders, ...auth.headers, ...options?.headers }
|
? { ...openRouterAttributionHeaders, ...auth.headers, ...options?.headers }
|
||||||
@@ -353,7 +357,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|||||||
followUpMode: settingsManager.getFollowUpMode(),
|
followUpMode: settingsManager.getFollowUpMode(),
|
||||||
transport: settingsManager.getTransport(),
|
transport: settingsManager.getTransport(),
|
||||||
thinkingBudgets: settingsManager.getThinkingBudgets(),
|
thinkingBudgets: settingsManager.getThinkingBudgets(),
|
||||||
maxRetryDelayMs: settingsManager.getRetrySettings().maxDelayMs,
|
maxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Restore messages if session has existing data
|
// Restore messages if session has existing data
|
||||||
|
|||||||
@@ -16,11 +16,17 @@ export interface BranchSummarySettings {
|
|||||||
skipPrompt?: boolean; // default: false - when true, skips "Summarize branch?" prompt and defaults to no summary
|
skipPrompt?: boolean; // default: false - when true, skips "Summarize branch?" prompt and defaults to no summary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProviderRetrySettings {
|
||||||
|
timeoutMs?: number; // SDK/provider request timeout in milliseconds
|
||||||
|
maxRetries?: number; // SDK/provider retry attempts
|
||||||
|
maxRetryDelayMs?: number; // default: 60000 (max server-requested delay before failing)
|
||||||
|
}
|
||||||
|
|
||||||
export interface RetrySettings {
|
export interface RetrySettings {
|
||||||
enabled?: boolean; // default: true
|
enabled?: boolean; // default: true
|
||||||
maxRetries?: number; // default: 3
|
maxRetries?: number; // default: 3
|
||||||
baseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s)
|
baseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s)
|
||||||
maxDelayMs?: number; // default: 60000 (max server-requested delay before failing)
|
provider?: ProviderRetrySettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TerminalSettings {
|
export interface TerminalSettings {
|
||||||
@@ -354,6 +360,30 @@ export class SettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migrate retry.maxDelayMs -> retry.provider.maxRetryDelayMs
|
||||||
|
if (
|
||||||
|
"retry" in settings &&
|
||||||
|
typeof settings.retry === "object" &&
|
||||||
|
settings.retry !== null &&
|
||||||
|
!Array.isArray(settings.retry)
|
||||||
|
) {
|
||||||
|
const retrySettings = settings.retry as Record<string, unknown>;
|
||||||
|
const providerSettings =
|
||||||
|
typeof retrySettings.provider === "object" && retrySettings.provider !== null
|
||||||
|
? (retrySettings.provider as Record<string, unknown>)
|
||||||
|
: undefined;
|
||||||
|
if (
|
||||||
|
typeof retrySettings.maxDelayMs === "number" &&
|
||||||
|
(providerSettings?.maxRetryDelayMs === undefined || providerSettings?.maxRetryDelayMs === null)
|
||||||
|
) {
|
||||||
|
retrySettings.provider = {
|
||||||
|
...(providerSettings ?? {}),
|
||||||
|
maxRetryDelayMs: retrySettings.maxDelayMs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
delete retrySettings.maxDelayMs;
|
||||||
|
}
|
||||||
|
|
||||||
return settings as Settings;
|
return settings as Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -683,12 +713,19 @@ export class SettingsManager {
|
|||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
getRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number; maxDelayMs: number } {
|
getRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number } {
|
||||||
return {
|
return {
|
||||||
enabled: this.getRetryEnabled(),
|
enabled: this.getRetryEnabled(),
|
||||||
maxRetries: this.settings.retry?.maxRetries ?? 3,
|
maxRetries: this.settings.retry?.maxRetries ?? 3,
|
||||||
baseDelayMs: this.settings.retry?.baseDelayMs ?? 2000,
|
baseDelayMs: this.settings.retry?.baseDelayMs ?? 2000,
|
||||||
maxDelayMs: this.settings.retry?.maxDelayMs ?? 60000,
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getProviderRetrySettings(): { timeoutMs?: number; maxRetries?: number; maxRetryDelayMs: number } {
|
||||||
|
return {
|
||||||
|
timeoutMs: this.settings.retry?.provider?.timeoutMs,
|
||||||
|
maxRetries: this.settings.retry?.provider?.maxRetries,
|
||||||
|
maxRetryDelayMs: this.settings.retry?.provider?.maxRetryDelayMs ?? 60000,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user