Skip to content

refactor(providers)!: data-only provider configuration and stream structs - #227

Closed
cunninghamcard-bit wants to merge 25 commits into
rfc-0036/wire-formatfrom
rfc-0036/provider-config
Closed

cunninghamcard-bit wants to merge 25 commits into
rfc-0036/wire-formatfrom
rfc-0036/provider-config

Conversation

@cunninghamcard-bit

Copy link
Copy Markdown
Contributor

Summary

Stacked on #225 (rfc-0036/wire-format). Public behaviour and AI SDK naming are unchanged; the internals become idiomatic Rust, following the shape rig uses: provider configuration is plain data, and every language-model doStream is a named stream struct.

Configuration is data, not closures

  • Removed: the transform_request_body setting of every provider and of PresetSettings (vendor body fields go through the provider's providerOptions namespace), and OpenAICompatibleProviderSettings::metadata_extractor with the MetadataExtractor / StreamMetadataExtractor traits (nothing implemented them).
  • ConvertUsage is an enum (OpenAICompatible, Alibaba, MoonshotAI); registry rows select it with profile.convert_usage instead of a vendor-name match in preset.rs. supported_urls is a SupportedUrls value. ChatDialect is folded into ChatSettings.
  • Endpoint, URL, region and header wiring are data resolved per request (EndpointSource, EndpointHeaders, SupportedUrlsSource, ProviderHeaders, OpenAIUrl, MessagesUrl, AnthropicEndpoint); Vertex Anthropic holds the Vertex resolver instead of a chain of boxed closures.
  • generate_id settings and the tool-call tracker take IdGenerator = fn() -> String; SigV4Fetch::with_clock takes a fn pointer.
  • Kept as runtime capabilities, not configuration: user-supplied credentials (Resolvable, e.g. rotating STS / Azure AD tokens) and the fetch transport.

Streams as structs

  • aimux-provider-utils gains Transformer / TransformStreamController / pipe_through, the Rust form of the WHATWG TransformStream the AI SDK models build doStream with (start / transform / flush; enqueue, enqueue_error, error, terminate).
  • OpenAI chat and Responses, OpenAI-compatible chat and completion, Groq, DeepSeek, Mistral, Anthropic, Bedrock, Cohere, Google, Vertex, xAI, Hugging Face and Open Responses streams are named structs whose fields are the former generator locals. Emitted parts are unchanged. After an Anthropic message_stop or a Google / Vertex / xAI / Hugging Face error event the rest of the body is now read and ignored before Finish (as the AI SDK's flush does) instead of dropping the connection early.
  • Left as generators on purpose: realtime transcription streams (WebSocket input), Bedrock's binary event-stream decoder, and Bedrock's abort-signal wrapper.

Not done, deliberately

  • Groq and DeepSeek keep their own model implementations: each package differs in message conversion, tool preparation, finish reasons, usage, options and errors, so folding them into the compatible model would turn every function into a vendor match.
  • is_deepseek_v4_model and the Groq browser-search model list stay as upstream's prefix / list rules rather than moving to the model catalogue, which would change results for future model ids.

Checks

On head a1e11487:

CHANGELOG, docs/api/rust.md, docs/provider-config-manual.md and docs/contributing/adding-a-provider.md are updated.

cunninghamcard-bit and others added 21 commits October 10, 2026 11:29
…a, not closures

The compatible chat config holds data where the AI SDK passes functions:

- `ConvertUsage` is an enum (OpenAICompatible / Alibaba / MoonshotAI); the
  Alibaba and Moonshot AI rows select it through `profile.convert_usage`
  in the registry instead of a name match in preset.rs.
- `supported_urls` is a `SupportedUrls` value.
- `ChatDialect` is folded into `ChatSettings`; the error structure, which
  only ever had the default value, is no longer a field.
- `MetadataExtractor` / `StreamMetadataExtractor` are removed: nothing
  implemented them.
- Provider headers are a `ProviderHeaders` value resolved per request, and
  a preset's lazy base URL is `BaseUrl::Preset { descriptor, params }`.

Behaviour is unchanged: provider-utils and providers tests pass as before.

Amp-Thread-ID: https://ampcode.com/threads/T-01a11e83-bf7b-75bb-900b-34851e6f12ee
Co-authored-by: Amp <amp@ampcode.com>
`generate_id` settings (anthropic, google, cohere, mistral) and the tool-call
tracker take `IdGenerator = fn() -> String` (the AI SDK's `IdGenerator`)
instead of boxed closures: no captured state, usable in const configs.

Amp-Thread-ID: https://ampcode.com/threads/T-01a11e83-bf7b-75bb-900b-34851e6f12ee
Co-authored-by: Amp <amp@ampcode.com>
Provider configuration is data; the request-body rewrite closure is gone
from every provider's settings, PresetSettings and the internal configs.
The OpenAI-compatible provider still passes unknown providerOptions fields
of its namespace through to the body. Callers that did not set a transform
see identical requests.

Amp-Thread-ID: https://ampcode.com/threads/T-01a11e83-bf7b-75bb-900b-34851e6f12ee
Co-authored-by: Amp <amp@ampcode.com>
…ock are plain data

- anthropic config: drop UrlFn/RequestUrlFn/BodyFn/ResolveFn. url() is
  base_url + path; MessagesUrl { Messages, RawPredict } selects the
  messages URL; prepare_body is a fn pointer; AnthropicEndpoint
  { Fixed, PerRequest(Arc<dyn ResolveEndpoint>) } replaces the late-binding
  closure (Vertex implements ResolveEndpoint over its Resolver).
- openai config: OpenAIUrl { Base, AzureDeployment, AzureSpeech } replaces
  UrlFn; azure UrlRules is pub(crate) and gains speech_url().
- SigV4Fetch clock is fn() -> DateTime<Utc>.

Amp-Thread-ID: https://ampcode.com/threads/T-01a11e83-bf7b-75bb-900b-34851e6f12ee
Co-authored-by: Amp <amp@ampcode.com>
EndpointConfig no longer holds EndpointFn / SupportedUrlsFn closures.
Where a request's base URL and headers come from is now described by
EndpointSource (Http { BaseUrl, EndpointHeaders } | Vertex | Bedrock |
AwsPolly), resolved by EndpointConfig::endpoint() per request; the URL
patterns a model fetches itself are SupportedUrlsSource (None | Fixed |
Google { base_url }).

- BaseUrl: Fixed | Env { var, description } (SearXNG instance URL).
- EndpointHeaders: Provider(ProviderHeaders) | Basic { login, password,
  user } (DataForSEO) | WithUserHeaders { provider, user } (Open
  Responses' resolvable user headers).
- Providers store ProviderHeaders instead of a HeadersFn; user-agent
  suffixes use ProviderHeaders::with_user_agent.
- vertex: GeminiEndpoint and ProjectLocationSource replace the endpoint
  and ProjectLocationFn closures.
- bedrock: BedrockEndpoint replaces the header/endpoint closures; the
  reranking model reads the region from the shared Auth (RegionFn gone).
- aws_polly: PollyEndpoint replaces signing_headers and the dynamic
  endpoint closure.

URLs, headers, error types and when the environment is read are
unchanged.
…rs data

OpenAIModelConfig and AnthropicModelConfig take the ProviderHeaders data
struct instead of a HeadersFn closure; the credential_headers /
provider_headers / into_headers_fn closure builders are gone. Vertex
Anthropic resolves its per-request endpoint through AnthropicEndpoint::Vertex
holding the Vertex resolver, so the ResolveEndpoint trait object is gone.

Amp-Thread-ID: https://ampcode.com/threads/T-01a11e83-bf7b-75bb-900b-34851e6f12ee
Co-authored-by: Amp <amp@ampcode.com>
The Rust form of the WHATWG TransformStream the AI SDK models build doStream
with: a Transformer value holds the state the TypeScript closures capture,
with start / transform / flush methods writing to a controller (enqueue,
error, terminate), and pipe_through drives it over an input stream.

Amp-Thread-ID: https://ampcode.com/threads/T-01a11e83-bf7b-75bb-900b-34851e6f12ee
Co-authored-by: Amp <amp@ampcode.com>
…ucts

GroqChatStream and MistralChatStream hold the doStream state and are
driven by pipe_through. DeepSeek keeps its generator: it yields a
recoverable Err (then continues) that the controller cannot express yet.

Amp-Thread-ID: https://ampcode.com/threads/T-01a11e83-bf7b-75bb-900b-34851e6f12ee
Co-authored-by: Amp <amp@ampcode.com>
…nsformer structs

OpenAICompatibleChatStream and OpenAIChatStream hold the doStream state
that lived in async_stream locals and are driven by pipe_through. The
emitted part sequence is unchanged.

Amp-Thread-ID: https://ampcode.com/threads/T-01a11e83-bf7b-75bb-900b-34851e6f12ee
Co-authored-by: Amp <amp@ampcode.com>
Convert the async_stream generators of the OpenAI, xAI, Hugging Face and
Open Responses doStream paths into OpenAIResponsesStream,
XaiResponsesStream, HuggingFaceResponsesStream and OpenResponsesStream
driven by pipe_through. Emitted part sequences are unchanged.

Amp-Thread-ID: https://ampcode.com/threads/T-01a11e83-bf7b-75bb-900b-34851e6f12ee
Co-authored-by: Amp <amp@ampcode.com>
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.

2 participants