@zephyrcloudio/harness-llm
Portable Effect Schema and Stream contracts for one provider model attempt.
The package owns canonical instructions and messages, model-visible tool descriptors, per-request
generation and provider options, assistant-message-bound private continuation artifacts, exact
attempt usage with optional provider billing provenance, ordered model events, typed failures, and
the scoped Model service.
Provider adapters lower these values to their own wire formats.
It does not execute tools, retry requests, mutate history, persist data, run the model/tool continuation loop, or decide Turn outcomes. The scripted model is a deterministic test provider and uses only Effect synchronization primitives.
Production Turns use this contract through the Core-owned continuation loop. Provider-specific wire values stay inside their native provider adapters.
Stateful providers use openModelRequestChain to allocate one scoped chain per Turn. The chain is
reused for retries and model/tool rounds, while request-chain state stays off the reusable service.
ModelService.stream MUST return its declared Stream synchronously. It must not be async or
return a Promise, thenable, or Proxy wrapper. Put asynchronous provider work inside the returned
Stream. Malformed async returns are unsupported trusted-code behavior. Ordinary Promise rejection
observation is best effort and is not a security boundary. Portable JavaScript cannot guarantee
observation for Proxy-hidden Promises or Promises with hostile Symbol.species behavior whose
constructor cannot be reversibly shadowed. A typed Turn outcome is not guaranteed. Under Node's
strict unhandled-rejection behavior, the process may terminate and the raw rejection reason may
appear in process diagnostics.
Native provider authoring contract
defineProvider defines a native LLM provider module. A native provider module exports its
defineProvider(...) value as a named export (default exports are not used):
definitionId: stable lowercase branded string matching/^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.version: author-owned version string.optionsSchema: Effect Schema codec for provider options.credentials: optional Effect Schema codec for private persisted credentials (e.g. API keys).requestOptionsSchema: Effect Schema codec for per-request options.filterModel: optional deterministic capability/protocol filter for catalog models.correctModel: optional narrow corrections for catalog models (upstream ID, limits, etc.).acquireModel: scoped synchronous Effect returning theModelServicefor execution. All callbacks must be synchronous (noasync/Promise):acquireModelreturns itsEffect,filterModelaboolean,correctModelthe corrected metadata. Core decodesoptionsonce per setup with the definition's schema, soacquireModelreceives already-validated options.credentialsresolve live at each acquisition (stored/configured/env) and arrive as opaqueRedactedvalues — validate emptiness defensively, as the compatible provider does. It constructs theModelServicedirectly instead of building a throwawayLayerper Turn. The acquisition input also carries the exact upstream model id, optional per-model API root, and selected wireprotocol(chat,responses, ormessages). Providers with a fixed protocol may ignore the protocol; a multi-protocol provider dispatches on it instead of hardcoding model lists.
Declarative compatible providers
makeCompatibleProviderDefinition builds one native provider definition from validated data. It
uses Ze's Chat Completions, Responses, and Messages adapters directly; no AI SDK sits between Ze and
the upstream API. The definition can configure routes, bearer/header/no authentication,
protocol-specific authentication and headers, static headers, Session-derived headers, and typed
failure classification. Core combines that transport with catalog-selected model protocols,
endpoints, upstream IDs, and body defaults.
Chat and Responses authenticate with bearer tokens by default. Messages uses x-api-key by default.
An explicit authentication rule overrides these defaults. Unsupported protocols are rejected by the
schema rather than routed to a fallback wire.
Provider base URLs
The protocol adapters share one base URL contract (providers/base-url.ts). A
configured base URL must be an absolute http(s) URL without credentials, query,
or fragment, including bare ?/# delimiters. Query and fragment are rejected
rather than stripped or preserved: silently rewriting the configured root would
change proxy routing without telling the operator, and preserving them would
forward ambient URL material into every provider request. Endpoint paths are
joined from the validated parsed pathname, so trailing slashes never produce
double slashes or a missing path segment.
Provider request deadlines
The protocol adapters share one deadline helper (providers/timeout.ts).
requestTimeoutMs bounds the whole HTTP request as total elapsed time, not
each idle interval. Omitted or null means unlimited. Progress never resets the
deadline: request setup, error-body reads, and every response-body pull are
bounded by the time remaining since the request started. Expiration fails the
attempt with ModelTimeoutFailure (timeoutKind: 'attempt-limit') after the
adapter's normal body cleanup aborts the host request.
Wire parser bounds
The protocol adapters share one size bound (providers/limits.ts): a single
SSE event and a single HTTP error body are each capped at 1 MiB. Overflow
fails the attempt with a typed malformed failure, so one pathological payload
cannot exhaust host memory before a typed failure is produced.
Credential boundary threat model
The provider already holds the API key by design (it authenticates every
request). The boundary keeps the key out of the opaque values a provider hands
back, which a proxy or gateway that reflects the Authorization header could
echo: response IDs and their continuation reference, the provider finish
reason, and failure evidence (provider codes, request IDs, header evidence).
A value that contains the credential is dropped, not rewritten.
Model output (text, reasoning, tool input, and continuation payloads) is not
rewritten. As in OpenCode and Pi, keep secrets out of what the model can read
instead: route calls through a gateway that holds the provider key, and carry
credentials as Redacted values to the request header.
Non-goals: the boundary is not a sandbox (adapters are trusted code), and it does not defend against a provider that misuses the key it already holds.
Images in tool results
ToolResultValue accepts { type: 'content', content: [...] } alongside text and JSON.
Each content part is text, JSON, or an image using the same URL/base64 references as
user images. The durable equivalent is CanonicalToolResult with format: 'content'
and the parts in value.
Responses and the Codex Responses deployment send these parts inside function_call_output.output.
Anthropic Messages nests them in tool_result.content. Chat Completions accepts only text in tool replies,
so its adapter sends text and JSON in the tool reply and adds labeled images in a user
message after the contiguous tool replies. Images retain their tool call IDs in those
labels; canonical history keeps them attached to the original tool result.
Public TypeScript exports
AssistantContentPartAssistantMessagebuiltinCredentialTransportsbuiltinLoginModulecanonicalizeProviderFailureCompatibleModelDiscoveryCompatibleProviderAuthenticationCompatibleProviderConfigCompatibleProviderConfigValueCompatibleProviderCredentialsCompatibleProviderCredentialsValueCompatibleProviderFailureRulesCompatibleProviderOptionsCompatibleProviderOptionsValueCompatibleProviderProtocolAuthenticationCompatibleProviderProtocolHeaderscompatibleProviderRequiresCredentialCompatibleProviderRoutesCompletedContentPartCredentialTransportCredentialTransportBindingcredentialTransportRegistryCredentialTransportRegistrydefineProviderDeviceLoginDescriptordiscoverCompatibleModelsdiscoverDeviceLoginDiscoveredRemoteModelFinishReasonGenerationSettingsInvalidProviderDefinitionErrorjoinProviderEndpointLegacyProviderLoginmakeCodexLoginmakeCompatibleProviderDefinitionmakeModelLayermakeModelTurnContextmakeOAuth2DeviceLoginmakeScriptedModelLayerMalformedProviderDataFailureMalformedProviderResponseFailureMalformedProviderStreamFailureModelModelAttemptUsageModelAuthenticationFailureModelCallIdentityModelContextOverflowFailureModelEventModelFailureModelImplementationModelIncompleteResponseFailureModelInstructionModelInstructionsModelInvalidRequestFailureModelMessageModelMessagesModelPermissionFailureModelProtocolmodelProviderIdModelRateLimitFailureModelRequestModelRequestChainModelRequestChainImplementationModelRequestStateModelRetryDirectiveModelServiceModelStreamTerminationFailureModelTimeoutFailureModelToolCallPartModelToolChoiceModelToolDescriptorModelTransportFailureModelTurnContextmodelTurnContextCallmodelTurnContextSessionIdModelUnavailableFailureModelUnsupportedCapabilityFailureNoProviderOptionsnormalizeProviderFailureOAuth2RefreshFailureopenModelRequestChainPrivateContinuationArtifactPrivateContinuationArtifactsPrivateMessageContinuationPrivateMessageContinuationsProviderBilledUnitProviderCostProviderCredentialSourceProviderDefinitionProviderFailureInputProviderLoginAttemptProviderLoginErrorProviderLoginMethodImplementationProviderLoginModuleProviderModelAcquisitionInputProviderModelCorrectionProviderModelDiscoveryErrorProviderModelFilterProviderRequestOptionsProviderResponseIdProviderUsageProvenanceReasoningBoundaryPartReasoningPartrefreshOAuth2CredentialRefusalPartRunnerCredentialScriptedModelScriptedModelControlScriptedModelRequestScriptedModelScriptScriptedModelStepstaticRunnerCredentialTextPartToolJsonSchemaToolMessageToolResultPartToolResultValueupgradeLegacyCodexCredentialUserContentPartUserMessagevalidateModelEventStream