Skip to content

AI provider contract: community providers, any model, capped routes and Grida Models facts (0.7.0) - #106

Merged
softmarshmallow merged 17 commits into
mainfrom
provider-contract
Oct 15, 2026
Merged

softmarshmallow merged 17 commits into
mainfrom
provider-contract

Conversation

@softmarshmallow

Copy link
Copy Markdown
Member

Closes #69. Addresses #79 as far as Grida Models' schema allows; the rest continues in #99 and gridaco/models#21.

FX will always be behind new models and providers, so a user can now run any model on a provider FX speaks, and add a provider FX does not ship, without waiting for an FX release. FX's own providers use the same adapter contract users build on. The plan, its decisions (R1–R15) and the record of each milestone are in docs/wg/provider-contract.md. The new guide page Community providers teaches the whole job.

What users can do now

Job Needs
A model FX does not list, on GG, OpenRouter or fal a route: the model id, extends to copy a built-in route, price: {cap_usd: N} when its price is unknown
A parameter FX does not expose provider_options, keyed by provider, on a step or a route's contract
A provider that speaks the OpenAI Chat Completions API (keyless for a local server) a declaration under providers: with family: openai-compatible
Any other provider a declaration and an adapter in Python or TypeScript (adapter(...) from grida.fx / @grida/fx)

Commits, one per milestone

Commit Milestone
Ratify the AI provider contract and its plan M0: decisions, overview amendments (1, 4, 9) and decision 15, AR-20
Split the adapter contract out of the providers spec M1: spec/adapters.md; per-adapter references beside the code; check_spec (l) holds every §N reference to a real heading. No behaviour change
Let the engine check each request against its route and capability M2: contract, request and every-route checks move into the engine. No behaviour change
Fill each route's adapter identity, and let a route extend another M3: contract fill before the fingerprint, extends, route vectors; all 29 built-in fingerprints pinned and unchanged
Let a route without a known price give a per-call cap M4: price: {cap_usd: N}; a price with no amount is refused
Pass parameters FX does not expose through provider_options M5
Run any model on GG, OpenRouter and fal from a route alone M6: registry keyed by contract adapter; generic openrouter-image and fal-image adapters; fal video limits from the route
Check the built-in routes against Grida Models, and send transparency on OpenRouter M7: tools/make_models.py pins v1.20261013.6 (verified against the release's SHA256SUMS), tests/models.rs, CI
Let a project declare the providers it uses M8: providers: in fx.yaml; FX's own providers become built-in declarations
Serve a declared OpenAI-compatible provider's chat routes M9
Let a project write its own provider adapter in Python or TypeScript M10: adapter.load/send/submit/collect and fetch within protocol v1; SDK authoring APIs; conformance adapter-py, adapter-ts
Document community providers end to end, and mark AR-20 current M11: guide page 15, llms.txt, the skill
Bump the version to 0.7.0 25 places agree; nothing tagged or published

What changes for existing users

Each is in llms.txt's upgrade notes and spec/identity.md §7:

  • A project route on a provider and capability FX serves, whose contract leaves out adapter or adapter_behavior, gets them filled in: its fingerprint moves once (re-run, not resumable). No built-in fingerprint moves.
  • A project route on OpenRouter or fal images without an adapter now gets the generic adapter, not GPT Image's request; naming adapter: fx-openrouter-image-v1 / fx-fal-image-v1 keeps it.
  • price: {} (any price without an amount) is refused instead of meaning $0.
  • fal's and Tripo's adapters now check adapter_behavior, as every other adapter does.
  • A route contract's provider_options is now sent, under the same fingerprint.
  • A contract naming an adapter FX does not have is no_route / doctor no adapter.
  • OpenRouter's GPT Image routes declare alpha; GG's video route declares text_to_video.
  • The README no longer promises a run never crosses its ceiling: a reported cost above a call's hold is charged in full.

Verification

The whole AGENTS.md list, green on the branch with GRIDA_FX_NETWORK=off: 2,250 Rust tests in 74 binaries, 1,612 Python tests, 508 TypeScript tests, 66 conformance cases (also through grida fx via check_host), plus viewer, www, examples, showcase, check_llms, check_spec, make_models.py --check and check_versions.py. No provider was called and nothing was spent: every path is proven over replay fixtures, stand-ins, synthetic adapters and loopback fakes.

Follow-ups

Filed, grouped by theme: #91 to #105 here, and gridaco/models#21 for the Models gaps.

The plan for #69 and #79: decisions R1 to R15, the
amendments to the overview's decisions 1, 4 and 9, decision 15, and the
milestones this branch follows. GG and the grida fx umbrella leave the
overview's Later list, since both shipped. AGENT_READINESS gains AR-20,
proposed.
spec/adapters.md is the adapter contract, version 1: where adapters run,
the transport, outcomes, refusals before sending, costs, long jobs,
downloads and reasons, moved from spec/providers.md unchanged in substance.
spec/providers.md keeps keys and endpoints, gains the built-in adapters as a
normative table with each contract's adapter and behaviour in exact JSON,
and keeps the route table and renamed features. Each adapter's wire details
move beside its code, in crates/grida-fx-providers/src/<provider>/README.md.

Every reference follows, and check_spec gains (l): every '<spec file> §N'
reference names a heading that exists. No behaviour changes.
FX's adapters name their contract as an Identity. Around an adapter that
names one, the engine makes the contract check, then the adapter's own
judgement of its other contract members, then the request's fit to its
capability, inside the attempt before each send or submit; and it runs the
capability's every-route checks after the adapter's check of each answer.
Adapters keep a guard for direct callers, built on the same functions.

No sentence, order, event, record, wire byte or identity changes; stand-ins
are unchanged.
The engine fills a route contract's missing adapter and adapter_behavior
from the built-in adapter table, keyed by capability and provider, before
the fingerprint; no built-in fingerprint moves (a test pins all 29), and a
project route declared without a full contract recomputes once. extends
copies a route of the same capability and provider (the table's own earlier
entries, the catalog so far, or the built-in table, even under --routes),
merges contract members one level deep and is never hashed.

fal's and Tripo's adapters now check adapter_behavior like every other.
digest.py fills and resolves as the engine does, with new route vectors and
a route-extends conformance case. jpeg_output, webp_output and
hosted_url_reference_input leave the feature vocabulary.
price: {cap_usd: N} stands alone: each call on the route holds the cap and
is charged what the provider reports, else the whole cap. A price that
states no amount is refused instead of meaning $0. The plan notes each
capped route, the graph marks a capped instance capped: true, and the
viewer shows it as up to $N.

A reported cost above a call's hold is charged in full, so the docs no
longer promise that a run never crosses its ceiling: each call is reserved
against it before it is made.
Every paid capability takes an optional provider_options, keyed by
provider, and a route's contract.provider_options merges under the step's.
An adapter reads only its route's provider's member and sends it after its
own members, so with none no byte changes. Members an adapter owns, file
values and a non-object are refused before anything leaves; Codex and Grok
Build refuse their own. Each adapter's reference lists what it owns.

movie-sprite turns off Wan's prompt expansion through
provider_options.fal, the parameter fal's published schema names.
The registry keys adapters by capability, provider and the contract's
adapter; a route without one takes its provider's default for the
capability, and a contract naming an adapter FX does not have is no_route.
OpenRouter and fal images get generic default adapters, openrouter-image
and fal-image, which send the prompt, the references and the route's
provider_options, refuse what they cannot send faithfully, and convert a
non-PNG answer to PNG. The built-in GPT Image routes keep their adapters.

fal-queue takes its tiers and length from the route and sends the first
frame only when given; a route that declares text_to_video draws without
one. GG image edits take the route's picture limit, 16 on the built-in
route. No built-in fingerprint or previously sent byte moves.
… on OpenRouter

tools/make_models.py pins Grida Models v1.20261013.6: it downloads the
release's catalogue, verifies it against the release's SHA256SUMS and
manifest, validates it against its schema, and writes a projection of the
schema-defined facts FX's routes use to routes/models.json; --check
verifies the projection offline and runs in CI's spec job. tests/models.rs
holds every built-in route to it: wire and endpoint ids, GG's 3D endpoints,
features both ways (documented and honoured), picture and size limits, and
text prices as bounds over the published token rates, with every exception
named and failing once it is no longer needed.

OpenRouter's GPT Image routes now declare alpha and send background:
transparent under the same adapter_behavior. GG's video route declares
text_to_video, which Models documents and the adapter already served.
providers.md says GG's planning prices are FX's allowances.
fx.yaml's providers: names a provider's credential (variable, header,
scheme; optional, for a keyless local server), its base URL (an optional
variable and a default, https unless loopback), the hosts its results may
be downloaded from, and its family. A declaration may not take a built-in
or runtime name, a variable another declaration or a built-in claims, or a
reserved variable (GRIDA_FX_*, proxies, certificates, PATH, HOME).

FX's own providers become built-in declarations, so the key loader, the
.env reader, the redactor and the environment scrubs (Codex and Grok Build
children, the service, the browser opener) read one computed list, with
every built-in sentence, doctor line and credential byte unchanged. One
download-host helper serves fal and Tripo. doctor lists each declared
provider with its key state and base-URL host. A conformance case declares
acme and a keyless provider.
A declaration with family: openai-compatible gets FX's
openai-compatible-structured and openai-compatible-tool-loop adapters
(behaviour 1) for structured.generate and agent.turn, over
{base}/chat/completions on the shared chat module: only the OpenAI Chat
Completions reference's members, the default status table and never a
$0 refusal, no cost read (the hold or the cap is charged), token usage
recorded, and no credential for a keyless declaration.

The contract fill extends to a declared family's routes, keyed on the
planning project's declarations, never on a provider name: digest.py,
vectors and a conformance workflow follow, and no undeclared route moves.
llms.txt gains a recipe for a provider FX does not ship.
A declaration may name adapter: <path>#<attribute> in place of a family.
Planning loads the adapter on the planning host (adapter.load) for its
version and each capability's kind, fills the route contract with the
adapter's name and its version or source digest, and locks it in fx.lock.
A live run starts one adapter host per declaration on its first call, with
only that declaration's variables, and calls adapter.send, adapter.submit
and adapter.collect within protocol v1; the adapter may reach the network
through the engine's fetch, which attaches the credential only on the
provider and upload lanes to the declaration's base-URL host. A lost host is
a billed, unretried failure mid-send, Uncertain mid-submit and Unreachable
mid-collect; the engine keeps every check and the retry rule.

grida.fx and @grida/fx gain an adapter authoring API with outcome types,
concurrent calls and fetch. Conformance cases adapter-py and adapter-ts run
a synthetic acme adapter live with the network off: an answer, a refusal
and a long job.
Guide page 15, Community providers, is the one place a user learns to
define their own provider: choosing the path (a route for a new model on a
provider FX speaks, a declaration for an OpenAI-compatible server, a
declaration and an adapter for anything else), the declaration and its
refusals, routes and caps, a complete adapter in Python and TypeScript,
outcomes and what each costs, fetch, versions and fx.lock, checking it
offline before any paid call, identity and the cache, sharing it, and its
limits. Every example was run through the engine.

Page 13 keeps new models and provider_options and points to page 15.
llms.txt gains a recipe for a model FX does not list, the community
provider term and the refusal sentences this branch added; the skill
teaches agents both jobs. AGENT_READINESS AR-20 is current in source, with
its evidence, and the overview marks decision 15 implemented.
The workspace version and every manifest and lockfile that repeats it move
to 0.7.0 (check_versions.py: 25 places agree), with the grida stand-in's
pin and llms.txt's Version line and @grida/fx pins. Nothing is tagged or
published: a release follows RELEASING.md.
The follow-ups are filed as #91 to #105 and gridaco/models#21,
grouped by theme, and M11's boxes are ticked with the verification list's
results.
@vercel

vercel Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
fx Ignored Ignored Preview Oct 15, 2026 1:58am UTC

Request Review

The latest release, with the same schema byte for byte. It gives the
built-in fal video route a card, google/gemini-omni-1.1-flash, whose fal
endpoint is the route's id and whose parameters are fal-queue's constants,
so CARDS maps it and the test exception for its missing card goes. The
references that quote the release follow: fal's image endpoint counts, and
Ideogram 4.5's edit, which takes one image_url and so needs a project's own
adapter. Of the gaps filed as gridaco/models#21, only that card is filled.
@softmarshmallow
softmarshmallow merged commit 5bf4446 into main Oct 15, 2026
13 checks passed
softmarshmallow added a commit to gridaco/grida that referenced this pull request Oct 15, 2026
Pins @grida/fx to 0.7.0 (from 0.6.1), so `grida fx` runs FX 0.7.0
(gridaco/fx#106), and bumps grida to 0.5.4 for the release workflow.
Host contract unchanged.

- packages/grida-cli/package.json: @grida/fx 0.7.0, version 0.5.4
- crates/grida-cli/Cargo.toml, Cargo.lock: 0.5.4; notices regenerated
- apps/docs/sources.lock.json: FX guide at gridaco/fx@v0.7.0
- pnpm-lock.yaml, pnpm-workspace.yaml: minimumReleaseAgeExclude names the
  FX 0.7.0 packages
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AI-provider contract: bring your own provider or a newer model, on the layer FX's own providers use

1 participant