/ docsGitHub ↗
Browse documentation
EXPERIMENTAL · LOCAL-FIRST

Evaluate carefully. Live-provider paths are experimental and are not money-grade production infrastructure.

Provider pack schema v1#

A provider pack describes one provider integration as data: a single TOML file that a reviewed Gongbu engine runs. Adding a provider, a model or a price becomes a pack change instead of Rust code and a release.

Status. Schema v1 is defined, Gongbu validates and loads packs, and the sync_fixed and async_poll engines run them. Every shipped contract (Gemini and FLUX.2) runs from a verified pack in packs/verified/. The hubu provider commands that activate your own packs arrive in a later release.

Verified packs#

Verified packs live in packs/verified/ and are compiled into Gongbu. Each shipped contract in contracts/provider-contracts-v1.json whose target settings say type = "pack" is that pack's checked-in projection: the same provider_config_version, pricing version, review date and pricing rules, pinned to the pack's content digest. Gongbu's tests fail if a pack and its contract entries disagree, so a pack change ships with a matching contract update.

What a pack declares and what engines own#

A pack declares what a provider is. An engine decides how calls are made safely, and a pack can't change that.

Owned by the engine (a pack can't change it) Declared by the pack
Zero mutation retries; ambiguous outcomes go to reconciliation Endpoint, API version, allowed HTTPS hosts
The proven-before-transmission rule for releasing a claim Auth header and credential alias
Durable checkpoints and read-only resume of polling Agent-settable parameters with types and bounds; fixed fields
Settling only from the frozen pricing snapshot Pricing rules, pricing_version, pricing_reviewed_on
Credentials sent only to transport.allowed_hosts Response paths: provider request ID, usage, reported cost, artifacts
Artifact media type, size and host checks Artifact media types and download hosts

Gongbu rejects a pack that sets an engine-owned field anywhere in the file, and the error names the field: retries, max_retries, generation_retries, retry, fallback, reconciliation, recovery, settlement, credential_hosts or idempotency_header. For example:

transport.max_retries: engine-owned field is not allowed in a pack; Gongbu never retries a provider mutation

Archetypes#

archetype Hold Settle Fits
sync_fixed Exact price from the request Same frozen price One request returns the result (Gemini images, search APIs)
async_poll Frozen price × units Frozen price plus the durable provider job ID Submit a job, then poll (FLUX)
metered Worst case, such as max_output_tokens × output rate Reported usage, never above the hold Model calls billed by usage

Example#

schema_version = 1
pack_id = "sandbox-image"
pack_version = "2026-10-19.1"
provider = "sandbox-image"
display_name = "Sandbox image provider"
billing_merchant = "merchant:sandbox-image"
capability = "capability:image:generate"
workload_type = "image_generation"
archetype = "sync_fixed"
pricing_trust = "verified"

[transport]
endpoint = "https://api.sandbox-image.test"
api_version = "v1"
allowed_hosts = ["api.sandbox-image.test"]
auth_header = "x-api-key"
credential = "sandbox_image"
timeout_ms = 30000

[[parameters]]
name = "prompt"
type = "string"
required = true
max_length = 4000

[[parameters]]
name = "image_count"
type = "integer"
field = "n"
min = 1
max = 4
default = 1
quantity = "image_count"

[[parameters]]
name = "quality"
type = "enum"
values = ["draft", "final"]
default = "draft"
selector = "quality"

[fixed]
response_format = "url"

[response]
provider_request_id = "/id"
usage = { images = "/data" }
artifact_items = "/data"
artifact_url = "/url"

[artifacts]
media_types = ["image/png"]
hosts = ["cdn.sandbox-image.test"]

[[models]]
model = "image-v1"
provider_config_version = "sandbox-image-v1-2026-10-19"
pricing_version = "sandbox-image-v1-usd-2026-10-19-v1"
pricing_reviewed_on = "2026-10-19"

[[models.pricing_rules]]
rule_id = "sandbox-image-v1-draft-2026-10-19"
currency = "USD"
selector = { quality = "draft" }
components = [
  { unit = "request", rate_numerator_minor = 1, rate_denominator = 10 },
  { unit = "image", rate_numerator_minor = 15, rate_denominator = 10 },
]

[[models.pricing_rules]]
rule_id = "sandbox-image-v1-final-2026-10-19"
currency = "USD"
selector = { quality = "final" }
components = [
  { unit = "request", rate_numerator_minor = 1, rate_denominator = 10 },
  { unit = "image", rate_numerator_minor = 6, rate_denominator = 1 },
]

With this pack, {"prompt": "a red circle", "image_count": 2, "quality": "final"} is held at exactly 0.1¢ + 2 × 6¢ = 12.1¢. The provider receives {"prompt": "a red circle", "n": 2, "quality": "final", "response_format": "url"}.

Fields#

Identity#

Field Rules
schema_version Required. 1.
pack_id Required lowercase slug (a–z, 0–9, ., _, -). One pack per provider integration.
pack_version Required slug. Change it with every content change.
provider Required slug. Becomes the provider ID of every target and pricing rule.
display_name Required, 1–128 characters.
billing_merchant Required, merchant:<name>.
capability Required, capability:<name>, for example capability:image:generate.
workload_type Required slug, for example image_generation.
archetype Required: sync_fixed, async_poll or metered.
adapter Optional slug recorded as the target's adapter ID. Defaults to the archetype.
pricing_trust Required: verified (shipped in the repository with reviewed pricing) or unverified (written by a user; live use will require an explicit acknowledgement).

[transport]#

Field Rules
endpoint Required HTTPS origin with no port, path or query.
allowed_hosts Required. Host names that receive the credential, including the endpoint host as an exact name.
api_version Required slug.
auth_header Required HTTP header name that carries the credential.
credential Required credential alias. The stack profile binds it to a Keychain item; the secret never appears in a pack.
timeout_ms Required overall deadline, within Gongbu's provider deadline range.
headers Optional non-secret headers. authorization, cookie and the auth header are rejected.
auth_scheme Optional: raw (default, the secret as is) or bearer (Bearer <secret>).

[request]#

Optional. Engines send one JSON POST to path on the endpoint; without [request], the path is /.

Field Rules
path Required absolute path, such as /v1beta/interactions, with no query, fragment or ...
body Optional body template. Without it, the body is [fixed] plus each parameter under its field.

In a body template, a string that is exactly {{name}} becomes the typed value of parameter name, and {{model}} becomes the model ID. {{name|upper}} and {{name|lower}} change a string's case. A key whose placeholder has no value (an optional parameter with no default) is left out. A placeholder must be the whole string, so values are never spliced into text. With a template, [fixed] and field aren't used, and every parameter must appear in the template unless it is a constant (an integer with min = max, or an enum with one value):

[request]
path = "/v1beta/interactions"

[request.body]
model = "{{model}}"
input = [{ type = "text", text = "{{prompt}}" }]
response_format = { type = "image", image_size = "{{image_size|upper}}" }

[poll]#

Required for async_poll packs and rejected for the others. The engine submits once, checkpoints the job, and polls read-only until the job is ready, refused, or the transport deadline passes. Every poll URL is rebuilt from this declared shape; the engine never follows a provider-supplied URL as is.

Field Rules
job_id Required response path to the job ID in the submission response.
path Required poll path of plain segments; one segment may be exactly {{job_id}}.
query Optional poll query. Keys are lowercase tokens; a value is a plain token or exactly {{job_id}}. The job ID must appear in the path or the query.
url Optional response path to a polling URL the provider returns. Its host must match hosts, and its path and query must equal the declared shape. A missing or rejected URL is checkpointed and sent to reconciliation without polling another host. When url is not declared, the engine polls the configured endpoint.
hosts Polling hosts (or patterns) a returned URL may name. They receive the credential, so each must also be in transport.allowed_hosts. Required with url.
interval_ms Required delay between polls, from 1 to transport.timeout_ms.
status Required response path to the job status, in submission and poll responses.
pending / ready Required lowercase status values meaning "keep polling" and "map the result". Statuses compare case-insensitively.
rejected Status values meaning the provider refused the job, each mapped to a provider_<reason> failure code. The claim is released.
ambiguous Status values whose outcome is unknown, each mapped to a failure code. The execution goes to reconciliation.

A submission response with no status is treated as pending once it carries a job ID; one already ready is mapped without polling. A poll response whose job ID differs from the checkpoint's goes to reconciliation.

[poll]
job_id = "/id"
path = "/v1/get_result"
query = { id = "{{job_id}}" }
url = "/polling_url"
hosts = ["api.eu.example.com", "api.us.example.com"]
interval_ms = 500
status = "/status"
pending = ["pending", "generating"]
ready = ["ready"]
rejected = { "content moderated" = "provider_content_moderated", "error" = "provider_error" }
ambiguous = { "task not found" = "provider_task_not_found" }

Host patterns#

transport.allowed_hosts, poll.hosts and artifacts.hosts accept exact names or patterns with one * label that matches exactly one DNS label, such as delivery.*.example.com for per-region delivery hosts. The * can't stand in the last two labels. An artifact host pattern that could name any credential-bearing host is rejected.

[[parameters]]#

Parameters are the only values an agent can set. Input that names an undeclared parameter, or breaks a parameter's bounds, is rejected before any spend is reserved.

Field Rules
name Required lowercase identifier the agent uses. model, output_width and output_height are reserved for the selected target's model ID and certified output dimensions.
type Required: string, integer, boolean or enum.
field Optional provider request field. Defaults to name.
required / default A required parameter has no default. A default must satisfy the bounds.
max_length Required on string parameters.
min / max integer only.
values enum only: unique, lowercase selector values.
quantity integer only: the priced quantity it sets (image_count, input_tokens, max_output_tokens, results, pages, seconds). Needs min >= 0 and a max, so every hold is bounded.
selector enum only: the pricing selector key it sets.
output_dimensions enum with a selector only: the certified [width, height] for each value, such as { "1k" = [1024, 1024] }. The chosen size is frozen into the request and available to the body template as {{output_width}} and {{output_height}}.
equals integer only: output_width or output_height. The value must equal the size the preset certifies, and the parameter is sent as that size.

A parameter name may sit in one input group, such as options.seed: the agent sends it as {"options": {"seed": 7}}, and the template refers to it as {{options.seed}}. Grouped parameters need a body template. Grouped names such as options.output_width remain separate from the reserved output_width placeholder and cannot replace its certified value.

A priced parameter (quantity or selector) must be required or have a default. [fixed] fields are sent with every request and can't collide with a parameter's field.

[response]#

Every value is a response path: a JSON Pointer into the provider's JSON body whose segments may filter an array. /steps[type=model_output]/content[type=image] keeps the elements of steps whose type is model_output, then the elements of their content whose type is image.

Field Rules
request_id_headers Optional response headers that carry the provider request ID, checked in order.
provider_request_id Optional body path to the request ID, used when no header carries it. Set it, request_id_headers, or both.
usage Map of usage quantity (images, input_tokens, output_tokens, results, pages, seconds) to a path at an integer, at an array whose length counts, or at a filtered selection whose size counts.
optional_usage Optional set of keys already in usage that may be absent. Only informational quantities without a declared request quantity binding may be optional; repeated entries have set semantics.
reported_cost Optional. A decimal amount in USD, such as "0.012". Ignored by sync_fixed, which settles from frozen prices and delivered usage. Other archetypes use the frozen price when the response omits it and send an unreadable reported cost to reconciliation.
reported_cost_scale Optional decimal places to shift the reported cost: 2 when the provider reports one-cent credits. Defaults to 0.
artifact_items Required path to the artifacts: an array, or a filtered selection.
artifact_url / artifact_base64 Exactly one: a path inside each item to a download URL or to inline base64 bytes.

Usage a pack doesn't map settles at the requested quantity, because a fixed-price call delivers what it was asked for. Map a usage path when the provider can return less, such as fewer images than requested; settlement then charges only what arrived.

Every required usage path must be present and have the expected shape. An unfiltered path must select one nonnegative integer or one array. A path whose last segment filters an array counts its matches, including zero matches; missing fields and non-array filter inputs still fail validation. Malformed or missing declared usage sends the response to reconciliation with its provider request ID, instead of treating it as zero or one unit.

For informational telemetry that a provider may omit, list the mapped keys in optional_usage, such as ["input_tokens", "output_tokens"] on an image-only pack. Missing optional values stay unreported; present values must still have the expected shape. Billed quantities, including zero-rate quantities frozen into a bundle, cannot be optional.

A response the mapping can't read arrives after the provider accepted the work, so the engine sends it to reconciliation and never releases the claim.

How the sync_fixed engine classifies outcomes#

Outcome Claim
Input fails the declared parameters, or a retry is requested Released; nothing was sent
The request couldn't be sent Released
HTTP 4xx Released as provider_rejected
Timeout or lost response after sending Reconciliation (timeout_unknown_outcome)
HTTP 5xx or any other non-2xx Reconciliation (provider_failure)
2xx the mapping can't read, an artifact off the allowlist, too large, or of the wrong type Reconciliation, keeping the request ID
2xx that maps cleanly Settled from the frozen price

How the async_poll engine classifies outcomes#

Submission follows the sync_fixed table, with 401, 402, 403 and 429 recorded as provider_authentication_failed, provider_insufficient_credit, provider_permission_denied and provider_rate_limited. Once the provider has accepted a job:

Outcome Claim
A rejected status Released with the pack's code, keeping the job ID
An ambiguous or undeclared status, or a job ID mismatch Reconciliation
Any non-2xx poll response, including 4xx Reconciliation
Lost poll response, or the deadline passes Reconciliation (timeout_unknown_outcome)
A checkpoint naming a host the pack doesn't allow Reconciliation (polling_origin_rejected)
ready and the result maps cleanly Settled from the frozen price, with the job ID as evidence

After a restart, the engine resumes from the checkpoint: it polls the checkpointed host under the original deadline and never resubmits.

[artifacts]#

Field Rules
media_types Required subset of image/png and image/jpeg. Gongbu checks the bytes, not the provider's label.
hosts Download hosts for URL artifacts. They never receive the credential, so a host that is also in transport.allowed_hosts is rejected.

[[models]]#

Field Rules
model Required unique slug.
provider_config_version Required unique slug, frozen into every execution.
pricing_version Required slug.
pricing_reviewed_on YYYY-MM-DD. Required for verified packs.
pricing_rules Required. Same rule format as providers.toml pricing rules, without provider and model, which come from the pack.

Every combination of selector parameter values must be priced by exactly one rule, and every priced unit must be set by a bounded parameter. Every explicit quantity binding must have a corresponding unit in every model's pricing rules, so it is retained in the frozen snapshot. A valid agent input can therefore never reach an unpriced or unbounded request.

For a flat per-request bundle, use ordinary bounded integer parameters without quantity bindings. If a bundled quantity must also be frozen and checked at settlement, give its component a zero rate and put the bundle price in the request component.

Integrity#

Gongbu computes a content digest over the validated pack; reformatting the file doesn't change it. Each model's target revision records the pack ID, version, digest and archetype. A target only binds to the exact pack it was frozen with, so editing a pack is a new revision, never a silent change to executions already in flight.

Binding also requires the runtime catalog's normalized rules for each pack target to match the pinned pack's rules exactly. The pack digest covers each model's pricing_version; a combined catalog can retain its own aggregate version without changing those per-model rules.

Edit this page on GitHub ↗Canonical source: docs/provider-packs.md