Repository navigation
Retry, response size, and timeout policy differ by provider branch #23
Description
The five provider branches do not share a transport policy, so which backend you configure decides whether a transient upstream failure is retried, how large a response is accepted, and whether a stalled connection is ever given up on.
Everything below is read off the current main (69ffb4b) and the installed client libraries; I have not seen any of this fail in production.
Retries are inherited from two libraries and absent from three branches
typesafegoes through@typesafe-ai/sdk(src/provider.ts:108), and the TypeSafe SDKs ship a retry policy — the API's own docs say 429/529 should be retried with exponential backoff and that "our client SDKs handle this automatically" (docs.typesafe.ai/api, Handling rate limits).vercelgoes throughexperimental_evaluatefromai(src/provider.ts:216), which callsprepareRetries({ maxRetries, abortSignal })and wraps the model call inretry(...). In the installed version (ai@7.0.105) the retryable set is408,409,429, and anything>= 500, with a default of 2 retries.openrouter(src/provider.ts:132),compatible(:159), andcloudflare(:233) each issue a singlefetchwith no status handling: a 429 or a 503 becomesisError: trueon the first answer. There is no retry helper anywhere insrc/.
So the same question asked through JEV_PROVIDER=compatible gets one attempt while JEV_PROVIDER=vercel gets three, and the failure a caller sees for a rate limit depends on which backend they happen to have credentials for. The status classes involved are not exotic: OpenRouter and the Cloudflare AI gateway both return 429 and 5xx, and the TypeSafe-compatible path is frequently a proxy in front of the same endpoint the docs describe.
Nothing bounds the response body
All three fetch branches read the body whole — await response.json() at src/provider.ts:148, :175, and :244. There is no Content-Length check and no byte ceiling. For the direct and Cloudflare paths the upstream is TypeSafe's own token budget, but compatible reads JEV_API_BASE_URL, which is operator-supplied: a proxy that answers a 2xx with a large HTML page (or a misconfigured base URL pointing somewhere else) is buffered into memory in full, and the JSON parse then fails in a way the caller sees as an opaque error. For comparison, itsmostafa/typesafe-mcp added a 16 MiB ceiling in 0.4.2 and chose to reject oversized responses rather than truncate them, which is the same fail-closed instinct this repo applies to malformed answers.
Nothing sets a deadline
No branch creates its own timeout: signal is threaded in from the caller (src/provider.ts:104, :115) and the fetch calls pass it through, so an upstream that accepts the connection and stalls holds the tool call open until the MCP client's own request timeout fires. typesafe-mcp uses a 60 s HTTP timeout for the same call.
What I am asking
Is this asymmetry deliberate — each client library owns its own policy and the hand-rolled branches stay minimal — or would a small shared transport helper be welcome? If it is welcome, the smallest useful version would be:
- bounded attempts with jittered backoff on 429/5xx (and 408/409 to match the AI SDK), leaving every other status to the existing error path;
- one byte ceiling on the read, erroring rather than truncating, so a bad
JEV_API_BASE_URLcannot turn into a memory problem; - one deadline per request, so a hung upstream fails as a timeout rather than as silence.
If it is deliberate, a sentence in the README next to the provider table saying which branches inherit retries and which do not would settle it just as well — that is genuinely the only thing I need to know before deciding whether to send a PR for any of this.
Found while comparing the retry and response-bound behavior here against itsmostafa/typesafe-mcp, which implements the 429/529 backoff and the size ceiling and leaves it at that.
Activity
Transport resilience: bounded retries, deadline, byte ceiling, cancel…