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-0040: Anthropic provider adapter
ADRs

ADR-0040: Anthropic provider adapter

Architecture decision record 0040 — Anthropic Messages API adapter with OpenAI-compatible wire translation, raw HTTP client, and 529-aware retries.

ADR-0040: Anthropic provider adapter

  • Status: Accepted
  • Date: 2026-08-21
  • Authors: IBEX Harness team
  • Milestone: 2.5.G1.M1 Anthropic provider adapter

Context

Phase 2 shipped a single OpenAI-compatible provider under packages/provider/openai/ and a proxy that forwards OpenAI-shaped JSON and SSE verbatim (ADR-0025, ADR-0026, ADR-0027). Phase 2.5 requires a second, dialect-different vendor to prove the Provider interface generalizes.

Anthropic’s Messages API differs from OpenAI chat completions:

  • top-level system instead of role: system messages
  • required max_tokens
  • named SSE events (message_start, content_block_delta, …) without data: [DONE]
  • HTTP 529 overloaded_error, including mid-stream event: error after HTTP 200

The official Go SDK (github.com/anthropics/anthropic-sdk-go) handles retries and typed streams, but would add transitive surface area, risk stacking retries with ours, and fight the existing provider.Response.Body io.ReadCloser contract.

Decision

1) Raw net/http client, mirroring OpenAI

Implement packages/provider/anthropic/ with stdlib HTTP. Shared transport/retry helpers live in packages/provider (httputil.go); Anthropic-specific request/response/SSE translation stays vendor-local. Do not add anthropic-sdk-go. Justification is recorded in DEPENDENCIES.md.

2) OpenAI-compatible wire translation inside the adapter

The public proxy API remains OpenAI chat completions. Anthropic Complete returns:

  • non-stream: OpenAI chat.completion JSON
  • stream: OpenAI chat.completion.chunk SSE plus trailing data: [DONE]\n\n

Streaming translation uses io.Pipe so slow clients apply natural backpressure (no unbounded in-memory SSE buffer). Assembled Anthropic event payloads are capped. The proxy SSE forwarder and openai.StreamAccumulator stay verbatim over that OpenAI-shaped body (ADR-0027). Reframing is adapter-local, not proxy SSE rewriting.

Client PassthroughFields for Anthropic use an allowlist (top_p, stop_sequences, metadata, service_tier) so powerful Messages fields (tools, thinking blocks, etc.) cannot be injected in this text-only milestone.

3) Request translation stays vendor-local

  • Extract leading consecutive system messages into Anthropic’s top-level system without mutating the shared provider.Request
  • Coalesce consecutive same-role turns; fail closed if the first non-system turn is assistant
  • When MaxTokens <= 0, default to 4096 (Anthropic requires max_tokens)
  • Fold mid-conversation system turns into the top-level system field (directive-injection order safety)
  • Reject empty turn content locally (fail closed before the network hop)
  • Headers: x-api-key, anthropic-version: 2023-06-01, content-type: application/json

4) Anthropic-specific retries

Retry HTTP 429, 500, 502, 503, 504, 529 and pre-delivery transport errors (dial/connect only). Do not retry timeouts, cancel, or deadline — a timed-out POST may already have been accepted upstream. Honor Retry-After on 429/503/529. No retry after a live streaming body is returned. Mid-stream Anthropic event: error terminates the translate pipe as incomplete — never JSON-swap mid-stream to the client.

5) Live multi-provider registration

When IBEX_LLM_MODE=live, register every provider that has credentials:

  • OPENAI_API_KEY → OpenAI
  • ANTHROPIC_API_KEY → Anthropic

Live mode requires at least one key. Duplicate model IDs fail at NewRegistry startup.

Consequences

Positive:

  • Proves ADR-0025 for a non–OpenAI-compatible dialect without changing the client contract
  • Keeps dual-write / StreamAccumulator working
  • Avoids SDK dual-retry and dependency bloat on the hot path

Negative / follow-ups:

  • Must maintain Anthropic↔OpenAI translation (text-only in this milestone; tools/images deferred)
  • Built-in Claude model allowlist needs periodic refresh via ANTHROPIC_EXTRA_MODELS

Was this page helpful?

Edit on GitHub

Last updated on

PreviousADR-0039: Proxy Postgres ownership for session and directive storesNextADR-0041: Model capability registry

On this page

  • Context
  • Decision
  • 1) Raw net/http client, mirroring OpenAI
  • 2) OpenAI-compatible wire translation inside the adapter
  • 3) Request translation stays vendor-local
  • 4) Anthropic-specific retries
  • 5) Live multi-provider registration
  • Consequences
0%