Skip to content

fix: Concentrate adapter timeout/retries/error taxonomy, stream diagnostics, Gemini request-ID - #110

Merged
Patel230 merged 5 commits into
mainfrom
fix/audit-sweep-2026-08
Aug 16, 2026
Merged

fix: Concentrate adapter timeout/retries/error taxonomy, stream diagnostics, Gemini request-ID#110
Patel230 merged 5 commits into
mainfrom
fix/audit-sweep-2026-08

Conversation

@Patel230

Copy link
Copy Markdown
Contributor

Summary

  • Concentrate adapter — replaced 120s whole-response http.Client.Timeout with shared pooled client; added DoWithRetry (matching openai/anthropic); replaced fmt.Errorf with ParseProviderError/FormatAPIError for structured error taxonomy; fixed normalizeToolParams mutating caller's map.
  • Stream diagnostics — non-fatal health diagnostics (e.g. reasoning-only responses) now marked as warnings and forwarded without setting Err() — the terminal done/usage event reaches the host correctly.
  • Gemini request-ID — captured X-Goog-Request-Id now propagated to NewStreamResult in both parser paths.
  • MiMo retry helper — deduplicated doRequestWithMimoAuthRetry from openai/anthropic into shared client/adapters/mimo.go.

Test plan

  • GOWORK=off go build ./...
  • Targeted tests: client/adapters (334), client/core (90), engine (90)
  • Note: one pre-existing failure in ./config (TestHasAnyConfiguredDeployment_RejectsPlaceholder) — reproduced on base commit; unrelated.

…ards

The Concentrate adapter lagged behind every other provider client in
three ways (audit E1-E3):

- E1: it built a private &http.Client{Timeout: 120s} instead of using
  the shared pooled transport. The hard-coded whole-response timeout
  killed any stream longer than two minutes and bypassed connection
  reuse. It now uses core.NewPooledHTTPClient(core.DefaultTimeout).
- E2: SetRetry was a no-op claiming retries were handled at the HTTP
  client level (http.Client never retries). Both request paths now go
  through core.DoWithRetry with GetBody set, mirroring openai.go and
  anthropic.go, and SetRetry/Retry store and return the config.
- E3: non-200 responses were read with an unbounded io.ReadAll and
  returned as fmt.Errorf strings, dropping status/request-ID structure
  so IsRetriable()/IsAuthError() and engine classify() could not work.
  Errors now come from core.ParseProviderError + core.FormatAPIError
  (*core.EyrieError with provider/op/status/request-ID), and the
  captured X-Request-Id is propagated to stream results.
- normalizeToolParams mutated the caller's tool schema map in place;
  it now returns a shallow copy with additionalProperties injected.

Tests cover retry-on-500-then-success, structured auth/rate-limit
errors on chat and stream paths, streaming through a slow server with
the default pooled client, and the no-mutation contract.
client/core's ProcessOpenAIStream emits end-of-stream health diagnostics
(reasoning-only or empty responses, via DetectResponseHealth) as an
error-type event immediately followed by the terminal done event — the
comment says non-fatal. But engine/stream.go mapped every error event
to &Error{Code: ErrorProviderUnavailable} and forward() returned, so
the terminal done/usage event never reached the host and Err() was set
even though content had been delivered. Reasoning-only responses
routinely trigger this diagnostic, failing otherwise-successful
streams (audit E4).

Event-flow change:
- client/core/stream.go finish() now marks the diagnostic error event
  with the existing EyrieStreamEvent.Warning field (additive; .Error is
  still populated for consumers that only read it).
- engine/stream.go normalizeEvent() forwards error events carrying
  Warning as Event{Type: EventWarning, Warning: ...} without an error,
  so forward() emits it and keeps consuming; the final done/usage is
  delivered and Err() stays nil. Error events without Warning remain
  terminal provider_unavailable failures, identical to before.
- client/continuation.go (deprecated helper) and client/tracing.go
  apply the same distinction: diagnostics are forwarded without ending
  the stream / failing the span.

Tests: core asserts the diagnostic carries Warning and precedes done;
engine adds regression tests for diagnostic-then-done (content + done
delivered, Err() nil) and fatal error (terminal, provider_unavailable).
Gemini StreamChat captured the X-Goog-Request-Id response header (used
for error correlation) but passed "" to llm.NewStreamResult on both
the shared-parser and legacy-parser paths, so successful streams lost
the provider correlation ID. Pass the captured value (audit E5).
doRequestWithMimoAuthRetry was duplicated verbatim between the OpenAI
and Anthropic adapters; extract it into a package-level
doWithMimoAuthRetry (next to mimoAuthHeaders) that takes a
setRetryHeaders callback for the provider-specific Bearer headers.
Both methods keep their signatures as thin wrappers — no behavior
change.
…o 1.26.6

- concentrate_responses.go: resp.Body was read but never closed when the
  stream request returned a non-200 status (bodyclose)
- go.mod + CI: Go 1.26.6 — 1.26.5 stdlib has reachable vulns
  (GO-2026-6090, GO-2026-6089) that fail govulncheck
@Patel230
Patel230 merged commit ede6671 into main Aug 16, 2026
16 checks passed
@Patel230
Patel230 deleted the fix/audit-sweep-2026-08 branch August 16, 2026 03:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant