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-0025: LLM provider abstraction
ADRs

ADR-0025: LLM provider abstraction

Architecture decision record 0025 — provider interface, registry, and package boundaries for Phase 2.

ADR-0025: LLM provider abstraction

  • Status: Accepted
  • Date: 2026-07-12
  • Authors: IBEX Harness team
  • Milestone: 2.1.1 Provider interface and registry

Context

When this ADR was written, Phase 1 validated and authenticated every POST /v1/chat/completions request but returned 501 PROVIDER_NOT_CONFIGURED with no upstream LLM call. Phase 2 later added mock/live forwarding via packages/provider. Anthropic lands in Phase 2.5 (ADR-0040); Azure OpenAI and AWS Bedrock remain later multi-provider work. Hard-coding OpenAI HTTP calls in the proxy handler would require a large refactor when multi-provider support lands.

The proxy already parses OpenAI-shaped JSON in services/proxy/internal/llm (Phase 1). Provider communication needs a shared abstraction in packages/provider with no service imports.

Decision

1) Single Provider interface with one Complete method

Go
type Provider interface {
    Complete(ctx context.Context, req Request) (Response, error)
    Name() string
    SupportedModels() []string
}

Request.Stream bool selects streaming vs non-streaming inside the implementation. A split interface (Streamer + Completer) would force callers to type-assert on every request.

2) Model routing and capabilities live in Registry, not providers

Each provider declares SupportedModels(). Registry.For(model) returns the implementation. Phase 2.5 registers Anthropic (ADR-0040); later phases add Azure OpenAI / Bedrock without changing handlers or middleware contracts.

NewRegistry takes a CapabilityCatalog and returns ErrDuplicateModel / ErrMissingCapability on conflicts or missing metadata — fail fast at startup, not on first customer request. Registry.Capability(model) exposes curated context window / feature / tokenizer-family rows (ADR-0041).

3) API keys are constructor details, not interface fields

Provider credentials come from environment variables (Phase 2) or org-scoped config (later). The Provider interface is key-agnostic; packages/provider/openai/ reads keys in its constructor.

4) Response.Body is io.ReadCloser

Streaming responses must not be fully buffered on the hot path. Non-streaming callers read the full body and decode JSON. The caller closes Body.

5) Messages-only request contract; directive injection in proxy

provider.Request.Messages is authoritative when Complete is called. Agent directive injection (milestone 2.3.3) runs in proxy middleware (packages/injection) and mutates Messages before the provider call. Provider implementations must not implement injection logic — this avoids duplicating injection between OpenAI client (2.1.2) and proxy middleware (2.3.3).

6) Package boundary

packages/provider imports only stdlib and other packages/* modules. No services/* imports. Concrete implementations live in subpackages (e.g. packages/provider/openai/ in milestone 2.1.2).

7) Registry lifecycle

Built once at proxy startup via provider.NewRegistry(catalog, ...). Read-only after construction. Milestone 2.1.1 shipped an empty registry (no providers registered); 2.1.2 registers OpenAI; 2.5.G1.M2 requires a capability entry for every registered model ID.

8) Error types

  • ErrNoProviderForModel → proxy returns 501 PROVIDER_NOT_CONFIGURED
  • ErrMissingCapability / ErrDuplicateModel → startup/config failure (never silent at request time)
  • ProviderError carries provider HTTP status and raw body for mapping in 2.1.5 — never log ProviderBody

9) Phase 2 proxy Postgres exception

Phase 1 rule: proxy has no Postgres for identity (auth via gRPC only). Phase 2 milestones 2.3.2 and 2.4.x add a proxy-owned database/sql pool for directive and session stores (including session writes), opened in bootstrap only. Formal decision: ADR-0039.

Consequences

Positive:

  • Phase 2.5 Anthropic registration is already live; later Azure/Bedrock work stays registration-only
  • Shared package testable offline at ≥90% unit coverage
  • Handler stays provider-agnostic
  • Milestone 2.1.4 extracted registry lookup from handler into ProviderRoutingMiddleware (no behavior change)

Negative:

  • Duplicate Message types in llm and provider — conversion function in 2.1.2 bridges them

References

  • Provider adapters
  • Phase 2 goals
  • ADR-0013: Proxy input validation and error envelope

Was this page helpful?

Edit on GitHub

Last updated on

PreviousADR-0024: Benchmark data publishing modelNextADR-0026: OpenAI client design

On this page

  • Context
  • Decision
  • 1) Single Provider interface with one Complete method
  • 2) Model routing and capabilities live in Registry, not providers
  • 3) API keys are constructor details, not interface fields
  • 4) Response.Body is io.ReadCloser
  • 5) Messages-only request contract; directive injection in proxy
  • 6) Package boundary
  • 7) Registry lifecycle
  • 8) Error types
  • 9) Phase 2 proxy Postgres exception
  • Consequences
  • References
0%