sendWatchedProviderRequest function

Future<StreamedResponse> sendWatchedProviderRequest(
  1. Client httpClient,
  2. Request request,
  3. CancelToken? cancelToken
)

One watched send: the cancel-token race plus the connect watchdog, with a bounded TRANSPARENT retry when the watchdog kills a request that never received a byte (issue #1121).

Public for transports that must own their response semantics and so bypass sendProviderRequest's redirect decision and status/body validation — chatgpt-codex stores Cloudflare cookies from error responses and replays challenges itself, so it takes the raw watched send and keeps its own cookie/challenge logic.

Implementation

Future<http.StreamedResponse> sendWatchedProviderRequest(
  http.Client httpClient,
  http.Request request,
  CancelToken? cancelToken,
) async {
  for (var attempt = 0; ; attempt++) {
    // A sent `http.Request` is finalized by the client and cannot be sent
    // again — every attempt after the first rides a fresh clone (the
    // redirect `_reissue` clones for the same reason).
    final attemptRequest = attempt == 0
        ? request
        : _reissue(request, request.url, 0);
    try {
      return await _sendWatchedOnce(
        httpClient,
        attemptRequest,
        cancelToken,
        attempt,
      );
    } on TimeoutException {
      // The connect watchdog fired (issue #1121). `httpClient.send`
      // completes only when the response HEADERS arrive, so a timeout in
      // THIS layer means zero response bytes — the provider never started
      // generating. That makes an in-place retry safe where a mid-stream
      // retry is not: no partial stream to corrupt, no committed state, and
      // nothing double-billed (a request the endpoint never answered is not
      // a billed generation) — the identical payload is simply re-sent.
      // The budget is deliberately separate from the roles failover ladder
      // (a network stall must not rotate the model chain, #1066 semantics)
      // and from TransientRetryStream (governed by bytes AFTER the first).
      // Mid-stream silence never re-enters here — the idle watchdog fires
      // downstream in createSseIterator — so only the never-started request
      // ever retries.
      if (attempt >= providerConnectRetries) rethrow;
      final delay = providerConnectRetryBackoff * (1 << attempt);
      // Observable on the EXISTING retry surface: the CLI host prints a dim
      // `[net]` line + fa.log entry; hosts leaving the hook null stay
      // silent — no new telemetry.
      transientRetryNotice?.call(
        attempt + 1,
        providerConnectRetries + 1,
        delay,
        'connect stall: no response bytes',
      );
      // Bounded backoff; the sleeper races the cancel token, so a user
      // abort during the wait wins over the pending retry.
      final survived = await transientRetrySleeper(delay, cancelToken);
      if (!survived) {
        cancelToken?.throwIfCancelled(); // the abort propagates
        rethrow;
      }
    }
  }
}