@alma-harness/providers
v0.12.0
Published
ModelClient adapters for Anthropic and OpenAI over Alma's neutral message format.
Readme
@alma-harness/providers
ModelClient adapters for Alma:
Anthropic and OpenAI first-party, plus the OpenRouter gateway bridge, over one
neutral message format.
Status: pre-1.0. The API is still moving; see the roadmap for where it stands.
What it owns
AnthropicModelClient— Messages API, streaming, with explicitcache_controlbreakpoints (at the wire's default duration, or the hour a policy asks for — spec: cache-ttl; the other two wires refuse the ask) at the stable-system boundary and the conversation tail.OpenAIModelClient— Responses API, streaming, automatic prefix caching.OpenRouterModelClient— the gateway bridge (chat completions). A gateway does not answer "who processed this data?" by itself, so construction REQUIRES a declared upstream allowlist, fallbacks are off by default, data collection is denied by default, andrequire_parametersrides along whenever the request carries tools. Opening the catalogue changes nothing about what the routing policy permits — that stays a product/DPO decision.- The translation between Alma's neutral
Msg/Blockformat and each provider's wire format, in both directions.
Routing decides which client runs; the adapter only speaks the protocol.
import { OpenRouterModelClient } from "@alma-harness/providers";
const client = new OpenRouterModelClient({
upstreams: ["deepinfra", "together"], // the declared data-processing chain
});Credentials
The harness never owns credentials. Each client accepts an explicitly injected key or falls back to the SDK's own environment resolution — configuration comes from the hosting environment, never from a file this repo reads.
import { AnthropicModelClient } from "@alma-harness/providers";
const client = new AnthropicModelClient(); // reads ANTHROPIC_API_KEY
const explicit = new AnthropicModelClient({ apiKey }); // or inject itJobs
AnthropicJobClient (Message Batches) and OpenAIJobClient (the Batch API,
over a JSONL file of /v1/responses requests) implement the ModelJobClient
seam (spec: model-jobs): the same request translations, minus streaming, and
a non-streaming translation of the complete answer. OpenRouter has no batch
API.
Reasoning
When the policy sets ModelChoice.reasoning (spec: reasoning-blocks), each
adapter asks for it and hands it back as one complete block per step:
| effort | Anthropic | OpenAI | OpenRouter |
|---|---|---|---|
| absent | nothing sent; default output dropped | nothing sent; default items dropped | nothing sent |
| none | thinking: disabled | reasoning.effort: none | reasoning.enabled: false |
| others | thinking: adaptive + output_config.effort | reasoning.effort + encrypted content | reasoning.effort |
An adapter replays only its own blocks, and only when reasoning is on for the request; every other reasoning block in the history is skipped.
Provider-executed web search
ModelRequest.providerTools (spec: provider-tools) declares the provider's
own search beside the registered tools; what comes back is two neutral
blocks, provider_tool_call and provider_tool_result, the provider's
payload kept opaque for replay.
| | Anthropic | OpenAI | OpenRouter |
|---|---|---|---|
| declaration | web_search_20250305 (the direct search; the agentic 2026-03-18 version drives code_execution and is another kind) with max_uses, allowed_domains, blocked_domains | { type: "web_search", filters: { allowed_domains } } + include: ["web_search_call.action.sources"]; blockedDomains is REFUSED before the network; maxUses is the loop's to enforce | refused before the network |
| on the stream | server_tool_use → call; web_search_tool_result → result (url, title, page age; an error code as error); usage.server_tool_use.web_search_requests | a completed web_search_call item → call (input: action) and result (the sources; failed as an error), counted | — |
| replay | its own two blocks as server_tool_use + web_search_tool_result with the encrypted content, only while the request declares the kind (a capped step replays the text and skips the search); another provider's skipped | its own result's opaque as the item, only while the request declares the kind; the call block skipped | skipped |
| pause_turn | → the neutral pause: the loop re-sends | — | — |
Media
| | Anthropic | OpenAI | OpenRouter |
|---|---|---|---|
| image by URL | source: { type: "url" } | input_image with the URL | image_url with the URL |
| image by bytes (spec: media-by-bytes) | base64 source; JPEG, PNG, GIF, WebP, else refused | input_image with a data URL | image_url with a data URL |
| document | by URL: document source · by bytes: base64 PDF, else refused | by bytes only: input_file with a data URL and the ref's filename, PDF · by URL refused | refused |
| audio | refused — products transcribe upstream | refused | refused |
A block carries bytes when the loop attached content from the product's MediaSource; the adapters never fetch anything and never see the ref's path on the bytes path.
Structured output and cache controls
ModelRequest.output selects native JSON schema output (spec:
provider-request-controls). It introduces no tool or business handler. Responses
still arrive as text and usage/stop events: validate the result and inspect
refusal/truncation before treating it as application data. The governed
single-call runner is not part of this slice.
| Control | Anthropic | OpenAI | OpenRouter |
|---|---|---|---|
| output: {type: "json_schema", name, schema} | output_config.format with type/schema, preserving reasoning effort | text.format with strict: true | refused |
| format name | validated portable label; no wire slot, intentionally omitted | required wire name | refused |
| cache: false | removes both explicit breakpoints | refused | refused |
| cache: {ttl, conversationTail: false} | only the last stable system block gets a breakpoint | refused | refused |
| absent controls | existing defaults | existing defaults | existing defaults |
Names use 1–64 ASCII letters, digits, underscores or dashes. Schemas must be objects and comply with the selected provider's strict schema subset; arbitrary tool input schemas are not automatically converted. Model-specific schema support is validated by the provider. Cache controls preserve message roles and order; disabling the tail prevents a system suffix from being included in that breakpoint. Disabling prompt cache is not a data-retention or erasure setting.
Streaming and batch reuse these mappings (toAnthropicBatchRequests and
toBatchLines expose the pure batch translations). Unsupported options reject
before network activity, including batch file upload. These fixtures prove
serialization against installed SDK types, not live model acceptance.
Arguments the wire cut
A tool call whose arguments do not parse — a response cut by max_tokens
mid-arguments, or a model that emitted broken JSON — is still a tool_call
(spec: what-the-wire-cuts): input: {} and the raw text as malformed,
followed by the wire's own stop reason. The loop answers a malformed call
with an invalid-input result on tool_use, and closes it on max_tokens.
The translators never throw on it.
Errors
What an SDK throws leaves every client as a ProviderError from
@alma-harness/core (spec: error-taxonomy), classified through one
duck-typed table over the fields the SDKs share: 429 → rate_limited
(OpenAI's insufficient_quota → rejected); 529, 503 or an overloaded body
→ overloaded; no status or 5xx → unavailable; a 400 that says the prompt
does not fit → context_window; the other 4xx → rejected. A user abort
passes through untouched. The translation error classes are
ProviderErrors too — rejected before the network, provider_drift
mid-stream — and toProviderError / classifyFailure are exported for a
product wrapping its own client.
What it must never do
- Decide routing, spend, or capability. Those are core, not adapter.
- Leak a provider type into the neutral format — the format is the boundary that makes a second provider cheap.
- Swallow provider drift: a stream that ends without a stop event fails loudly rather than looking like a successful empty turn.
Documentation
Docs index · Architecture §6.2 · Anthropic adapter spec · OpenAI adapter spec
Apache-2.0
Opt-in single-dispatch clients
createAnthropicSingleDispatchClient({ apiKey?, baseURL? }) and
createOpenAISingleDispatchClient({ apiKey?, baseURL? }) return a frozen
SingleDispatchModelClient. They disable SDK retries and HTTP redirects and
withhold authentication-provider, middleware and transport injection options.
The existing constructors keep their current retry/usage behavior; batches and
OpenRouter do not implement this new capability.
The capability accepts the same neutral tools, tool results, supported media and
reasoning history as the first-party translators, plus supported system, JSON
schema, reasoning, tier and cache controls (spec: rich-single-dispatch-requests).
It executes no local tool handler; the host owns authorization and sensitivity. validate is pure and
streamEvidence validates/clones before its first network await. These checks
prove local adapter support, not model-specific schema acceptance. Clone errors
are content-free rejected ProviderErrors. Consume each returned stream once.
Known final counts are emitted before stop or a terminal translation error.
Missing/invalid/partial usage never becomes zero. unpriced preserves counts
for unknown served tiers or missing/mixed/inconsistent cache TTL attribution, and must
not be sent to pricing with a fallback. Anthropic final cumulative updates win;
OpenAI input excludes cache reads AND writes to avoid counting writes twice.
OpenAI search counts use unique completed item identities and terminal output when
present; missing/conflicting attribution remains unknown. Anthropic search activity
requires a valid positive cumulative search count. Known and unpriced evidence
retain search counts without copying queries or URLs into billing metadata.
Only bounded provider request IDs enter reference events, including HTTP errors.
Content events remain content: never store the whole evidence stream as a
financial row. OpenAI still sends store: false.
The legacy stream method on these new clients converts known evidence to a
usage event and fails on unknown/unpriced evidence. Drain streamEvidence to
retain known usage even if a later error occurs. Pre-abort is not-dispatched;
all ambiguous network outcomes require reconciliation. An early return aborts
transport even immediately after headers, but cannot promise final usage.
This is transport evidence, not durable deduplication or invoice reconciliation.
Reasoning history replays through the existing translator when reasoning is enabled and the block belongs to that provider. Preserve opaque replay content unchanged. This support does not itself adopt durable multi-turn execution. Reusing a stream fails with a rejected ProviderError. Caller cancellation preserves the AbortSignal reason, including custom errors.
Explicit temperature
temperature is optional and sent unchanged in streaming and batch bodies. The
matrix admits Anthropic 1, plus 0..1 for claude-haiku-4-5 and
claude-haiku-4-5-20251001 with reasoning absent or none
(spec: haiku-temperature-compatibility),
and OpenAI GPT-5.1/GPT-5.2 base IDs or dated snapshots with explicit reasoning
none, in 0..2. Pro/chat/codex variants and OpenRouter reject it. This is a
conservative adapter matrix; other Anthropic IDs remain explicit-1-only. Omit it for
provider defaults. Invalid/unsupported values fail before network or batch upload;
zero is preserved. There is no automatic downgrade or determinism guarantee.
Single-dispatch batch evidence
createAnthropicSingleDispatchBatchClient and
createOpenAISingleDispatchBatchClient implement execution's additive
SingleDispatchBatchClient. Frozen capabilities admit apiKey/baseURL only,
with SDK retries disabled and redirects rejected. Legacy job clients are unchanged.
Validate 1..512 unique batch-tier items (IDs: ASCII alphanumeric, underscore or
hyphen, max64), one model, and a 16 MiB wire bound before network. Requests and
handles are snapshotted before asynchronous work. Use a trusted immutable
configuration for baseURL/account selection.
Consume submitEvidence exactly once after a durable dispatch:true decision. OpenAI yields input_file before its separate create POST, then accepted. Save needed file evidence before resuming. Lost responses never authorize another attempt; creating a new iterable is a new attempt. File cleanup is not provided. Resolve handles and expected item IDs from the scoped durable manifest before polling or collecting: the adapter itself provides no tenant authorization.
resultsEvidence requires terminal status and yields independently validated usage and content, including when translation fails. Cancellation in progress remains running. Both OpenAI result files are read, even for terminal cancellation. Anthropic result URLs must match the configured origin and batch results path before authenticated retrieval. JSONL is incremental, with a 2 MiB line/32 MiB collection limit, max512 expected IDs, and readers cancelled on abandonment. Unknown, duplicate or missing IDs fail the collection. Prior observations survive such failure and must not be interpreted as complete collection or free remainder.
Batch transport supplies batch billing for compatible default tier fields; contradictory tiers and mixed cache TTLs remain unpriced. Missing counts stay unknown. Content has no duplicate usage field; provider errors have closed reasons. Server-tool content may be unusable while its validated search charges survive. These are adapter prerequisites, not durable collection or settlement. No runner, routine, production delivery or live billing validation is implied.
Governed OpenRouter requests
createOpenRouterSingleDispatchClient({ upstreams: ['anthropic'], apiKey })
implements the same SingleDispatchModelClient capability as the first-party
factories. It snapshots the declared upstream list, denies data collection by
default, and disables gateway fallbacks, SDK retries and HTTP redirects. Optional
zeroDataRetention and explicit dataCollection retain the gateway translator's
controls. allowFallbacks: true is rejected; the legacy constructor remains
unchanged until cutover. The key defaults only from OPENROUTER_API_KEY.
The adapter preserves the current text/tool/image/reasoning translator and its unsupported-control refusals. Financial evidence uses native token counts from the final usage chunk: ordinary input excludes cache reads and writes, and reasoning is a subset of output. Positive cache writes without a TTL are unpriced; missing or malformed counts are unknown. A context-window HTTP error never proves zero usage. Candidate counts stay private until the stream closes or fails; contradictory observed chunks fail without an early known charge. The provider's cost field does not replace the runner's persisted price table. The promise is one Alma inference attempt, with gateway fallback disabled, not an attestation of the gateway's internal infrastructure. See spec openrouter-single-dispatch.
The Anthropic single-dispatch adapter attests anthropic_context_window_v1 only
for the exact structured HTTP 400 input-window rejection before SSE, with a
bounded request ID and known zero usage. Generic errors and post-stream failures
remain uncertain. OpenAI/OpenRouter do not attest this proof. No automatic SDK
retry is added; the governed conversation owns any continuation after settlement
(spec: safe-context-rejection-rotation).
Both official single-dispatch batch clients support cancel(handle, {signal?})
(spec: governed-batch-cancellation-adoption). Each explicit invocation sends at
most one cancellation POST, with SDK retries and redirects disabled. Handles are
snapshotted before awaiting; returned identity, status and counts are validated.
Anthropic canceling and OpenAI cancelling remain running. Lost acknowledgements
and malformed responses reject without another request or invented terminal state.
Explicit client-tool selection
ModelRequest.toolChoice is optional host policy (spec: governed-tool-choice).
Absence sends no tool_choice and retains previous provider behavior; explicit
auto remains distinct from absence in execution identity. No choice grants
permission to a tool or runs its handler.
| Neutral control | Anthropic streaming / batch | OpenAI Responses / batch | OpenRouter |
|---|---|---|---|
| { type: "auto" } | { type: "auto" } | "auto" | refused |
| { type: "none" } | { type: "none" } | "none" | refused |
| { type: "required" } | { type: "any" } | "required" | refused |
| { type: "tool", name } | { type: "tool", name } | { type: "function", name } | refused |
Forced modes (required / tool) admit these exact IDs only:
- Anthropic:
claude-haiku-4-5,claude-haiku-4-5-20251001,claude-sonnet-4-5,claude-sonnet-4-5-20250929,claude-sonnet-4-6,claude-opus-4-5,claude-opus-4-5-20251101,claude-opus-4-6,claude-opus-4-7. - OpenAI:
gpt-4.1,gpt-4.1-mini,gpt-5.1,gpt-5.2,gpt-5.6-luna,gpt-6-sol,gpt-6-luna.
Other IDs, including unlisted dated snapshots, reject forcing before network.
Auto/none use the documented first-party wire without a forced-model allowlist;
model availability and general model capabilities remain provider concerns.
All explicit choices reject simultaneous output or nonempty providerTools.
Forced modes on other models reject an explicit reasoning effort other than none.
For exact OpenAI gpt-5.6-luna, use explicit reasoning none/low/medium/high/xhigh/max
in both execution controls and the loaded request (spec: luna-forced-reasoning).
Omitted, minimal, unknown and malformed efforts reject; no provider default is
silently selected. Active efforts retain the existing summary and encrypted
reasoning transport for stateless replay in Responses and batch. Reasoning tokens
are already part of output tokens and are not charged again.
Auto/none and omitted selection retain existing behavior. Omit temperature;
Luna temperature support is unchanged. The remaining conservative model/provider
guards are retained rather than extrapolated from Luna compatibility.
No control is silently downgraded, and absence bypasses this new matrix.
A named tool must match exactly one advertised ToolSpec; forced modes need
client tools. Invalid shapes, names and ambiguous registries reject before
stream dispatch or batch file upload. The single-dispatch clients snapshot
requests before asynchronous work; all first-party batch mappings share the
stream translator. OpenRouter explicit selection is refused until a reviewed
model/upstream matrix can guarantee local validation.
For a governed structured one-call result, use createGovernedStepRunner from
@alma-harness/single-call with the same choice in planned execution controls and its
loaded request. Inspect the tool calls, original stop and receipts; a forced
choice does not guarantee valid arguments, a successful stop or exactly one
block. The text/JSON createSingleCallRunner does not accept this control.
See the single-call composition. There is no automatic tool
execution or personal-memory write on the structured step path.
Sources checked 2026-09-17: Anthropic tool selection, Anthropic thinking compatibility, OpenAI function calling, Luna capabilities and default effort, OpenAI batch body parity, OpenRouter tool calling. Deterministic tests establish serialization, rejection and governed composition. A maintainer-authorized synthetic named-tool probe on 2026-09-17 succeeded with medium and xhigh; the latter returned nonzero reasoning usage. This is not a quality evaluation, live batch proof or consumer integration claim.
GPT-6 Sol and Luna
Exact gpt-6-sol and gpt-6-luna support Responses streaming, governed single
calls and per-item batch translation. Forced tools require explicit
none|low|medium|high|xhigh|max reasoning. Unsupported efforts reject before
HTTP even without forced selection. Without forcing, omitted reasoning preserves
the provider default. Temperature and simultaneous output/forced-tool guards
remain unchanged. No application model default is switched automatically.
These two models send system blocks as ordered developer input text, with an
explicit breakpoint at the end of the initial consecutive stable prefix and
prompt_cache_options: {mode: "implicit", ttl: "30m"}. The provider selects the
conversation breakpoint. Keep instructions/tools/history stable and place changing
content after the stable prefix. A stable block following volatile content does
not receive a breakpoint. Existing model request shapes remain unchanged.
OpenAI requires at least 1,024 visible input tokens for cache eligibility; no
hit or latency reduction is guaranteed. Cache writes cost 1.25 times ordinary
input, reads 0.1 times, and each reuse renews the minimum 30-minute lifetime.
The adapter subtracts both counts from ordinary input on legacy and governed
paths, so writes are not billed twice. Reasoning remains included in output.
fast is normalized to neutral priority; a contradictory tier in governed
batch remains unpriced.
The existing cache field expresses Anthropic 5m/1h TTL controls. OpenAI still
rejects explicit values, including false; this release adds automatic prefix
placement, not a new public cache-policy contract. Prewarming, cache keys,
explicit-only mode and mid-conversation reasoning changes are not exposed.
store:false remains set; it does not disable provider KV caching or promise
zero cache retention. Confirm organizational data-retention requirements.
Host price table
Prices remain host-owned and versioned. Populate both cache rates rather than relying on core's ordinary-input fallback. Standard USD per million tokens, checked 2026-09-22:
| Model | Input | Cache read | Cache write | Output | |---|---:|---:|---:|---:| | gpt-6-sol | 2 | 0.20 | 2.50 | 10 | | gpt-6-luna | 0.10 | 0.01 | 0.125 | 0.50 |
For each tier row, add bands: [{aboveInputTokens: 272_000, ...rates}]
with input/read/write rates doubled and output multiplied by 1.5. The threshold
uses ordinary input plus reads plus writes and applies to the whole request.
Flex/batch multiply all standard rates by 0.5; priority/Fast by 2. Regional
processing adds 10% where applicable; EU residency supports Standard only.
These token prices do not cover separately billed provider tools.
const model = { provider: "openai" as const, id: "gpt-6-luna" };
const prices = [{
model, inputUsdPerMTok: 0.10, outputUsdPerMTok: 0.50,
cacheReadUsdPerMTok: 0.01, cacheWriteUsdPerMTok: 0.125,
bands: [{ aboveInputTokens: 272_000, inputUsdPerMTok: 0.20,
outputUsdPerMTok: 0.75, cacheReadUsdPerMTok: 0.02,
cacheWriteUsdPerMTok: 0.25 }],
}];Sources: Sol, Luna, prompt caching, Fast mode. Synthetic fixtures verify translation, accounting and installed governed replay; no live cache-hit, model-quality or batch-acceptance claim is implied.
