Feat: Network Resilience Retry Timeout - #35
Draft
tcsizmadia wants to merge 3 commits into
Draft
Conversation
tcsizmadia
marked this pull request as draft
August 4, 2026 10:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add retry with exponential backoff and request timeouts to all Berget API fetch calls, preventing transient network errors (
ECONNRESET,ETIMEDOUT, socket hang up) from surfacing as fatal "connection reset by server" failures.Context & Motivation
Problem: Users experience "connection reset by server" errors when using the Berget AI provider. Other providers remain stable on the same machines, indicating the issue is in the plugin's transport layer — not the user's network. The plugin's
fetchWithAuthwrapper called rawfetch()with zero retry, zero timeout, and zero error classification. A singleECONNRESETkilled the entire request.Why now: The plugin is the sole transport layer between OpenCode and Berget (unlike providers that use OpenCode's built-in HTTP client). Every API request — chat completions, streaming, token refresh, model discovery — flows through the plugin's custom fetch. Network resilience is not optional for an AI inference provider proxy.
Root causes identified:
fetchWithAuth(src/plugin.ts:36) — barefetch()with no retry or timeoutrefreshAccessTokenInternal(src/plugin/token.ts:182) — treated ECONNRESET as permanent failure during token refreshAbortSignal.timeouton any fetch call — requests hung indefinitely on half-open connectionsfetchBergetModels(src/plugin/models.ts:42) — no retry, silently fell back to stale defaultsSolution
Introduced a new
resilientFetchwrapper (src/plugin/resilient-fetch.ts) that replaces all rawfetch()calls in the plugin. The wrapper is transparent — it has the same signature asfetch()and returns a standardResponse.Retry strategy:
ResilientFetchOptionsfor callers that need different behaviorWhat gets retried:
ECONNRESET,ETIMEDOUT,ECONNABORTED,EPIPE,ENOTFOUND,EAI_AGAIN, socket hang up, "Failed to fetch"What does NOT get retried:
Timeout: 30s default via
AbortSignal.timeout(), composable with caller-provided abort signals viaAbortSignal.any().Changes
Features/Major Changes
resilientFetch(src/plugin/resilient-fetch.ts) — New fetch wrapper with retry, timeout, and error classification. Self-contained module with no external dependencies.fetchWithAuth(src/plugin.ts:36) — Now delegates toresilientFetchinstead of rawfetch(). Every API request to Berget (chat completions, streaming) automatically retries on transient errors.refreshAccessTokenInternal(src/plugin/token.ts:182) — UsesresilientFetchfor the token refresh HTTP call. Network errors during token refresh now retry instead of immediately failing.fetchBergetModels(src/plugin/models.ts:42) — UsesresilientFetchfor model discovery. Transient failures during startup no longer silently fall back to stale defaults.Bug Fixes/Minor Changes
handleErrorResponse(src/plugin/token.ts:94) — Removed redundant 5xx retry logic (now handled at transport layer byresilientFetch). This function now only handles domain-level errors (invalid_grant, invalid_token).refreshAccessTokenInternal(src/plugin/token.ts:211) — Changedconsole.errortologErrorfor consistent debug logging.Testing
npm test)npm run typecheck)npm run lint)npm run format:check)Test coverage:
ECONNRESET,ETIMEDOUT,ECONNABORTED, socket hang up, "Failed to fetch"Review Focus
resilientFetch(src/plugin/resilient-fetch.ts:65) — Core retry loop. Verify the retry/skip logic for both error types (thrown errors vs retryable HTTP statuses).isRetryableError(src/plugin/resilient-fetch.ts:149) — Error classification. Ensure we don't retry non-transient errors (e.g., DNS resolution failures that are permanent).fetchWithTimeout(src/plugin/resilient-fetch.ts:127) — Signal composition. VerifyAbortSignal.any()correctly merges caller signal with timeout signal.handleErrorResponse(src/plugin/token.ts:94) — Confirm the 5xx retry removal doesn't break the invalid_grant recovery flow.Breaking Changes
None.
resilientFetchis a drop-in replacement forfetch()with the same signature and return type. The plugin's external API is unchanged.Deployment Notes
Standard deployment. The new
resilient-fetch.tsmodule has no external dependencies — it uses only Node.js built-in APIs (fetch,AbortSignal,setTimeout).Additional Context
OPENCODE_BERGET_DEBUG=1) now shows retry attempts:Network error on attempt 1/4: read ECONNRESET, retrying in 387msResilientFetchOptionsinterface is exported for future callers that need custom retry behavior