IBEX Harness
DocsBenchmarksBlogChangelogRoadmap
GitHub
IBEX Harness

Documentation

Architecture Decision RecordsADR-0002: Repository foundation bootstrapADR-0003: Branch protection and merge policyADR-0004: Protobuf and code generation policyADR-0005: Postgres migration strategyADR-0006: Auth protobuf contract (`ibex.auth.v1`)ADR-0007: Auth token validation implementationADR-0008: Security scanning and CI quality gatesADR-0009: Permission bitmap layoutADR-0010: Cryptography policyADR-0011: Proxy auth gRPC client and middlewareADR-0012: Proxy request normalization (OpenAI chat)ADR-0013: Proxy input validation and stable error envelopeADR-0014: Core domain migration sequencingADR-0015: Proxy rate limit skeleton (Phase 1)ADR-0016: Proxy agent identity verification (Phase 1)ADR-0017: Request ID and trace context strategy (Phase 1)ADR-0018: Graceful shutdown contract (Phase 1)ADR-0019: OpenTelemetry provider configuration (Phase 1)ADR-0020: Shared package boundaries — `packages/config` and `packages/apierror`ADR-0021: Prometheus Metric Catalog (Phase 1)ADR-0022: Health check contract (Phase 1)ADR-0023: Docs site architecture (Phase 1.5)ADR-0024: Benchmark data publishing modelADR-0025: LLM provider abstractionADR-0026: OpenAI client designADR-0027: Streaming dual-write strategyADR-0028: Auth cache designADR-0029: Token revocation propagation via Redis pub/subADR-0030: Directive versioning strategyADR-0031: System prompt injection strategyADR-0032: Session data model and retentionADR-0033: ClickHouse llm_traces schema and retentionADR-0034: Proxy overhead performance measurement methodologyADR-0035: Chat Idempotency-Key Redis dedupeADR-0038: Context assembly service design and gRPC contractADR-0039: Proxy Postgres ownership for session and directive storesADR-0040: Anthropic provider adapterADR-0041: Model capability registryADR-0042: Self-hosted OpenAI-compatible LLM adapterADR-0043: Tokenizer registry architectureADR-0044: Non-streaming response pipelineADR-0045: Streaming response transformationADR-0046: Embedder interface and profile registryADR-0047: Memory temporal validity foundationADR-0048: Memory multi-label categoriesADR-0049: Memory relationship graph readinessADR-0050: MCP server skeleton (transport, auth, audit)ADR-0051: Local LGTM observability stack (Phase 2.5 exit pull-forward)ADR-0052: Memory schema v2 expand (HNSW, quality columns)ADR-0053: Vector store abstraction and composite scoring v2
ADRs›ADR-0044: Non-streaming response pipeline
ADRs

ADR-0044: Non-streaming response pipeline

Architecture decision record 0044 — typed OpenAI-shaped response decode, stage pipeline seam, raw-byte passthrough policy, and fail-open execution for non-streaming chat completions.

ADR-0044: Non-streaming response pipeline

  • Status: Accepted
  • Date: 2026-08-22
  • Authors: IBEX Harness team
  • Milestone: 2.5.G3.M1 Response middleware hook

Context

Non-streaming chat completions today pass upstream JSON verbatim from writeProviderSuccess (services/proxy/internal/http/chat_provider.go). Phase 3 guardrails (PII redaction, injection scanning, metadata injection) and Phase 3.5 embedded ibex response blocks require a typed extension point between provider response and client write.

Constraints:

  • ADR-0020 — response transformation stays out of packages/provider/* adapters.
  • ADR-0034 — proxy overhead budget; decode+noop pipeline must benchmark p99 < 2ms.
  • ADR-0027 — streaming SSE forwarding is unchanged; streaming transformation is deferred to 2.5.G3.M2.

Wire shape confirmed against v1 providers:

SourceShape
mockllmOpenAI chat.completion JSON
openaicompatible / OpenAINative OpenAI JSON
anthropic adapterTranslated to OpenAI chat.completion before proxy sees body

Decision

1) packages/responsepipeline contract

Go
type Stage interface {
    Name() string
    Process(ctx context.Context, resp *ChatResponse) (*ChatResponse, error)
}
 
type SecurityCritical interface {
    SecurityCritical() bool
}
  • Pipeline.Run executes stages in order.
  • Fail-open: non-critical stage errors log (stage name + error class), restore a pre-stage snapshot (so in-place mutations from the failing stage are discarded), and continue with subsequent stages.
  • Fail-closed: stages implementing SecurityCritical with SecurityCritical() == true propagate errors to the proxy as ProviderError with HTTP 502 via providerErr502 (client envelope maps to provider-unavailable 503 through existing MapError).

2) Raw-byte preservation (noop default)

ChatResponse stores a clone of upstream bytes plus a typed ResponseDoc. When no stage marks the response modified, Bytes() returns the original raw bytes (no json.Marshal), guaranteeing byte-for-byte passthrough with the default noop pipeline. Callers must not mutate the returned slice.

Stages should prefer Mutate(fn func(*ResponseDoc) error), which sets the dirty flag automatically. Direct Doc() mutation requires an explicit MarkModified() or client-visible bytes stay stale.

When dirty, Bytes() re-encodes from ResponseDoc (Phase 3+).

Stage author contract: do not mutate the response on an error return path; fail-open rollback covers accidental in-place edits, but stages should treat errors as leaving the response untouched.

3) Proxy wiring (non-streaming only)

  • Bootstrap builds NewDefaultPipeline() (single noop stage).
  • RouterDeps.ResponsePipeline → chatCompletionHandler.responsePipeline.
  • writeProviderSuccess: ReadAllBody → Decode → Pipeline.Run → Bytes() → WriteJSONBody.
  • Invalid upstream JSON → ProviderError / 502 (same posture as corrupt provider body).
  • Streaming path (forwardSSEStream) does not invoke the pipeline in G3.M1.

4) Logging and metrics

Never log choices[].message.content. Stage failure logs include stage name and a stable error_class only (never raw error strings, which may echo response or directive content).

Optional PipelineObserver records:

MetricTypeLabels
ibex_proxy_response_pipeline_stage_duration_secondsHistogramstage, result (success/error/fail_open)
ibex_proxy_response_pipeline_stage_fail_open_totalCounterstage

Bootstrap wires the proxy *metrics.ProxyRegistry as the observer when non-nil.

5) Config surface (v1)

No new environment variables in G3.M1. Per-agent pipeline configuration deferred.

Consequences

Positive:

  • Phase 3 can add stages without new proxy plumbing.
  • No-op default preserves today’s client-visible behavior and idempotency replay bytes.
  • Typed model enables future metadata injection (3.5.D.3).

Negative / follow-ups:

  • Modified responses re-encode typed fields and merge preserved unknown top-level keys from upstream JSON.
  • Streaming pipeline requires G3.M2 / ADR-0045 before Phase 3 implementation.
  • Security-critical stages not shipped in M1 — interface only.

References

  • ADR-0020 Shared package boundaries
  • ADR-0027 Streaming dual-write
  • ADR-0034 Performance methodology

Was this page helpful?

Edit on GitHub

Last updated on

PreviousADR-0043: Tokenizer registry architectureNextADR-0045: Streaming response transformation

On this page

  • Context
  • Decision
  • 1) packages/responsepipeline contract
  • 2) Raw-byte preservation (noop default)
  • 3) Proxy wiring (non-streaming only)
  • 4) Logging and metrics
  • 5) Config surface (v1)
  • Consequences
  • References
0%