From e55e7fae353e274e6b3f5a4e42a8c57ee4f5c1d8 Mon Sep 17 00:00:00 2001
From: Claude
Date: Mon, 12 Oct 2026 09:33:39 +0000
Subject: [PATCH] refactor!: providers are AI SDK V4 factories built on the AI
SDK request layer
One change, without a transition layer: the old provider interface and
its configs are removed in the same commit that adds the new ones.
Request layer (aimux-provider-utils):
- Injectable fetch, resolvable settings (values or functions evaluated per
request) and the AI SDK request helpers. API keys and settings load per
request, with LoadApiKey and LoadSetting errors.
Provider interface:
- Provider takes the AI SDK ProviderV4 shape, with a provider registry.
Vendor packages, each a factory as in its upstream package:
- openai-compatible, with the presets read from the registry table at run
time.
- The OpenAI family: openai, azure, codex, huggingface, xai.
- groq and deepseek.
- The Anthropic and Google families: anthropic, anthropic_aws, google,
vertex, bedrock, aws_polly, open_responses.
- The remaining vendors: mistral, cohere, voyage, elevenlabs, jev and 28
single-modality vendors.
Consumers:
- FFI, the bindings, the CLI, the web console and replay build providers
by name through create_provider. Recordings keep only the provider
identity. Retries are call options; provider configs reject the removed
keys, and a missing key or an unfilled preset parameter fails at call
time.
Co-Authored-By: Claude Opus 5.5 (1M context)
Claude-Session: https://claude.ai/code/session_01A3WxDBWBA4ituJjXsHaEAa
---
.github/workflows/ci.yml | 2 +
.gitignore | 3 +
CHANGELOG.md | 283 +-
CONTRIBUTING.md | 29 +-
README.md | 83 +-
ROADMAP.md | 14 +-
aimux-core/Cargo.toml | 1 +
aimux-core/src/composite.rs | 12 +-
aimux-core/src/decision_model.rs | 25 +-
aimux-core/src/decision_replay.rs | 13 +-
aimux-core/src/embedding_model.rs | 15 +-
aimux-core/src/error.rs | 49 +-
aimux-core/src/files_model.rs | 5 -
aimux-core/src/generate.rs | 27 +-
aimux-core/src/image_model.rs | 25 +-
aimux-core/src/image_model_middleware.rs | 137 +
aimux-core/src/language_model.rs | 43 +-
aimux-core/src/language_model_middleware.rs | 150 +
aimux-core/src/lib.rs | 13 +-
aimux-core/src/moa.rs | 28 +-
aimux-core/src/model_catalogue.rs | 5 +-
aimux-core/src/options.rs | 6 -
aimux-core/src/provider.rs | 215 +-
aimux-core/src/provider_registry.rs | 241 ++
aimux-core/src/recording.rs | 176 +-
aimux-core/src/replay.rs | 48 +-
aimux-core/src/reranking_model.rs | 15 +-
aimux-core/src/retry.rs | 79 +-
aimux-core/src/router.rs | 27 +-
aimux-core/src/search_model.rs | 15 +-
aimux-core/src/session.rs | 4 +-
aimux-core/src/shared.rs | 10 +
aimux-core/src/skills_model.rs | 47 +
aimux-core/src/speech_model.rs | 15 +-
aimux-core/src/trace/layer.rs | 36 +-
aimux-core/src/trace/mod.rs | 2 +-
aimux-core/src/trace/verdict.rs | 2 +-
aimux-core/src/transcription_model.rs | 15 +-
aimux-core/src/util.rs | 2 +-
aimux-core/src/video_model.rs | 34 +-
aimux-core/tests/error_value_golden_test.rs | 19 +-
aimux-ffi/aimux-error.h | 20 +-
aimux-ffi/aimux-ffi.h | 9 +-
aimux-ffi/src/lib.rs | 582 ++--
aimux-ffi/tests/common/mod.rs | 6 +-
aimux-ffi/tests/exports_smoke_test.rs | 22 +-
aimux-provider-utils/Cargo.toml | 4 +
aimux-provider-utils/src/api_key.rs | 58 +-
.../src/extract_response_headers.rs | 6 +-
aimux-provider-utils/src/fetch.rs | 429 +++
aimux-provider-utils/src/generate_id.rs | 17 +
.../src/handle_fetch_error.rs | 22 +-
aimux-provider-utils/src/headers.rs | 94 +-
aimux-provider-utils/src/http.rs | 454 ++-
aimux-provider-utils/src/lib.rs | 38 +-
aimux-provider-utils/src/logging.rs | 18 +-
aimux-provider-utils/src/media_type.rs | 210 ++
.../src/read_response_with_size_limit.rs | 8 +-
aimux-provider-utils/src/resolvable.rs | 157 +
aimux-provider-utils/src/response_handler.rs | 5 +-
aimux-provider-utils/src/retry.rs | 3 -
aimux-provider-utils/src/sigv4_fetch.rs | 712 +++++
aimux-provider-utils/src/ws.rs | 136 +-
.../tests/api_redirect_test.rs | 179 ++
.../tests/download_guard_test.rs | 1 +
.../tests/fetch_injection_test.rs | 108 +
aimux-provider-utils/tests/live_sigv4_test.rs | 101 +
aimux-provider-utils/tests/load_api_key.rs | 90 +-
aimux-provider-utils/tests/ws_proxy_test.rs | 2 +
aimux-providers/Cargo.toml | 7 +-
.../src/anthropic/cache_control.rs | 52 +-
aimux-providers/src/anthropic/config.rs | 217 ++
aimux-providers/src/anthropic/convert.rs | 536 ++--
aimux-providers/src/anthropic/files.rs | 82 +-
aimux-providers/src/anthropic/mod.rs | 650 ++--
aimux-providers/src/anthropic/model.rs | 237 +-
aimux-providers/src/anthropic/options.rs | 64 +
.../src/anthropic/prepare_tools.rs | 35 +-
aimux-providers/src/anthropic/skills.rs | 183 ++
aimux-providers/src/anthropic/stream.rs | 249 +-
aimux-providers/src/anthropic/types.rs | 35 +-
aimux-providers/src/anthropic/usage.rs | 126 +-
aimux-providers/src/anthropic_aws/mod.rs | 464 ++-
aimux-providers/src/anthropic_aws/model.rs | 248 +-
aimux-providers/src/assemblyai.rs | 556 ++--
aimux-providers/src/assemblyai/options.rs | 16 +
aimux-providers/src/aws_polly.rs | 420 ++-
aimux-providers/src/aws_polly/options.rs | 16 +
aimux-providers/src/azure/mod.rs | 523 ++-
aimux-providers/src/azure/model.rs | 618 ----
aimux-providers/src/azure/options.rs | 22 +
aimux-providers/src/azure/responses.rs | 384 ---
aimux-providers/src/bedrock/convert.rs | 46 +-
aimux-providers/src/bedrock/embedding.rs | 84 +-
aimux-providers/src/bedrock/image.rs | 77 +-
aimux-providers/src/bedrock/mod.rs | 708 +++--
aimux-providers/src/bedrock/model.rs | 175 +-
aimux-providers/src/bedrock/options.rs | 24 +
aimux-providers/src/bedrock/reranking.rs | 90 +-
aimux-providers/src/bedrock/sigv4.rs | 223 --
aimux-providers/src/bedrock_mantle.rs | 115 -
aimux-providers/src/black_forest_labs.rs | 427 +--
.../src/black_forest_labs/options.rs | 16 +
aimux-providers/src/cartesia.rs | 284 +-
aimux-providers/src/cartesia/options.rs | 16 +
aimux-providers/src/catalogue.rs | 64 +-
aimux-providers/src/codex.rs | 508 +--
aimux-providers/src/cohere/convert.rs | 29 +-
aimux-providers/src/cohere/embedding.rs | 49 +-
aimux-providers/src/cohere/mod.rs | 299 +-
aimux-providers/src/cohere/model.rs | 119 +-
aimux-providers/src/cohere/options.rs | 24 +
aimux-providers/src/cohere/reranking.rs | 49 +-
aimux-providers/src/cybertron.rs | 79 -
aimux-providers/src/dataforseo.rs | 256 +-
aimux-providers/src/decision_scoring.rs | 3 +-
aimux-providers/src/decision_support.rs | 50 +-
aimux-providers/src/deepgram.rs | 197 +-
aimux-providers/src/deepgram/options.rs | 16 +
aimux-providers/src/deepseek/convert.rs | 386 +++
aimux-providers/src/deepseek/finish_reason.rs | 15 +
aimux-providers/src/deepseek/is_v4_model.rs | 10 +
aimux-providers/src/deepseek/mod.rs | 193 ++
aimux-providers/src/deepseek/model.rs | 774 +++++
aimux-providers/src/deepseek/options.rs | 208 ++
aimux-providers/src/deepseek/prepare_tools.rs | 96 +
aimux-providers/src/deepseek/types.rs | 128 +
aimux-providers/src/deepseek/usage.rs | 39 +
aimux-providers/src/default_providers.rs | 403 +++
aimux-providers/src/docker_model_runner.rs | 79 -
aimux-providers/src/elevenlabs.rs | 340 +-
aimux-providers/src/elevenlabs/options.rs | 16 +
aimux-providers/src/exa_ai.rs | 180 +-
aimux-providers/src/fal.rs | 536 ++--
aimux-providers/src/fal/options.rs | 14 +
aimux-providers/src/firecrawl.rs | 184 +-
aimux-providers/src/gaudi.rs | 79 -
aimux-providers/src/gladia.rs | 461 +--
aimux-providers/src/gladia/options.rs | 16 +
aimux-providers/src/google/convert.rs | 198 +-
aimux-providers/src/google/embedding.rs | 78 +-
aimux-providers/src/google/files.rs | 230 +-
aimux-providers/src/google/image.rs | 79 +-
aimux-providers/src/google/mod.rs | 479 ++-
aimux-providers/src/google/model.rs | 163 +-
aimux-providers/src/google/options.rs | 100 +
aimux-providers/src/google/types.rs | 25 +-
aimux-providers/src/google/video.rs | 157 +-
aimux-providers/src/google_pse.rs | 255 +-
aimux-providers/src/google_pse/options.rs | 16 +
.../src/groq/browser_search_models.rs | 18 +
aimux-providers/src/groq/convert.rs | 152 +
aimux-providers/src/groq/error.rs | 23 +
aimux-providers/src/groq/finish_reason.rs | 14 +
aimux-providers/src/groq/mod.rs | 179 ++
aimux-providers/src/groq/model.rs | 592 ++++
aimux-providers/src/groq/options.rs | 100 +
aimux-providers/src/groq/prepare_tools.rs | 78 +
aimux-providers/src/groq/types.rs | 151 +
aimux-providers/src/groq/usage.rs | 48 +
aimux-providers/src/huggingface.rs | 272 +-
aimux-providers/src/huggingface/options.rs | 24 +
aimux-providers/src/huggingface/responses.rs | 238 +-
aimux-providers/src/hume.rs | 196 +-
aimux-providers/src/hume/options.rs | 16 +
aimux-providers/src/jev.rs | 27 +-
aimux-providers/src/jina_ai.rs | 206 +-
aimux-providers/src/jina_ai/options.rs | 16 +
aimux-providers/src/jlama.rs | 79 -
aimux-providers/src/klingai.rs | 213 +-
aimux-providers/src/klingai/options.rs | 16 +
aimux-providers/src/lib.rs | 425 ++-
aimux-providers/src/linkup.rs | 191 +-
aimux-providers/src/linkup/options.rs | 16 +
aimux-providers/src/litellm_proxy.rs | 79 -
aimux-providers/src/llamacpp.rs | 86 -
aimux-providers/src/llamafile.rs | 100 -
aimux-providers/src/lmnt.rs | 185 +-
aimux-providers/src/lmnt/options.rs | 16 +
aimux-providers/src/lmstudio.rs | 100 -
aimux-providers/src/local.rs | 79 -
aimux-providers/src/localai.rs | 86 -
aimux-providers/src/luma.rs | 378 +--
aimux-providers/src/luma/options.rs | 16 +
aimux-providers/src/mistral/convert.rs | 55 +-
aimux-providers/src/mistral/embedding.rs | 49 +-
aimux-providers/src/mistral/mod.rs | 287 +-
aimux-providers/src/mistral/model.rs | 202 +-
aimux-providers/src/mistral/options.rs | 24 +
aimux-providers/src/mistral/types.rs | 13 +-
aimux-providers/src/mistralrs.rs | 100 -
aimux-providers/src/mlx.rs | 79 -
aimux-providers/src/ollama.rs | 110 -
aimux-providers/src/omlx.rs | 79 -
aimux-providers/src/onnx.rs | 79 -
aimux-providers/src/oobabooba.rs | 79 -
aimux-providers/src/open_responses.rs | 403 +--
aimux-providers/src/openai/completion.rs | 5 +
aimux-providers/src/openai/config.rs | 168 +
aimux-providers/src/openai/convert.rs | 1035 +-----
aimux-providers/src/openai/decision.rs | 37 +-
aimux-providers/src/openai/embedding.rs | 62 +-
aimux-providers/src/openai/files.rs | 71 +-
aimux-providers/src/openai/image.rs | 86 +-
aimux-providers/src/openai/mod.rs | 509 ++-
aimux-providers/src/openai/model.rs | 290 +-
aimux-providers/src/openai/options.rs | 148 +
.../src/openai/responses/convert.rs | 409 ++-
aimux-providers/src/openai/responses/mod.rs | 102 +-
aimux-providers/src/openai/speech.rs | 65 +-
aimux-providers/src/openai/transcription.rs | 86 +-
aimux-providers/src/openai/types.rs | 18 +-
aimux-providers/src/openai_compatible/chat.rs | 883 ++++++
.../src/openai_compatible/completion.rs | 558 ++++
.../src/openai_compatible/config.rs | 235 ++
.../src/openai_compatible/convert.rs | 736 +++++
.../src/openai_compatible/embedding.rs | 142 +
.../src/openai_compatible/image.rs | 195 ++
aimux-providers/src/openai_compatible/mod.rs | 342 ++
.../src/openai_compatible/types.rs | 160 +
aimux-providers/src/openrouter.rs | 90 -
aimux-providers/src/openvino.rs | 79 -
aimux-providers/src/parallel_ai.rs | 185 +-
aimux-providers/src/preset.rs | 523 +++
aimux-providers/src/prodia.rs | 272 +-
aimux-providers/src/prodia/options.rs | 16 +
aimux-providers/src/provider.rs | 1147 ++-----
aimux-providers/src/provider_registry.json | 604 +++-
aimux-providers/src/recraft.rs | 272 +-
aimux-providers/src/recraft/options.rs | 16 +
aimux-providers/src/replay.rs | 524 +--
aimux-providers/src/replicate.rs | 386 +--
aimux-providers/src/replicate/options.rs | 16 +
aimux-providers/src/revai.rs | 329 +-
aimux-providers/src/revai/options.rs | 16 +
aimux-providers/src/runtime_decision.rs | 49 +-
aimux-providers/src/runwayml.rs | 224 +-
aimux-providers/src/searxng.rs | 210 +-
aimux-providers/src/serper.rs | 180 +-
aimux-providers/src/sglang.rs | 86 -
aimux-providers/src/shared/discovery.rs | 59 +
aimux-providers/src/shared/exchange.rs | 183 ++
aimux-providers/src/shared/mod.rs | 156 +
aimux-providers/src/shared/poll.rs | 373 +++
aimux-providers/src/stability.rs | 207 +-
aimux-providers/src/stability/options.rs | 16 +
aimux-providers/src/tavily.rs | 182 +-
aimux-providers/src/tinyfish.rs | 197 +-
aimux-providers/src/vertex/anthropic_model.rs | 723 +----
aimux-providers/src/vertex/embedding.rs | 83 +-
aimux-providers/src/vertex/image.rs | 81 +-
aimux-providers/src/vertex/mod.rs | 790 +++--
aimux-providers/src/vertex/model.rs | 150 +-
aimux-providers/src/vertex/transcription.rs | 111 +-
aimux-providers/src/vertex/video.rs | 212 +-
aimux-providers/src/vertex_ai_ai21_models.rs | 137 -
.../src/vertex_ai_anthropic_models.rs | 137 -
.../src/vertex_ai_deepseek_models.rs | 137 -
aimux-providers/src/vertex_ai_llama_models.rs | 137 -
.../src/vertex_ai_minimax_models.rs | 137 -
.../src/vertex_ai_mistral_models.rs | 137 -
.../src/vertex_ai_moonshot_models.rs | 137 -
.../src/vertex_ai_openai_models.rs | 137 -
aimux-providers/src/vertex_ai_qwen_models.rs | 137 -
aimux-providers/src/vertex_ai_zai_models.rs | 137 -
aimux-providers/src/vllm.rs | 86 -
aimux-providers/src/voyage/embedding.rs | 45 +-
aimux-providers/src/voyage/mod.rs | 185 +-
aimux-providers/src/voyage/options.rs | 16 +
aimux-providers/src/voyage/reranking.rs | 45 +-
aimux-providers/src/xai/convert.rs | 595 +---
aimux-providers/src/xai/mod.rs | 252 +-
aimux-providers/src/xai/model.rs | 598 ----
aimux-providers/src/xai/options.rs | 24 +
aimux-providers/src/xai/responses/convert.rs | 125 +-
aimux-providers/src/xai/responses/mod.rs | 84 +-
aimux-providers/src/xai/types.rs | 162 -
aimux-providers/src/xinference.rs | 79 -
aimux-providers/src/you_com.rs | 196 +-
aimux-providers/tests/alibaba_test.rs | 114 +-
.../tests/amazon_bedrock_image_test.rs | 81 +-
.../tests/anthropic_factory_test.rs | 537 ++++
aimux-providers/tests/anthropic_files_test.rs | 51 +-
.../tests/assemblyai_transcription_test.rs | 117 +-
aimux-providers/tests/aws_polly_test.rs | 49 +-
aimux-providers/tests/baseten_test.rs | 78 +-
aimux-providers/tests/bedrock_convert_test.rs | 2 +-
.../tests/bedrock_embedding_test.rs | 56 +-
.../tests/bedrock_stream_replay_test.rs | 14 +-
.../tests/black_forest_labs_image_test.rs | 65 +-
aimux-providers/tests/cartesia_speech_test.rs | 125 +-
.../tests/cartesia_transcription_test.rs | 56 +-
.../tests/cassette_exhaustive_test.rs | 18 +-
aimux-providers/tests/cassette_full_test.rs | 202 +-
.../tests/cassette_multimodal_test.rs | 58 +-
aimux-providers/tests/cerebras_test.rs | 78 +-
aimux-providers/tests/codex_test.rs | 113 +-
.../tests/cohere_embedding_test.rs | 68 +-
aimux-providers/tests/common/mock_fetch.rs | 217 ++
aimux-providers/tests/common/replay.rs | 9 +-
aimux-providers/tests/config_snapshot_test.rs | 208 --
aimux-providers/tests/conformance_test.rs | 340 +-
.../tests/data_loss_regression_test.rs | 113 +-
aimux-providers/tests/dataforseo_test.rs | 35 +-
.../tests/decision_recording_test.rs | 38 +-
.../tests/deepgram_transcription_test.rs | 56 +-
aimux-providers/tests/deepinfra_test.rs | 67 +-
aimux-providers/tests/deepseek_chat_test.rs | 2799 ++++++++++++++---
aimux-providers/tests/deepseek_test.rs | 50 +
.../tests/download_ssrf_guard_test.rs | 11 +-
aimux-providers/tests/e2e_test.rs | 147 +-
.../tests/elevenlabs_realtime_stream_test.rs | 29 +-
.../tests/elevenlabs_speech_test.rs | 42 +-
.../tests/elevenlabs_transcription_test.rs | 41 +-
aimux-providers/tests/exa_ai_test.rs | 31 +-
aimux-providers/tests/fal_image_test.rs | 69 +-
.../tests/fal_transcription_test.rs | 48 +-
aimux-providers/tests/fal_video_test.rs | 38 +-
aimux-providers/tests/firecrawl_test.rs | 31 +-
aimux-providers/tests/fireworks_test.rs | 86 +-
.../tests/fixtures/jev_systemone_live.jsonl | 4 +-
.../tests/fixtures/jev_systemone_live.md | 4 +-
.../tests/gladia_transcription_test.rs | 46 +-
.../tests/google_embedding_test.rs | 74 +-
aimux-providers/tests/google_factory_test.rs | 459 +++
aimux-providers/tests/google_files_test.rs | 207 +-
aimux-providers/tests/google_image_test.rs | 358 +--
aimux-providers/tests/google_pse_test.rs | 56 +-
.../tests/google_vertex_image_test.rs | 121 +-
aimux-providers/tests/google_video_test.rs | 323 +-
aimux-providers/tests/groq_test.rs | 805 ++++-
.../tests/huggingface_responses_test.rs | 890 +++++-
aimux-providers/tests/hume_speech_test.rs | 75 +-
aimux-providers/tests/jev_test.rs | 14 +-
aimux-providers/tests/jina_ai_test.rs | 34 +-
aimux-providers/tests/klingai_video_test.rs | 53 +-
aimux-providers/tests/linkup_test.rs | 43 +-
aimux-providers/tests/list_models_test.rs | 160 +-
aimux-providers/tests/lmnt_speech_test.rs | 77 +-
aimux-providers/tests/luma_image_test.rs | 69 +-
.../tests/mistral_embedding_test.rs | 80 +-
aimux-providers/tests/moonshotai_test.rs | 78 +-
aimux-providers/tests/open_responses_test.rs | 947 +++++-
.../tests/openai_compatible_factory_test.rs | 722 +++++
.../tests/openai_compatible_test.rs | 219 +-
aimux-providers/tests/openai_decision_test.rs | 76 +-
.../tests/openai_embedding_test.rs | 77 +-
aimux-providers/tests/openai_factory_test.rs | 779 +++++
aimux-providers/tests/openai_image_test.rs | 230 +-
.../tests/openai_output_round_trip_test.rs | 59 +-
aimux-providers/tests/openai_speech_test.rs | 145 +-
.../tests/openai_transcription_stream_test.rs | 771 -----
.../tests/openai_transcription_test.rs | 120 +-
aimux-providers/tests/openrouter_test.rs | 97 +-
aimux-providers/tests/parallel_ai_test.rs | 43 +-
aimux-providers/tests/perplexity_test.rs | 108 +-
aimux-providers/tests/presets_test.rs | 230 ++
aimux-providers/tests/prodia_image_test.rs | 53 +-
aimux-providers/tests/prodia_video_test.rs | 40 +-
aimux-providers/tests/provider_error_test.rs | 60 +-
aimux-providers/tests/reasoning_map_test.rs | 361 ---
aimux-providers/tests/recraft_test.rs | 31 +-
aimux-providers/tests/replicate_image_test.rs | 75 +-
aimux-providers/tests/replicate_video_test.rs | 40 +-
.../tests/revai_transcription_test.rs | 48 +-
.../tests/runtime_decision_test.rs | 103 +-
aimux-providers/tests/runwayml_test.rs | 51 +-
aimux-providers/tests/searxng_test.rs | 58 +-
aimux-providers/tests/serper_test.rs | 31 +-
.../tests/sse_terminal_error_test.rs | 11 +-
aimux-providers/tests/stability_test.rs | 100 +-
aimux-providers/tests/tavily_test.rs | 31 +-
.../tests/thin_wrapper_config_test.rs | 262 +-
aimux-providers/tests/tinyfish_test.rs | 34 +-
.../tests/vertex_transcription_test.rs | 78 +-
aimux-providers/tests/vertex_video_test.rs | 84 +-
.../tests/voyage_embedding_test.rs | 36 +-
.../tests/voyage_reranking_test.rs | 34 +-
aimux-providers/tests/you_com_test.rs | 34 +-
bindings/flutter/lib/aimux.dart | 24 +-
bindings/flutter/lib/errors.dart | 50 +-
bindings/flutter/lib/types.dart | 18 +-
bindings/flutter/lib/types.g.dart | 2 -
bindings/flutter/test/errors_test.dart | 13 +-
bindings/go/aimux.go | 24 +-
bindings/go/aimux_test.go | 49 +-
bindings/go/error.go | 38 +-
bindings/go/error_test.go | 15 +-
bindings/go/types.go | 13 +-
.../ai/arcships/aimux/AimuxException.java | 77 +-
.../main/java/ai/arcships/aimux/AimuxFFI.java | 5 +-
.../java/ai/arcships/aimux/AimuxResult.java | 7 +-
.../main/java/ai/arcships/aimux/Model.java | 6 +-
.../main/java/ai/arcships/aimux/Types.java | 117 +-
.../ai/arcships/aimux/AimuxExceptionTest.java | 24 +-
.../main/kotlin/ai/arcships/aimux/Errors.kt | 55 +-
.../main/kotlin/ai/arcships/aimux/Model.kt | 7 +-
.../kotlin/ai/arcships/aimux/Multimodal.kt | 2 +-
.../main/kotlin/ai/arcships/aimux/Types.kt | 16 +-
.../kotlin/ai/arcships/aimux/ErrorsTest.kt | 21 +-
bindings/node/__test__/body_overrides.test.ts | 254 --
.../node/__test__/cassette-exhaustive.test.ts | 4 +-
bindings/node/__test__/cassette.test.ts | 14 +-
bindings/node/__test__/e2e.test.ts | 18 +-
bindings/node/__test__/error.test.ts | 66 +-
bindings/node/__test__/index.test.ts | 7 +-
.../node/__test__/provider_config.test.ts | 248 ++
.../node/__test__/reasoning_replay.test.ts | 10 +-
bindings/node/__test__/session.test.ts | 6 +-
bindings/node/__test__/trace.test.ts | 15 +-
bindings/node/__test__/wrapper.test.ts | 20 +-
bindings/node/index.d.ts | 20 +-
bindings/node/src/error.rs | 27 +-
bindings/node/src/error.ts | 20 +
bindings/node/src/index.ts | 2 +
bindings/node/src/lib.rs | 453 ++-
bindings/node/src/multimodal.rs | 143 +-
bindings/node/src/types.ts | 3 +
bindings/node/src/types/AiMuxError.ts | 20 +-
bindings/node/src/types/CallOptions.ts | 6 -
.../node/src/types/GenerateTextOptions.ts | 4 -
bindings/node/src/types/InputRecord.ts | 12 +-
bindings/node/src/types/ModelSpec.ts | 3 +-
bindings/node/src/types/ProviderRecord.ts | 28 +-
bindings/node/src/types/Recording.ts | 3 +-
bindings/node/src/types/SkillFile.ts | 4 +
.../node/src/types/UploadSkillCallOptions.ts | 5 +
bindings/node/src/types/UploadSkillResult.ts | 5 +
bindings/python/python/aimux/__init__.py | 12 +-
bindings/python/python/aimux/wrapper.py | 3 -
bindings/python/src/error.rs | 37 +-
bindings/python/src/lib.rs | 222 +-
bindings/python/src/multimodal.rs | 127 +-
bindings/python/tests/test_aimux.py | 71 +-
bindings/python/tests/test_cassette.py | 14 +-
.../python/tests/test_cassette_exhaustive.py | 4 +-
bindings/python/tests/test_e2e.py | 18 +-
bindings/python/tests/test_error.py | 46 +
bindings/python/tests/test_session.py | 6 +-
bindings/python/tests/test_trace.py | 10 +-
bindings/python/tests/test_wrapper.py | 24 +-
bindings/swift/Sources/Aimux/Aimux.swift | 39 +-
bindings/swift/Sources/Aimux/Types.swift | 34 +-
.../swift/Tests/AimuxTests/AimuxTests.swift | 16 +
docs/API.md | 34 +-
docs/PROJECT-OVERVIEW.md | 66 +-
docs/ai-sdk-request-pipeline.md | 41 +-
docs/aisdk-architecture-alignment.md | 38 +-
docs/api/c.md | 24 +-
docs/api/decision.md | 16 +-
docs/api/flutter.md | 15 +-
docs/api/go.md | 37 +-
docs/api/java.md | 17 +-
docs/api/kotlin.md | 15 +-
docs/api/node.md | 53 +-
docs/api/providers.md | 695 ++--
docs/api/python.md | 42 +-
docs/api/reference.md | 47 +-
docs/api/rust.md | 147 +-
docs/api/swift.md | 13 +-
docs/codex-subscription-guide.md | 48 +-
docs/contributing/adding-a-provider.md | 104 +-
docs/error-model.md | 2 +-
docs/external-provider-config-research.md | 1 +
docs/provider-config-manual.md | 110 +-
docs/session-affinity-guide.md | 2 +-
fixtures/aisdk/VERSIONS.json | 9 +
fixtures/aisdk/anthropic/messages-basic.json | 108 +
.../aisdk/anthropic/messages-custom-name.json | 141 +
.../anthropic/messages-provider-options.json | 135 +
.../anthropic/messages-stream-basic.json | 166 +
.../anthropic/messages-system-tools.json | 166 +
fixtures/aisdk/google/embedding-basic.json | 94 +
fixtures/aisdk/google/generate-basic.json | 113 +
.../google/generate-provider-options.json | 138 +
.../aisdk/google/generate-stream-basic.json | 172 +
fixtures/aisdk/google/generate-tools.json | 169 +
.../aisdk/openai-compatible/chat-basic.json | 95 +
.../openai-compatible/chat-no-api-key.json | 97 +
...-provider-options-generic-key-unknown.json | 105 +
.../chat-provider-options-namespace.json | 110 +
.../chat-query-params-and-headers.json | 109 +
.../openai-compatible/chat-stream-basic.json | 157 +
.../openai-compatible/embedding-basic.json | 86 +
fixtures/aisdk/openai/chat-basic.json | 104 +
fixtures/aisdk/openai/chat-custom-name.json | 107 +
fixtures/aisdk/openai/chat-headers-merge.json | 119 +
.../aisdk/openai/chat-provider-options.json | 153 +
fixtures/aisdk/openai/chat-stream-basic.json | 160 +
.../openai/chat-system-and-settings.json | 130 +
fixtures/aisdk/openai/chat-tools.json | 158 +
fixtures/aisdk/openai/embedding-basic.json | 86 +
fixtures/aisdk/openai/empty-api-key.json | 109 +
fixtures/aisdk/openai/missing-api-key.json | 24 +
fixtures/aisdk/openai/responses-basic.json | 120 +
rfc/0037-decision-api.md | 4 +-
scripts/aisdk-fixtures/README.md | 61 +
scripts/aisdk-fixtures/cases/anthropic.mjs | 111 +
scripts/aisdk-fixtures/cases/google.mjs | 87 +
.../cases/openai-compatible.mjs | 116 +
scripts/aisdk-fixtures/cases/openai.mjs | 233 ++
scripts/aisdk-fixtures/common.mjs | 27 +
scripts/aisdk-fixtures/mock-fetch.mjs | 81 +
scripts/aisdk-fixtures/package-lock.json | 205 ++
scripts/aisdk-fixtures/package.json | 19 +
scripts/aisdk-fixtures/record.mjs | 252 ++
scripts/check_provider_boundaries.sh | 296 ++
scripts/gen_providers_doc.py | 36 +-
scripts/generate_thin_wrapper_cassettes.py | 2 +-
tools/aimux-cli/src/probe/provider.rs | 60 +-
tools/aimux-replay/src/main.rs | 81 +-
tools/aimux-web/examples/gen_demo_fixtures.rs | 10 +-
tools/aimux-web/examples/gen_fixture.rs | 10 +-
tools/aimux-web/src/api/cache_probe.rs | 4 +-
tools/aimux-web/src/api/mod.rs | 2 +
tools/aimux-web/src/api/providers.rs | 11 +-
tools/aimux-web/src/api/replay.rs | 60 +-
tools/aimux-web/src/api/traces.rs | 10 +-
tools/aimux-web/src/model_builder.rs | 75 +-
tools/aimux-web/src/wire.rs | 3 -
.../web/src/types/generated/WireOptions.ts | 2 +-
tools/aimux-web/web/src/views/Playground.vue | 6 -
522 files changed, 45246 insertions(+), 27096 deletions(-)
create mode 100644 aimux-core/src/image_model_middleware.rs
create mode 100644 aimux-core/src/language_model_middleware.rs
create mode 100644 aimux-core/src/provider_registry.rs
create mode 100644 aimux-core/src/skills_model.rs
create mode 100644 aimux-provider-utils/src/fetch.rs
create mode 100644 aimux-provider-utils/src/generate_id.rs
create mode 100644 aimux-provider-utils/src/media_type.rs
create mode 100644 aimux-provider-utils/src/resolvable.rs
delete mode 100644 aimux-provider-utils/src/retry.rs
create mode 100644 aimux-provider-utils/src/sigv4_fetch.rs
create mode 100644 aimux-provider-utils/tests/api_redirect_test.rs
create mode 100644 aimux-provider-utils/tests/fetch_injection_test.rs
create mode 100644 aimux-provider-utils/tests/live_sigv4_test.rs
create mode 100644 aimux-providers/src/anthropic/config.rs
create mode 100644 aimux-providers/src/anthropic/options.rs
create mode 100644 aimux-providers/src/anthropic/skills.rs
create mode 100644 aimux-providers/src/assemblyai/options.rs
create mode 100644 aimux-providers/src/aws_polly/options.rs
delete mode 100644 aimux-providers/src/azure/model.rs
create mode 100644 aimux-providers/src/azure/options.rs
delete mode 100644 aimux-providers/src/azure/responses.rs
create mode 100644 aimux-providers/src/bedrock/options.rs
delete mode 100644 aimux-providers/src/bedrock/sigv4.rs
delete mode 100644 aimux-providers/src/bedrock_mantle.rs
create mode 100644 aimux-providers/src/black_forest_labs/options.rs
create mode 100644 aimux-providers/src/cartesia/options.rs
create mode 100644 aimux-providers/src/cohere/options.rs
delete mode 100644 aimux-providers/src/cybertron.rs
create mode 100644 aimux-providers/src/deepgram/options.rs
create mode 100644 aimux-providers/src/deepseek/convert.rs
create mode 100644 aimux-providers/src/deepseek/finish_reason.rs
create mode 100644 aimux-providers/src/deepseek/is_v4_model.rs
create mode 100644 aimux-providers/src/deepseek/mod.rs
create mode 100644 aimux-providers/src/deepseek/model.rs
create mode 100644 aimux-providers/src/deepseek/options.rs
create mode 100644 aimux-providers/src/deepseek/prepare_tools.rs
create mode 100644 aimux-providers/src/deepseek/types.rs
create mode 100644 aimux-providers/src/deepseek/usage.rs
create mode 100644 aimux-providers/src/default_providers.rs
delete mode 100644 aimux-providers/src/docker_model_runner.rs
create mode 100644 aimux-providers/src/elevenlabs/options.rs
create mode 100644 aimux-providers/src/fal/options.rs
delete mode 100644 aimux-providers/src/gaudi.rs
create mode 100644 aimux-providers/src/gladia/options.rs
create mode 100644 aimux-providers/src/google/options.rs
create mode 100644 aimux-providers/src/google_pse/options.rs
create mode 100644 aimux-providers/src/groq/browser_search_models.rs
create mode 100644 aimux-providers/src/groq/convert.rs
create mode 100644 aimux-providers/src/groq/error.rs
create mode 100644 aimux-providers/src/groq/finish_reason.rs
create mode 100644 aimux-providers/src/groq/mod.rs
create mode 100644 aimux-providers/src/groq/model.rs
create mode 100644 aimux-providers/src/groq/options.rs
create mode 100644 aimux-providers/src/groq/prepare_tools.rs
create mode 100644 aimux-providers/src/groq/types.rs
create mode 100644 aimux-providers/src/groq/usage.rs
create mode 100644 aimux-providers/src/huggingface/options.rs
create mode 100644 aimux-providers/src/hume/options.rs
create mode 100644 aimux-providers/src/jina_ai/options.rs
delete mode 100644 aimux-providers/src/jlama.rs
create mode 100644 aimux-providers/src/klingai/options.rs
create mode 100644 aimux-providers/src/linkup/options.rs
delete mode 100644 aimux-providers/src/litellm_proxy.rs
delete mode 100644 aimux-providers/src/llamacpp.rs
delete mode 100644 aimux-providers/src/llamafile.rs
create mode 100644 aimux-providers/src/lmnt/options.rs
delete mode 100644 aimux-providers/src/lmstudio.rs
delete mode 100644 aimux-providers/src/local.rs
delete mode 100644 aimux-providers/src/localai.rs
create mode 100644 aimux-providers/src/luma/options.rs
create mode 100644 aimux-providers/src/mistral/options.rs
delete mode 100644 aimux-providers/src/mistralrs.rs
delete mode 100644 aimux-providers/src/mlx.rs
delete mode 100644 aimux-providers/src/ollama.rs
delete mode 100644 aimux-providers/src/omlx.rs
delete mode 100644 aimux-providers/src/onnx.rs
delete mode 100644 aimux-providers/src/oobabooba.rs
create mode 100644 aimux-providers/src/openai/completion.rs
create mode 100644 aimux-providers/src/openai/config.rs
create mode 100644 aimux-providers/src/openai/options.rs
create mode 100644 aimux-providers/src/openai_compatible/chat.rs
create mode 100644 aimux-providers/src/openai_compatible/completion.rs
create mode 100644 aimux-providers/src/openai_compatible/config.rs
create mode 100644 aimux-providers/src/openai_compatible/convert.rs
create mode 100644 aimux-providers/src/openai_compatible/embedding.rs
create mode 100644 aimux-providers/src/openai_compatible/image.rs
create mode 100644 aimux-providers/src/openai_compatible/mod.rs
create mode 100644 aimux-providers/src/openai_compatible/types.rs
delete mode 100644 aimux-providers/src/openrouter.rs
delete mode 100644 aimux-providers/src/openvino.rs
create mode 100644 aimux-providers/src/preset.rs
create mode 100644 aimux-providers/src/prodia/options.rs
create mode 100644 aimux-providers/src/recraft/options.rs
create mode 100644 aimux-providers/src/replicate/options.rs
create mode 100644 aimux-providers/src/revai/options.rs
delete mode 100644 aimux-providers/src/sglang.rs
create mode 100644 aimux-providers/src/shared/discovery.rs
create mode 100644 aimux-providers/src/shared/exchange.rs
create mode 100644 aimux-providers/src/shared/mod.rs
create mode 100644 aimux-providers/src/shared/poll.rs
create mode 100644 aimux-providers/src/stability/options.rs
delete mode 100644 aimux-providers/src/vertex_ai_ai21_models.rs
delete mode 100644 aimux-providers/src/vertex_ai_anthropic_models.rs
delete mode 100644 aimux-providers/src/vertex_ai_deepseek_models.rs
delete mode 100644 aimux-providers/src/vertex_ai_llama_models.rs
delete mode 100644 aimux-providers/src/vertex_ai_minimax_models.rs
delete mode 100644 aimux-providers/src/vertex_ai_mistral_models.rs
delete mode 100644 aimux-providers/src/vertex_ai_moonshot_models.rs
delete mode 100644 aimux-providers/src/vertex_ai_openai_models.rs
delete mode 100644 aimux-providers/src/vertex_ai_qwen_models.rs
delete mode 100644 aimux-providers/src/vertex_ai_zai_models.rs
delete mode 100644 aimux-providers/src/vllm.rs
create mode 100644 aimux-providers/src/voyage/options.rs
delete mode 100644 aimux-providers/src/xai/model.rs
create mode 100644 aimux-providers/src/xai/options.rs
delete mode 100644 aimux-providers/src/xai/types.rs
delete mode 100644 aimux-providers/src/xinference.rs
create mode 100644 aimux-providers/tests/anthropic_factory_test.rs
create mode 100644 aimux-providers/tests/common/mock_fetch.rs
delete mode 100644 aimux-providers/tests/config_snapshot_test.rs
create mode 100644 aimux-providers/tests/deepseek_test.rs
create mode 100644 aimux-providers/tests/google_factory_test.rs
create mode 100644 aimux-providers/tests/openai_compatible_factory_test.rs
create mode 100644 aimux-providers/tests/openai_factory_test.rs
delete mode 100644 aimux-providers/tests/openai_transcription_stream_test.rs
create mode 100644 aimux-providers/tests/presets_test.rs
delete mode 100644 aimux-providers/tests/reasoning_map_test.rs
delete mode 100644 bindings/node/__test__/body_overrides.test.ts
create mode 100644 bindings/node/__test__/provider_config.test.ts
create mode 100644 bindings/node/src/types/SkillFile.ts
create mode 100644 bindings/node/src/types/UploadSkillCallOptions.ts
create mode 100644 bindings/node/src/types/UploadSkillResult.ts
create mode 100644 fixtures/aisdk/VERSIONS.json
create mode 100644 fixtures/aisdk/anthropic/messages-basic.json
create mode 100644 fixtures/aisdk/anthropic/messages-custom-name.json
create mode 100644 fixtures/aisdk/anthropic/messages-provider-options.json
create mode 100644 fixtures/aisdk/anthropic/messages-stream-basic.json
create mode 100644 fixtures/aisdk/anthropic/messages-system-tools.json
create mode 100644 fixtures/aisdk/google/embedding-basic.json
create mode 100644 fixtures/aisdk/google/generate-basic.json
create mode 100644 fixtures/aisdk/google/generate-provider-options.json
create mode 100644 fixtures/aisdk/google/generate-stream-basic.json
create mode 100644 fixtures/aisdk/google/generate-tools.json
create mode 100644 fixtures/aisdk/openai-compatible/chat-basic.json
create mode 100644 fixtures/aisdk/openai-compatible/chat-no-api-key.json
create mode 100644 fixtures/aisdk/openai-compatible/chat-provider-options-generic-key-unknown.json
create mode 100644 fixtures/aisdk/openai-compatible/chat-provider-options-namespace.json
create mode 100644 fixtures/aisdk/openai-compatible/chat-query-params-and-headers.json
create mode 100644 fixtures/aisdk/openai-compatible/chat-stream-basic.json
create mode 100644 fixtures/aisdk/openai-compatible/embedding-basic.json
create mode 100644 fixtures/aisdk/openai/chat-basic.json
create mode 100644 fixtures/aisdk/openai/chat-custom-name.json
create mode 100644 fixtures/aisdk/openai/chat-headers-merge.json
create mode 100644 fixtures/aisdk/openai/chat-provider-options.json
create mode 100644 fixtures/aisdk/openai/chat-stream-basic.json
create mode 100644 fixtures/aisdk/openai/chat-system-and-settings.json
create mode 100644 fixtures/aisdk/openai/chat-tools.json
create mode 100644 fixtures/aisdk/openai/embedding-basic.json
create mode 100644 fixtures/aisdk/openai/empty-api-key.json
create mode 100644 fixtures/aisdk/openai/missing-api-key.json
create mode 100644 fixtures/aisdk/openai/responses-basic.json
create mode 100644 scripts/aisdk-fixtures/README.md
create mode 100644 scripts/aisdk-fixtures/cases/anthropic.mjs
create mode 100644 scripts/aisdk-fixtures/cases/google.mjs
create mode 100644 scripts/aisdk-fixtures/cases/openai-compatible.mjs
create mode 100644 scripts/aisdk-fixtures/cases/openai.mjs
create mode 100644 scripts/aisdk-fixtures/common.mjs
create mode 100644 scripts/aisdk-fixtures/mock-fetch.mjs
create mode 100644 scripts/aisdk-fixtures/package-lock.json
create mode 100644 scripts/aisdk-fixtures/package.json
create mode 100644 scripts/aisdk-fixtures/record.mjs
create mode 100755 scripts/check_provider_boundaries.sh
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b093ff4d0..d6e639a44 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -403,6 +403,8 @@ jobs:
- uses: actions/checkout@v4
- name: Providers doc generator drift (docs/api/providers.md must match generator output)
run: python3 scripts/gen_providers_doc.py --check
+ - name: Provider boundaries (no call-level reads or provider-name branches in providers, removed names stay removed)
+ run: bash scripts/check_provider_boundaries.sh
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: ts-rs export drift (checked-in types must match generator output)
diff --git a/.gitignore b/.gitignore
index 6e6fabbd4..6567f9180 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,3 +53,6 @@ quirks-corpus/
# Generated audit artifacts — keep conclusions in Markdown, not raw output.
/docs/quality-audit/**/lcov.info
/docs/quality-audit/**/*.log
+
+# AI SDK protocol fixture recorder (scripts/aisdk-fixtures) — npm install output
+scripts/aisdk-fixtures/node_modules/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 746dfdf78..d185890db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,14 +9,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Breaking
+- Middleware wraps generation and streaming with callbacks for both operations,
+ parameter transforms and provider/model/supported-URL overrides. Explicit
+ model and provider ids take precedence over middleware overrides.
+ `NoSuchProvider` carries `provider_id`, `model_id`, `model_type` and
+ `available_providers`. Node and Python OpenAI constructors use Responses
+ by default.
+
+- Public surface (`aimux-core`, `aimux-provider-utils`): removed
+ `recording::init_recording_from_env`; made private the retry preparation
+ helpers and default constants, `composite::{add_usage,
+ build_aggregator_prompt, extract_text}`, `recording::new_call_id`,
+ `util::rfc3339_now`, two session helpers, `fetch_error_to_ai_mux_error`,
+ `extract_response_header_pairs`, the SigV4 signing internals, the logging
+ internals, `DEFAULT_MAX_JSON_RESPONSE_SIZE` and `TungsteniteConnector`.
+
- Provider results carry request and response information as the AI SDK does
(`aimux-core`). `GenerateResult` has `request: Option` and
`response: Option`; `ResponseInfo` gains `id` and holds the
timestamp, model id, headers and body. `StreamResult` has `request` and
`response: Option`. The flat `request_body` and
`response_headers` fields are removed and `response` is optional; the call
- layer fills a missing id, timestamp and model id. The user-facing results and
- their JSON are unchanged.
+ layer fills a missing id, timestamp and model id. `GenerateTextResult`
+ exposes `request: RequestInfo` and `response: ResponseInfo`, including
+ headers and body.
- The prompt a provider receives is modelled by role (`aimux-core`), as the AI
SDK's `LanguageModelV4Message`. `LanguageModelPrompt` is
@@ -48,11 +64,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(`aimux-core`). A provider emits `tool::RawToolCall` (`input` is the raw
argument text) in `GenerateContent::ToolCall(..)` and
`StreamPart::ToolCall(..)`, and `tool::ToolResult` in the `ToolResult`
- variants; these variants are newtype variants now. `StreamPart` is generic
- over the tool-call type: a provider's `do_stream` yields `StreamPart`
- (= `StreamPart`), `stream_text` and everything after it yield
- `TextStreamPart` (= `StreamPart`, parsed input with `invalid` /
- `error`). `RawToolCall` moved from `parse_tool_call` to `tool`. Wire format:
+ variants; these variants are newtype variants now. A provider's `do_stream`
+ yields `StreamPart` with raw tool-call input. `stream_text` and everything
+ after it yield `TextStreamPart` with parsed input and `invalid` / `error`.
+ Call-layer streams consume provider response-metadata events internally;
+ `TextStreamPart` has no `ResponseMetadata` variant. `RawToolCall` moved from `parse_tool_call` to `tool`. Wire format:
enum variants keep their JSON shape; a provider-layer `StreamPart::ToolCall`
has a string `input` and no `invalid` / `error`; a
`GenerateContent::ToolCall` with a non-string `input` no longer
@@ -74,8 +90,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Removed the generated `ProviderName` type in every binding (Rust enum, TS
const object, Go/Java/Kotlin consts, Swift enum, Dart consts, Python
`Literal`) and `scripts/gen_provider_names.py`. Provider names are plain
- strings: `provider("groq", ...)`. Built-in and overlay-registered names now
- share one string path. The provider list lives in
+ strings: Rust uses `create_provider("groq", PresetSettings)`; bindings retain
+ `provider("groq", ...)`, including overlay-registered names. The provider list lives in
[docs/api/providers.md](docs/api/providers.md); Rust also gains
`provider_names()`.
@@ -141,6 +157,255 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
name ends the stream with `InvalidResponseData` (previously it started a
call with an empty name). `DeltaToolCall.index` is now `Option`.
+**Rust (provider factory, RFC-0036)**
+
+- The native Decision/Jev extension from master is retained. Its typed
+ `JevConfig` / `JevProvider::new` entry point and `Provider::decision_model`
+ remain available. Decision replay now takes the caller's registry;
+ recordings contain provider identity only and keep decision capabilities
+ separately under input. Decision retries use the core call-level policy.
+
+- `OpenAIProvider::language_model(id)` (and so `create_provider("openai", ..)`
+ and the registry id `openai:`) returns the **Responses** model, as
+ `@ai-sdk/openai` does. It returned the Chat Completions model. Call
+ `.chat(id)` for Chat Completions, for example against an endpoint that has
+ no `/responses` route.
+ The bindings' `openai(...)` constructors and the C ABI's `aimux_openai_new*`
+ are unchanged: they still create the Chat Completions model.
+
+- Removed crate-root exports `provider()`, `provider_handle`,
+ `provider_from_env`, `provider_discovery`, `provider_registry_entry`,
+ `ProviderOptions`, `ProviderProfile`, `register_provider` and
+ `load_providers_from_json`. Binding compatibility lives in
+ `aimux_providers::provider`; binding APIs are unchanged. Rust by-name
+ creation is `create_provider(name, PresetSettings)`; `provider_names()`
+ lists names, and `default_providers()` supplies the map for
+ `aimux_core::provider_registry::create_provider_registry(providers, options)`
+ with `provider:model` ids by default.
+
+- Redirects are handled in the request helper layer, not in the transport.
+ An ordinary API call follows every redirect, as the AI SDK's `postToApi`
+ does under fetch's default `redirect: "follow"`. A hop that leaves the
+ request's origin keeps only `User-Agent`, the rule the AI SDK's
+ `fetchWithValidatedRedirects` applies on the server, so no credential
+ header (`authorization`, `x-api-key`, `x-goog-api-key`, `api-key`, ...)
+ reaches another origin, and dropped headers stay dropped if a later hop
+ returns. `FetchRequest.credentials` (`RequestCredentials::Include` /
+ `Omit`, after the fetch `credentials` member) is `Omit` on such a hop, and
+ `SigV4Fetch` then sends it unsigned, without `x-amz-security-token`. Every
+ followed hop goes through the request's transport again, so SigV4 signs
+ the URL it sends. A `Fetch` never follows a redirect itself:
+ `RedirectPolicy`, `FetchRequest.redirect` and `PinnedFetch::unpinned` are
+ removed. Validated downloads keep their hop-by-hop guard.
+- `Provider` follows the AI SDK's `ProviderV4`: `language_model`,
+ `embedding_model` and `image_model` are required and return
+ `NoSuchModel { model_id, model_type }` when a vendor has no such modality;
+ `transcription_model`, `speech_model`, `reranking_model` and `files` are
+ optional (`None` = not offered); video and search stay aimux extensions.
+ AI SDK model constructors return an `Arc`. Removed: `Provider::name()`,
+ `specification_version()` (from `Provider` and the nine model traits),
+ `LanguageModel::config_snapshot()` and `list_models` on `Provider`.
+ Discovery is the separate `ProviderDiscovery` trait, accessed through
+ `Provider::discovery()`; `supported_urls()` is added to `LanguageModel`.
+- AI SDK-aligned packages remove `XxxConfig`, config builders, `from_env()`,
+ `with_*` methods and `XxxProvider::new(...)`. Each package
+ has package-specific `XxxProviderSettings`, `create_xxx(settings)`
+ (validating package-specific settings, including base URLs, conflicting
+ credentials and template parameters) and a default instance `xxx()` where
+ the package provides one.
+ Models read a crate-private per-model config; there are no getters and no
+ snapshot. `Fetch` (HTTP) and package-specific WebSocket settings are
+ transport injection points. OpenAI, Anthropic and Google use fixed string
+ credentials and header maps, without a body-transform setting. The
+ OpenAI-compatible package exposes its upstream `transform_request_body`,
+ `supported_urls` and `convert_usage` hooks. Cohere and Mistral expose
+ `generate_id` settings. Package settings omit `name` and body transforms
+ when the upstream package does not offer them. Every request carries
+ `ai-sdk-/` in its user-agent.
+- For packages using the API-key loader, environment fallbacks are evaluated
+ on every request: `api_key: None` reads the package's environment variable, `Some("")` is
+ sent verbatim and never falls back to the environment, and a missing key
+ fails the call (not the factory) with `AiMuxError::LoadApiKey { env_var,
+ description }`. A missing required setting (AWS region, Azure resource name,
+ Vertex project, …) fails the call with `AiMuxError::LoadSetting { env_var,
+ name }`. Credentials and custom headers otherwise follow each package's
+ settings; the generic compatible factory sends no auth for a missing or empty
+ key. Headers merge case-insensitively in package-specific order; custom and
+ call headers can override credential headers.
+- Retry is a call-level concern only. `RetryConfig`, `retry_config()`,
+ `with_retry_config`, the `max_retries` fields on every provider config and
+ `ProviderRecord.max_retries` are deleted; the nine core operations call
+ `prepare_retries(max_retries, abort)` with the constant defaults 2 retries,
+ 2000 ms initial delay, factor 2 (jitter, `Retry-After` and the error history
+ are unchanged). Providers no longer retry anything themselves: `list_models`
+ and files uploads are single exchanges, and job-creating requests are sent
+ exactly once.
+- `body_overrides` is removed everywhere (provider configs, builders,
+ `CallOptions`, `ProviderOptions`, `config_json`, binding `ProviderConfig`).
+ OpenAI-compatible provider-level body rewrites use its upstream
+ `transform_request_body` closure on the finished JSON body. `ProviderOptions`,
+ the C ABI `config_json` and the Node `ProviderConfig` that carry `max_retries` or
+ `body_overrides` now fail with `InvalidArgument` instead of ignoring them
+ (`reject_removed_provider_options` for other shapes).
+- Recording: `RECORDING_SCHEMA = 3`. `ProviderRecord` holds identity only
+ (`provider_id`, `provider`, `model_id`); base URL, key source, profile,
+ options and retry settings are no longer recorded and schema-2 files are
+ rejected on read. `rebuild_provider(record, registry)` resolves
+ `provider_id` through the caller-supplied registry, retaining its settings.
+ If the default model method differs from the recorded one, pass the model
+ to `replay_with_model`. Recorded provider strings change with the next item.
+- `model.provider()` follows the package's fixed identity or, where offered,
+ its `settings.name` override. Defaults: `openai.chat` /
+ `openai.responses` / `openai.embedding` / `openai.image` / `openai.speech` /
+ `openai.transcription` / `openai.files`; `anthropic.messages` (a custom name
+ is used verbatim; `{name}.files` derives from it), `anthropic-aws`,
+ `googleVertex.anthropic.messages`; `google.generative-ai` (+ `.video`,
+ `.files`), `google.vertex` (+ `.video`, `.transcription`),
+ `amazon-bedrock`; `azure.chat` / `.responses` / `.embeddings` / `.image` /
+ `.transcription` / `.speech`; `xai.responses`, `huggingface.responses`,
+ `mistral.chat` / `.embedding`, `cohere.chat` / `.textEmbedding` /
+ `.reranking`, `codex.responses`, `{name}.responses` (open_responses),
+ `voyage.embedding` / `.reranking`, `elevenlabs.speech` / `.transcription`,
+ `groq.chat`, `deepseek.chat`; compat and preset providers
+ `{name}.chat` / `.embedding` / `.image`; single-modality packages
+ `luma.image`, `deepgram.transcription`, `tavily.search`, `amazon-polly.speech`,
+ `blackForestLabs.image`, `cartesia.transcription`, … . Consumers that
+ compared `provider` to `"openai"` must match the prefix (the CLI and web
+ probes filter by `model.provider()`).
+- providerOptions namespaces are canonical only: `googleVertex` and
+ `amazonBedrock` (the legacy `vertex` / `bedrock` keys are neither read nor
+ written; Bedrock still reads `anthropic` for its Anthropic models). The
+ Anthropic package reads `anthropic` merged with the first segment of a
+ custom `name` (custom wins) and writes metadata under the custom key;
+ compat providers no longer read the `openai` key; `providerOptions.deepseek`
+ is honoured (`reasoningEffort`, `thinking`).
+- Presets: `provider_registry.json` has 282 rows; Groq and DeepSeek are
+ vendor packages instead of preset rows. Former thin wrapper types are
+ deleted. The JSON is embedded and parsed once into a runtime descriptor
+ table in `preset.rs`; there is no `family` field, generated presets source
+ or per-name Rust function. Create presets or vendor packages by name via
+ `create_provider(name, PresetSettings)`.
+ Rows carry `auth: api_key | none` (`none` sends no
+ `Authorization` unless the row's optional key variable, such as
+ `VLLM_API_KEY`, is set; `PLACEHOLDER_API_KEY` is gone), `base_url_env` and
+ template `params` (env, default, derived host maps). Only declared
+ parameters are accepted, host parameters reject `/ @ : ?`, and an
+ unexpanded placeholder is an error. Unknown names are `NoSuchProvider`;
+ nothing falls back to OpenAI. `litellm_proxy` reads `LITELLM_PROXY_BASE_URL`
+ (the old wrapper read its API-key variable as a URL). Registry validation
+ runs in the table test, and
+ `scripts/check_provider_boundaries.sh` runs in CI.
+- Decisions (#226) come from the providers: `OpenAIProvider::decision` and
+ `Provider::decision_model` on OpenAI, and `decision_model` on the official
+ runtime presets (ollama, llamacpp, localai, laya, vllm, sglang,
+ cloudflare). `laya` is a registry row. Decision recordings store the
+ provider identity only, and `rebuild_decision_provider` takes the
+ caller's `ProviderRegistry` instead of an API key.
+- Groq and DeepSeek are standalone packages (`create_groq`, `create_deepseek`)
+ with their own chat models; the native OpenAI package no longer knows any
+ other vendor. Compat providers expose chat, embedding and image only (the
+ upstream set); the native OpenAI package warns on and drops `top_k`;
+ `OpenAICompatProfile` and `with_profile` are deleted in favour of dialect
+ hooks.
+- Anthropic: the base URL includes `/v1` (endpoint `{base}/messages`; only the
+ bare `https://api.anthropic.com` is rewritten); `ANTHROPIC_BASE_URL` and
+ `OPENAI_BASE_URL` supply the base URL when the corresponding setting is
+ absent; `stream` is omitted on non-streaming calls; `providerOptions.anthropic.metadata.userId` maps to `metadata.user_id`; `anthropic-beta` is
+ sent on every host. Result-level `providerMetadata` is now populated
+ (`usage`, `stopSequence`, `iterations`, `container`, `contextManagement`,
+ under `anthropic` and the custom name), `usage.raw` is the provider's own
+ usage object, and `usage.iterations` now feeds the token totals. Vertex-
+ Anthropic and `anthropic_aws` run on the Anthropic core.
+- Google: request bodies follow the AI SDK (`generationConfig` is always
+ sent, function tools use `parametersJsonSchema`, `providerOptions.google`
+ maps `thinkingConfig`, `responseModalities`, `safetySettings`,
+ `cachedContent`, …; response `modelId` comes from `modelVersion`).
+ Vertex: Gemini uses `v1beta1`, Anthropic-on-Vertex `v1`; a tuned model in
+ express mode now fails at request time instead of at construction.
+ Bedrock signs the final request bytes through `SigV4Fetch`;
+ `list_models` now calls the control-plane host
+ `bedrock.{region}.amazonaws.com` (it used `….api.amazonaws.com`).
+- Azure defaults to `api_version = "v1"` with `/v1{path}`; a dated
+ `api_version` selects the deployment URL form. xAI, Hugging Face and Azure
+ default to the Responses API. **xAI and Hugging Face no longer have a Chat
+ Completions model**: the AI SDK packages serve Responses only, so `XaiModel`
+ and the Hugging Face chat model are removed (no `chat` / `chat_completions`
+ method). To keep calling those endpoints, use
+ `create_openai_compatible` with `https://api.x.ai/v1` or
+ `https://router.huggingface.co/v1`; the xAI-specific chat response
+ handling (citations, search parameters, 200-status errors) is not available
+ on that path. The xAI Responses model now sends `top_k` and warns for
+ `frequencyPenalty` and `presencePenalty`, as upstream does. The OpenAI default `language_model` and
+ `call` use Responses, matching upstream; `chat` remains an explicit accessor. Codex is `codex.responses`, its ChatGPT-account base URL
+ defaults to `https://chatgpt.com/backend-api/codex` (it was missing
+ `/codex`), and `store: false` is a package rule. `open_responses`: `url` →
+ `base_url`. The Azure deepseek / completion / MAI models and the Foundry item
+ type are not ported.
+- Single-modality vendors (28 packages) run async jobs on package constants:
+ a fixed interval and attempt cap (previously unbounded loops now stop after
+ 6000 × 100 ms), a transient poll error spends one attempt, and the interval
+ is overridden through the package namespace (`pollIntervalMs`, luma
+ `pollIntervalMillis`) which replaces the old per-provider poll settings.
+ `cartesia` `version` and `runwayml` `poll_interval` / `timeout` settings are
+ removed (headers / constants); `google_pse` `cx` resolves settings, then
+ providerOptions, then `GOOGLE_CSE_ID` per request; DataForSEO without
+ credentials fails with `LoadApiKey`; searxng accepts an optional bearer
+ token; the user-agent suffix is gone. `VideoModel::poll_config()` stays as
+ the package-constant source.
+- Removed from `aimux-providers`: `body_merge`, `openai_legacy`, `AzureAuth`,
+ `TokenProvider`, `VertexAuth`, `BedrockAuth`, `StaticBearerConfig` and the
+ `hmac` dependency (`sha2` / `hex` are dev-dependencies).
+- Bedrock regions must be a single DNS label; invalid values fail the call
+ with `InvalidArgument` before a request is sent.
+- `ExternalProviderEntry` debug output reports only whether `api_key` is
+ present, keeping literal keys out of debug logs.
+
+**C ABI**
+
+- New error codes `AIMUX_E_LOAD_API_KEY = 18` and `AIMUX_E_LOAD_SETTING = 19`
+ (appended; retired 4 is not reused). `aimux_error_provider_code` returns the
+ environment variable that was consulted and `aimux_error_provider_message`
+ the key's description (`"OpenAI"`) or the setting name (`"region"`); no new
+ symbol. `aimux_error_model_type` carries `NoSuchModel.model_type`. These two
+ failures used to surface as `AIMUX_E_INVALID_ARGUMENT`.
+- `aimux_provider_new`, `aimux_provider_handle_new` and `config_json` resolve
+ names through the binding compatibility module (overlays, vendor packages
+ and presets); `config_json` accepts
+ `base_url`, `headers`, `organization`, `project` and `params`, and rejects
+ `max_retries` and `body_overrides` with `AIMUX_E_INVALID_ARGUMENT`. A
+ provider handle is provider + discovery. `aimux_register_providers`
+ entries reject the same two keys.
+
+**Node / Python**
+
+- Node `ProviderConfig`: `maxRetries` and `bodyOverrides` are kept in the type
+ only so that passing them throws `InvalidArgumentError`, for every native
+ constructor (`openai`, `anthropic`,
+ `google`, `cohere`, `mistral`, `xai`, `deepseek`, `bedrock`, `vertex`,
+ `anthropicAws`, `azure`, `provider`, `createProvider`), not only OpenAI.
+ `headers` now applies to all of them. New `params: Record`
+ fills a preset's template parameters. Python `config` /
+ `config_json` accept `params` and reject `max_retries` / `body_overrides`
+ the same way.
+- New error classes `LoadAPIKeyError` (`envVar`, `description`) and
+ `LoadSettingError` (`envVar`, `settingName`) in Node; `LoadAPIKeyError`
+ (`env_var`, `description`) and `LoadSettingError` (`env_var`,
+ `setting_name`) in Python. Both extend `AimuxError`, no longer
+ `InvalidArgumentError`.
+- Recorded and traced provider strings follow `"{name}.{method}"`
+ (`openai.chat`, not `openai`).
+
+**Go / Java / Kotlin / Swift / Dart**
+
+- New error codes 18 / 19 (`CodeLoadAPIKey` / `CodeLoadSetting`,
+ `LoadAPIKeyError` / `LoadSettingError`, `.loadApiKey` / `.loadSetting`),
+ with the consulted environment variable as `EnvVar` / `envVar`.
+- The call-level `body_overrides` / `bodyOverrides` field is deleted from
+ `GenerateTextOptions` (the core no longer accepts the key; leaving it would
+ have been silently ignored). The provider `config_json` helpers
+ (`ProviderConfig` in Go and Dart) drop `max_retries` / `body_overrides` and
+ gain `params`; call-level `max_retries` is unchanged.
+
## [0.5.0] - 2026-09-27
**Breaking release.** 13 PRs since 0.3.0: the cross-language error model
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 959bcfa67..32e07985f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,8 +9,8 @@ to set up a development environment, run the tests, and submit changes.
aimux/
├── aimux-core/ # Core abstractions: LanguageModel / Provider / Message / StreamPart
├── aimux-providers/ # provider implementations + cassettes (counts: docs/api/providers.md)
-├── aimux-stream/ # SSE parsing
-├── aimux-provider-utils/ # HTTP utilities: retry, backoff, error parsing, API-key loading, streamed tool-call tracking
+├── aimux-stream/ # SSE decoding
+├── aimux-provider-utils/ # HTTP utilities: Fetch transport, Resolvable settings, error parsing, API-key loading, streamed tool-call tracking
├── aimux-ffi/ # C ABI (opaque handle + JSON + push callback) for non-native bindings
├── bindings/ # Node, Python, Swift, Kotlin, Flutter, Go, C — share one Rust core
├── rfc/ # Design docs (RFCs)
@@ -38,6 +38,20 @@ cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
```
+Provider changes also run the generator and boundary checks CI runs in the
+`contract-tests` job:
+
+```bash
+python3 scripts/gen_providers_doc.py --check # docs/api/providers.md
+python3 scripts/gen_ts_types.py --check # ts-rs types for the Node binding
+bash scripts/check_provider_boundaries.sh # factory-shape rules for provider packages
+```
+
+The Node and Python bindings are separate workspaces: run `cargo fmt` and
+`cargo clippy -- -D warnings` inside each, then `npm ci && npm run build &&
+npm test` (Node) or `maturin build --release`, `pip install` the wheel and
+`python -m pytest tests/` (Python).
+
## Testing
Tests run entirely on **cassette playback** — no network access and no API keys
@@ -62,10 +76,13 @@ aimux distinguishes three kinds of providers:
1. **Native protocol** — providers with their own request/response model
(OpenAI, Anthropic, Google, Bedrock, …). Implement the full `convert`/model
path and handle provider-specific differences.
-2. **OpenAI-compatible thin wrapper** — the majority. Describe differences via
- `OpenAICompatProfile` (top_k, tools, response_format, streaming usage,
- request-body post-processing) so the thin wrapper does not erase
- provider-specific behavior.
+2. **OpenAI-compatible preset** — the majority. One row of
+ `aimux-providers/src/provider_registry.json` (base URL, key variable or
+ `auth: none`, optional URL variable and template parameters) that
+ is embedded and parsed once into a runtime table. Create presets by name
+ through `provider(name, ...)`. Vendors with behavior of their own get a package on
+ the shared compatible chat model (`groq/`, `deepseek/`) instead of a
+ branch on their name.
3. **Modality-specific** — speech, image, video, transcription, etc.
Step-by-step checklists (with the generator and CI rules) live in
diff --git a/README.md b/README.md
index ef7630255..84ffcde9f 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
-> **A unified LLM access layer written in Rust. One API for [327 AI providers](docs/api/providers.md).**
+> **A unified LLM access layer written in Rust. One API for [327 provider names](docs/api/providers.md).**
[](https://github.com/arcships/aimux/actions/workflows/ci.yml)
[](LICENSE)
@@ -30,13 +30,10 @@ difference: aimux is an access layer, those are orchestration layers.
## Why aimux
-- **327 providers** (as of 2026-09-24) — 251 registry-backed OpenAI-compatible
- (unified `provider(name, ...)` entry) + 76 typed providers (native protocol
- implementations such as OpenAI/Anthropic/Google/Bedrock/Vertex, local
- engines like Ollama/vLLM, and speech/image/video/search modality providers).
- Counts by category and the full list live in
- [docs/api/providers.md](docs/api/providers.md) — generated, and the single
- source of truth for provider counts (only the badge above repeats the total).
+- **327 provider names** — 282 registry-backed OpenAI-compatible presets
+ + 45 vendor packages, covering text and other modalities. Local engines
+ such as Ollama/vLLM are presets. `provider_names()` lists the Rust names;
+ the generated catalogue lives in [docs/api/providers.md](docs/api/providers.md).
- **Unified, object-safe interface** — the `LanguageModel` trait supports
`Box` so providers are interchangeable without changing call sites.
- **Full multimodal** — text, streaming, tool calling, embeddings, image,
@@ -49,10 +46,12 @@ difference: aimux is an access layer, those are orchestration layers.
- **Composite models** (new in 0.3.0) — `RouterModel` routes each call with
fallback (RFC-0021); `MoaModel` aggregates parallel reference models
mixture-of-agents style (RFC-0022). Both are plain `LanguageModel`s.
-- **Config-driven provider registry** — `provider-registry.json` describes
- each registry-backed OpenAI-compatible provider (base URL, env var, profile
- quirks: top_k, tools, response_format, streaming usage, max_tokens key);
- one unified `provider(name, ...)` entry in every binding.
+- **Config-driven provider registry** — `provider_registry.json` describes
+ each registry-backed OpenAI-compatible provider (base URL, env var, auth
+ mode, template parameters); the embedded JSON is parsed once into a runtime
+ descriptor table. Rust uses `create_provider(name, PresetSettings)` for
+ presets and vendor packages; bindings retain `provider(name, ...)`.
+ Packages follow the AI SDK shape: `XxxProviderSettings` + `create_xxx()` + a default `xxx()`.
- **Fast and small** — Rust core, release profile tuned for binary size
(`lto`, `codegen-units=1`, `panic="abort"`, `strip`, `opt-level="z"`).
- **8 language bindings** from one core: Node, Python, Swift, Kotlin, Flutter,
@@ -148,14 +147,12 @@ console.log(result.text)
```rust
use aimux_core::prelude::*;
-use aimux_providers::{OpenAIConfig, OpenAIProvider};
+use aimux_providers::openai::openai;
#[tokio::main]
async fn main() -> Result<(), AiMuxError> {
- let provider = OpenAIProvider::new(
- OpenAIConfig::new(std::env::var("OPENAI_API_KEY")?)
- );
- let model = provider.model("gpt-4o");
+ // The default provider loads OPENAI_API_KEY when a request is made.
+ let model = openai().chat("gpt-4o");
let result = generate_text(
&model,
@@ -182,8 +179,8 @@ let result = stream_text(
let mut stream = result.stream;
while let Some(part) = stream.next().await {
match part? {
- StreamPart::TextDelta { delta, .. } => print!("{}", delta),
- StreamPart::Finish { .. } => println!("\n[done]"),
+ TextStreamPart::TextDelta { delta, .. } => print!("{}", delta),
+ TextStreamPart::Finish { .. } => println!("\n[done]"),
_ => {}
}
}
@@ -220,9 +217,10 @@ use aimux_core::moa::{MoaConfig, MoaModel};
use aimux_core::router::{FallbackPolicy, RouterConfig, RouterModel, RuleRouter};
// Router: pick one model per call, fall back on failure (RFC-0021).
+let provider = aimux_providers::openai::openai();
let children: Vec = vec![
- Arc::new(provider.model("gpt-4o")) as ChildModel,
- Arc::new(provider.model("gpt-4o-mini")) as ChildModel,
+ Arc::new(provider.chat("primary-model-id")) as ChildModel,
+ Arc::new(provider.chat("secondary-model-id")) as ChildModel,
];
let routed = RouterModel::new(
children.clone(),
@@ -234,7 +232,7 @@ let routed = RouterModel::new(
// MoA: fan out to references, aggregate their answers (RFC-0022).
let moa = MoaModel::new(
children,
- Arc::new(provider.model("gpt-4o")) as ChildModel,
+ Arc::new(provider.chat("primary-model-id")) as ChildModel,
MoaConfig::default(),
);
@@ -273,35 +271,36 @@ session.close()
## Switch providers
```rust
-// OpenAI → DeepSeek: only the provider name changes (registry-backed;
-// key read from the provider's env var)
-use aimux_providers::{provider, provider_from_env};
+use aimux_core::provider_registry::create_provider_registry;
+use aimux_providers::{create_provider, default_providers, provider_names, PresetSettings};
-let model = provider("deepseek", None, "deepseek-chat", None)?;
-let model = provider_from_env("deepseek", "deepseek-chat", None)?;
+// Keys are loaded when a request is made.
+let provider = create_provider("deepseek", PresetSettings::default())?;
+let model = provider.language_model("deepseek-chat")?;
+
+// Or assemble the built-in providers and resolve a provider:model id.
+let registry = create_provider_registry(default_providers(), Default::default());
+let model = registry.language_model("deepseek:deepseek-chat")?;
+let names = provider_names();
// model usage is identical — it's all dyn LanguageModel
```
-All registry-backed OpenAI-compatible providers share one entry:
-`provider(name, ...)` in every binding. Provider names are runtime strings;
-the generated provider list is the discovery surface.
-The retired per-provider shell types (`XxxConfig`/`XxxProvider`) are gone —
-see [docs/API.md](docs/API.md#providers).
-
-> **Scope:** OpenAI-compatible → `provider(name, ...)`; others (Anthropic,
-> multimodal, local…) → their constructors. List:
-> [docs/api/providers.md](docs/api/providers.md).
+Rust by-name creation covers vendor packages and all 282 presets. Each vendor
+also has `create_xxx(XxxProviderSettings {..})` and a default instance `xxx()`.
+Bindings keep their own `provider(name, ...)` API. Runtime model listing uses
+`Provider::discovery()` and then `ProviderDiscovery::list_models()` — see
+[docs/api/rust.md](docs/api/rust.md#providers).
## Provider coverage
-Counts by category live in
-[docs/api/providers.md](docs/api/providers.md#totals) — generated from the
-registry and `lib.rs`, and the single source of truth. The rough shape of
+The generated catalogue is [docs/api/providers.md](docs/api/providers.md).
+The accepted Rust names come from `provider_names()`. The rough shape of
the roster:
-- **Native protocol** — OpenAI, Anthropic, Google, Bedrock, Vertex, Azure, Cohere, Mistral, xAI, Anthropic-AWS, Voyage, Codex, OpenRouter
-- **OpenAI-compatible (registry-backed)** — Groq, Fireworks, Together, Perplexity, Ollama Cloud, DeepSeek, Alibaba Tongyi, Zhipu, Baidu, Tencent, Moonshot, SiliconFlow…
-- **OpenAI-compatible (standalone + Vertex-hosted)** — Hugging Face, Ollama, vLLM, SGLang, Llama.cpp, LiteLLM Proxy, Vertex-hosted DeepSeek/Qwen/Llama…
+- **Native protocol** — OpenAI, Anthropic, Google, Bedrock, Vertex, Azure, Cohere, Mistral, Groq, DeepSeek, xAI, Anthropic-AWS, Voyage
+- **OpenAI-compatible (registry-backed)** — Fireworks, Together, Perplexity, OpenRouter, Ollama Cloud, Alibaba Tongyi, Zhipu, Baidu, Tencent, Moonshot, SiliconFlow…
+- **Local and Vertex-hosted presets** — Ollama, vLLM, SGLang, Llama.cpp, LiteLLM Proxy, Vertex-hosted DeepSeek/Qwen/Llama…
+- **Responses API for text** — xAI and Hugging Face (no chat-completions entry)
- **Speech / transcription** — ElevenLabs, Deepgram, AssemblyAI, AWS Polly, Cartesia, Hume, Gladia, RevAI, LMNT, Fal
- **Image / video** — Black Forest Labs, Replicate, Luma, Prodia, KlingAI, Recraft, Stability, RunwayML
- **Embeddings / rerank / search** — Voyage, Jina, Tavily, Exa, Firecrawl, Serper, SearXNG, You.com…
diff --git a/ROADMAP.md b/ROADMAP.md
index bcd4c3d8e..4ea40c032 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -26,7 +26,7 @@ aimux 是 **provider 接入与治理的运行时**:
│ ├─→ #174 Auth L1 ──→ #175 Auth L2 ← 依赖 B1
│ └─→ #167 transport-level replay ──→ #179 replay 子命令 ← 依赖 B1
├─ C 轨 FFI(现 123 个 extern "C",#166 基线 109)→ ops 协议(C1 升级为传输无关协议:op 表 / 二进制帧 / 版本协商)
-│ └─ C2 构造器转发 shim 是 B4/B5 前置
+│ └─ C2 构造器转发 shim 取消:B4/B5 的直接构造调用点随 provider 工厂切换一次迁移(调整:见 [docs/aisdk-architecture-alignment.md §0.7](docs/aisdk-architecture-alignment.md#07-与-roadmap--rfc-0036-既有承诺的关系))
├─ D 轨 8 种绑定重写为 ops 薄封装(依赖 C1)+ D8 类型镜像生成
├─ E 轨 内部清理(#164 已合入,改为 master 上独立 PR)+ 文档三层重组
│
@@ -66,30 +66,30 @@ aimux 是 **provider 接入与治理的运行时**:
| 项 | 内容 |
|---|---|
| B2–B8 | 17 个 LanguageModel 实现 → ~7 个协议;33 个 wrapper 退役;responses 家族合并(遵循 #166 修正 3/6) |
-| #174 / #175 | registry `auth` schema + `apply_auth()`;Credential 解析序 + CredentialStore + TokenRefresher |
+| #174 / #175 | registry `auth` 列作为 preset descriptor 数据,鉴权在各厂商包内以 headers 闭包 / fetch 装饰器实现;CredentialStore + TokenRefresher 随宿主回调后置(调整:见 [docs/aisdk-architecture-alignment.md §0.7](docs/aisdk-architecture-alignment.md#07-与-roadmap--rfc-0036-既有承诺的关系)) |
| L2 补齐 | `StreamPart::Raw` / `provider_metadata` 在各协议接线——投影不下的不得静默丢弃 |
| **S3** | `cargo-bloat` 审计 0.5.0 增重(Android `.so` 13.4 → 21 MB);feature gating 仅作可选小体积路径,默认全量 |
| ~~调研 RFC~~ | ~~L2 数据模型选型(AI SDK 形态 / Open Responses items / 自有),指标见 RFC-0036 §3~~ 撤销:选型已定为 AI SDK V4 形态(调整:见 [docs/aisdk-architecture-alignment.md §0.7](docs/aisdk-architecture-alignment.md#07-与-roadmap--rfc-0036-既有承诺的关系)) |
-版本语义:Rust API 大 breaking(33 wrapper 退役);C ABI 经 C2 转发不受影响。
+版本语义:Rust API、binding wire 与 C ABI 在全链路对齐切换时一次性 breaking(33 wrapper 退役、构造器替换),不经 C2 转发(调整:见 [docs/aisdk-architecture-alignment.md §0.7](docs/aisdk-architecture-alignment.md#07-与-roadmap--rfc-0036-既有承诺的关系))。
### 0.8.0 ——「ops 协议落地」(~4-5 周)
| 项 | 内容 |
|---|---|
-| C1 | 9 个导出 + `dispatch`,**同一 dispatch 同时服务 FFI 与 stdio CLI**;旧导出保留共存 |
+| C1 | 9 个导出 + `dispatch`,**同一 dispatch 同时服务 FFI 与 stdio CLI**;旧符号按切换门一次替换,不共存(调整:见 [docs/aisdk-architecture-alignment.md §0.7](docs/aisdk-architecture-alignment.md#07-与-roadmap--rfc-0036-既有承诺的关系)) |
| C3 | `aimux_error_*` 访问器 → 错误 JSON 信封(含派生 `retry_after_ms`) |
| D1–D7 | 8 种绑定迁到 ops 薄封装。D1 Kotlin(只依赖 Java artifact)与 D5 Flutter ffigen 过渡版不依赖 C1,可在 0.6/0.7 提前做 |
| D8 | 类型镜像生成:serde → JSON Schema → 各语言,`--check` 门禁扩到全部输出 |
| P | stdio 入口往返开销纳入性能门禁 |
-版本语义:新 ABI 加入,旧导出并存(#166 要求共存至少一个 minor 版本);绑定按各自节奏迁移。“共存至少一个 minor”不再作为承诺:全链路对齐切换本身没有共存期,之后 ops ABI 引入时的旧符号也按切换门一次替换;C2 转发 shim 与 C4 旧导出清理随之取消(调整:见 [docs/aisdk-architecture-alignment.md §0.7](docs/aisdk-architecture-alignment.md#07-与-roadmap--rfc-0036-既有承诺的关系))。
+版本语义:新 ABI 按切换门一次替换旧符号,不提供共存期(#166 原要求的“共存至少一个 minor”撤销);全链路对齐切换本身同样没有共存期,C2 转发 shim 与 C4 旧导出清理随之取消(调整:见 [docs/aisdk-architecture-alignment.md §0.7](docs/aisdk-architecture-alignment.md#07-与-roadmap--rfc-0036-既有承诺的关系))。
### 0.9.0 ——「治理」(~3-4 周)
| 项 | 内容 |
|---|---|
-| #167 | transport-level replay:mock 挂 HTTP 层跑真协议代码,覆盖全协议 / 全模态;`ProviderRecord` = registry 行 + protocol |
+| #167 | transport-level replay:mock 挂 HTTP 层跑真协议代码,覆盖全协议 / 全模态;录制只存身份,不保存可重建的 `ProviderRecord`(调整:见 [docs/aisdk-architecture-alignment.md §0.7](docs/aisdk-architecture-alignment.md#07-与-roadmap--rfc-0036-既有承诺的关系)) |
| 漂移检测 | #170 扩展:定期重录,与 cassette 字节级 diff |
| 能力矩阵 | provider × 能力,每格由探测或录制支撑 |
| CLI | `aimux probe / replay / diff`(#179 replay 子命令并入);#181 debug CLI 并入 |
@@ -98,7 +98,7 @@ aimux 是 **provider 接入与治理的运行时**:
| 项 | 内容 |
|---|---|
-| C4 | 8 种绑定全部迁移后删除旧导出、旧头文件、旧 FFI 测试 |
+| ~~C4~~ | ~~8 种绑定全部迁移后删除旧导出、旧头文件、旧 FFI 测试~~ 取消:旧导出在切换时随绑定一起删除,1.0 不再补删(调整:见 [docs/aisdk-architecture-alignment.md §0.7](docs/aisdk-architecture-alignment.md#07-与-roadmap--rfc-0036-既有承诺的关系)) |
| 冻结范围 | ops 协议与 L0 / L1 契约。**L2 独立版本化,不随 1.0 冻结** |
| 发布判据 | #166 ledger 全勾(B9 / #171 后半除外);错误模型 #95 与请求管线 #164 稳定一个周期;各门禁(S2 / P)连续绿 |
diff --git a/aimux-core/Cargo.toml b/aimux-core/Cargo.toml
index c48aaee7c..70ab4aeec 100644
--- a/aimux-core/Cargo.toml
+++ b/aimux-core/Cargo.toml
@@ -26,6 +26,7 @@ httpdate = { workspace = true }
rand = { workspace = true }
base64 = "0.22"
jsonschema = { workspace = true }
+regex = "1"
[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt", "test-util"] }
diff --git a/aimux-core/src/composite.rs b/aimux-core/src/composite.rs
index d4f3b5505..e737471cb 100644
--- a/aimux-core/src/composite.rs
+++ b/aimux-core/src/composite.rs
@@ -8,9 +8,9 @@
//!
//! This module holds the pieces both share:
//! - [`ChildModel`] — the child-handle shape (`Arc`).
-//! - [`add_usage`] — per-field `Usage` accumulation.
-//! - [`extract_text`] — pull text out of a `GenerateContent` list.
-//! - [`build_aggregator_prompt`] — assemble the MoA aggregator prompt.
+//! - `add_usage` — per-field `Usage` accumulation.
+//! - `extract_text` — pull text out of a `GenerateContent` list.
+//! - `build_aggregator_prompt` — assemble the MoA aggregator prompt.
//!
//! See [`crate::trace::layer::TraceLayer`] for the established decorator pattern
//! (`inner: Arc` implementing `LanguageModel`); composite
@@ -34,7 +34,7 @@ pub type ChildModel = Arc;
/// Each usage detail field is `Option`; `None` is treated as zero so a
/// child that doesn't report a breakdown doesn't erase the other child's data.
#[must_use]
-pub fn add_usage(a: Usage, b: &Usage) -> Usage {
+pub(crate) fn add_usage(a: Usage, b: &Usage) -> Usage {
Usage {
input_tokens: InputTokenUsage {
total: opt_add(a.input_tokens.total, b.input_tokens.total),
@@ -64,7 +64,7 @@ fn opt_add(a: Option, b: Option) -> Option {
/// are kept; `Reasoning` / `ToolCall` / `Source` / `File` are dropped (MoA
/// thin version — references contribute analysis text, not tool calls).
#[must_use]
-pub fn extract_text(content: &[GenerateContent]) -> String {
+pub(crate) fn extract_text(content: &[GenerateContent]) -> String {
let mut out = String::new();
for c in content {
if let GenerateContent::Text { text, .. } = c {
@@ -83,7 +83,7 @@ pub fn extract_text(content: &[GenerateContent]) -> String {
/// The reference list may be empty — in that case the aggregator just runs the
/// original prompt (degenerates to a single-model call).
#[must_use]
-pub fn build_aggregator_prompt(
+pub(crate) fn build_aggregator_prompt(
prompt: &LanguageModelPrompt,
instructions: Option<&str>,
references: &[(String, String)],
diff --git a/aimux-core/src/decision_model.rs b/aimux-core/src/decision_model.rs
index 29a27278f..d72ef3825 100644
--- a/aimux-core/src/decision_model.rs
+++ b/aimux-core/src/decision_model.rs
@@ -507,21 +507,9 @@ impl DecisionResult {
#[async_trait]
pub trait DecisionModel: Send + Sync {
- fn specification_version(&self) -> &'static str {
- "v4"
- }
fn provider(&self) -> &str;
fn model_id(&self) -> &str;
fn capabilities(&self) -> DecisionCapabilities;
- fn config_snapshot(&self) -> crate::recording::ProviderRecord {
- let mut snapshot =
- crate::recording::ProviderRecord::minimal(self.provider(), self.model_id());
- snapshot.profile = Some(serde_json::json!({"decision_capabilities": self.capabilities()}));
- snapshot
- }
- fn retry_config(&self) -> crate::retry::RetryConfig {
- crate::retry::RetryConfig::default()
- }
/// Perform one provider attempt.
/// # Errors
/// Returns provider, transport or invalid response errors.
@@ -606,11 +594,7 @@ pub async fn decide(
}
let timeout = timeout::OperationTimeout::new(options.timeout.unwrap_or_default())?;
let abort_signal = options.abort_signal.clone();
- let retries = retry::prepare_retries(
- options.max_retries,
- model.retry_config(),
- abort_signal.clone(),
- );
+ let retries = retry::prepare_retries(options.max_retries, abort_signal.clone());
let context = crate::recording::recorder().map(|recorder| {
let ctx =
crate::recording::RecordingContext::new(crate::recording::new_call_id(), recorder);
@@ -620,9 +604,12 @@ pub async fn decide(
&options,
model.provider(),
model.model_id(),
+ &capabilities,
+ );
+ ctx.recorder.record_provider(
+ &ctx.call_id,
+ &crate::recording::ProviderRecord::from_model(model.provider(), model.model_id()),
);
- ctx.recorder
- .record_provider(&ctx.call_id, &model.config_snapshot());
ctx
});
let result = timeout::run(
diff --git a/aimux-core/src/decision_replay.rs b/aimux-core/src/decision_replay.rs
index 1b3c0ddb8..64f169d72 100644
--- a/aimux-core/src/decision_replay.rs
+++ b/aimux-core/src/decision_replay.rs
@@ -51,16 +51,9 @@ impl MockDecisionReplayModel {
"decision replay: no matching provider/model recording".into(),
)
})?;
- let capabilities = serde_json::from_value(
- first
- .provider
- .profile
- .as_ref()
- .and_then(|v| v.get("decision_capabilities"))
- .cloned()
- .unwrap_or(Value::Null),
- )
- .map_err(|e| AiMuxError::InvalidArgument(format!("decision replay capabilities: {e}")))?;
+ let capabilities = first.input.decision_capabilities.clone().ok_or_else(|| {
+ AiMuxError::InvalidArgument("decision replay: missing decision capabilities".into())
+ })?;
Ok(Self {
provider,
model_id,
diff --git a/aimux-core/src/embedding_model.rs b/aimux-core/src/embedding_model.rs
index 7f1807048..e97ff1dab 100644
--- a/aimux-core/src/embedding_model.rs
+++ b/aimux-core/src/embedding_model.rs
@@ -117,21 +117,12 @@ pub struct EmbeddingResponse {
/// `true`; otherwise they should return an error.
#[async_trait]
pub trait EmbeddingModel: Send + Sync {
- /// Specification version (always `"v4"`).
- fn specification_version(&self) -> &'static str {
- "v4"
- }
-
/// Provider name, e.g. `"openai"`.
fn provider(&self) -> &str;
/// Provider-specific model ID, e.g. `"text-embedding-3-small"`.
fn model_id(&self) -> &str;
- fn retry_config(&self) -> crate::retry::RetryConfig {
- crate::retry::RetryConfig::default()
- }
-
/// Limit of how many embeddings can be generated in a single API call.
///
/// `None` means the model has no fixed limit. The TS spec allows this to
@@ -159,11 +150,7 @@ pub async fn embed(
) -> Result {
let timeout = timeout::OperationTimeout::new(options.timeout.unwrap_or_default())?;
let abort_signal = options.abort_signal.clone();
- let retries = retry::prepare_retries(
- options.max_retries,
- model.retry_config(),
- abort_signal.clone(),
- );
+ let retries = retry::prepare_retries(options.max_retries, abort_signal.clone());
timeout::run(
retries.retry(|| model.do_embed(&options)),
abort_signal.as_ref(),
diff --git a/aimux-core/src/error.rs b/aimux-core/src/error.rs
index e39bf2be8..d9c7bfd9b 100644
--- a/aimux-core/src/error.rs
+++ b/aimux-core/src/error.rs
@@ -224,6 +224,34 @@ pub enum AiMuxError {
#[error("invalid prompt: {0}")]
InvalidPrompt(String),
+ /// No API key was passed and the fallback environment variable is unset
+ /// (the AI SDK's `LoadAPIKeyError`). Pre-HTTP: no request was made.
+ #[error(
+ "No API key found for {description}. \
+ Please provide it via the `api_key` parameter \
+ or set the `{env_var}` environment variable."
+ )]
+ LoadApiKey {
+ /// The environment variable consulted as the fallback.
+ env_var: String,
+ /// What the key is for, e.g. `"OpenAI"`.
+ description: String,
+ },
+
+ /// A required provider setting was not passed and its fallback
+ /// environment variable is unset (the AI SDK's `LoadSettingError`).
+ #[error(
+ "No value found for setting `{name}`. \
+ Please provide it via the `{name}` parameter \
+ or set the `{env_var}` environment variable."
+ )]
+ LoadSetting {
+ /// The environment variable consulted as the fallback.
+ env_var: String,
+ /// The setting's parameter name, e.g. `"region"`.
+ name: String,
+ },
+
/// The access token has expired (or was invalidated) and must be refreshed
/// by the caller. RFC-0018 subscription mode: the library maps a 401 from
/// the Codex subscription endpoint to this variant; the integrator
@@ -252,8 +280,13 @@ pub enum AiMuxError {
/// Registry-level "provider name does not resolve" (the AI SDK's
/// `NoSuchProviderError`).
- #[error("No such provider: {provider_id}")]
- NoSuchProvider { provider_id: String },
+ #[error("No such provider: {provider_id} (available providers: {})", .available_providers.join(","))]
+ NoSuchProvider {
+ provider_id: String,
+ model_id: String,
+ model_type: String,
+ available_providers: Vec,
+ },
#[error("request timed out: {0}")]
Timeout(String),
@@ -296,6 +329,18 @@ pub fn is_retryable_status(status: u16) -> bool {
}
impl AiMuxError {
+ /// Build a [`NoSuchModel`](Self::NoSuchModel) for `model_id`.
+ ///
+ /// `model_type` is the AI SDK's `modelType` (`"languageModel"`,
+ /// `"embeddingModel"`, `"imageModel"`, …).
+ #[must_use]
+ pub fn no_such_model(model_id: &str, model_type: &str) -> Self {
+ Self::NoSuchModel {
+ model_id: model_id.to_string(),
+ model_type: model_type.to_string(),
+ }
+ }
+
/// Returns `true` for a malformed individual stream frame that does not
/// prove the underlying transport has failed. Stream reducers may report
/// these errors and continue polling for later, independently framed data.
diff --git a/aimux-core/src/files_model.rs b/aimux-core/src/files_model.rs
index d2fcb1120..091488838 100644
--- a/aimux-core/src/files_model.rs
+++ b/aimux-core/src/files_model.rs
@@ -98,11 +98,6 @@ pub struct UploadFileResult {
/// et al.
#[async_trait]
pub trait Files: Send + Sync {
- /// Specification version (always `"v4"`).
- fn specification_version(&self) -> &'static str {
- "v4"
- }
-
/// Provider ID.
fn provider(&self) -> &str;
diff --git a/aimux-core/src/generate.rs b/aimux-core/src/generate.rs
index 622219230..3a71543df 100644
--- a/aimux-core/src/generate.rs
+++ b/aimux-core/src/generate.rs
@@ -102,8 +102,6 @@ pub struct GenerateTextOptions {
pub reasoning: Option,
/// System instructions prepended to the prompt.
pub instructions: Option,
- /// Per-call request body overrides (deep-merged). See RFC-0017.
- pub body_overrides: Option,
/// Per-call retry count override. `None` = provider default, `Some(0)` = disable.
pub max_retries: Option,
/// Per-call timeout configuration (total / first-chunk / chunk idle).
@@ -159,7 +157,6 @@ impl GenerateTextOptions {
headers: self.headers,
provider_options: self.provider_options,
reasoning: self.reasoning,
- body_overrides: self.body_overrides,
max_retries: self.max_retries,
timeout: self.timeout,
session_id: self.session_id,
@@ -579,11 +576,7 @@ pub(crate) async fn generate_text_from_language_model_prompt(
let operation_timeout =
timeout::OperationTimeout::new(call_options.timeout.unwrap_or_default())?;
let abort_signal = call_options.abort_signal.clone();
- let retries = retry::prepare_retries(
- call_options.max_retries,
- model.retry_config(),
- abort_signal.clone(),
- );
+ let retries = retry::prepare_retries(call_options.max_retries, abort_signal.clone());
// 2a. RFC-0023: 关闭时零成本(M2 评审)——仅在录制开启时生成 call_id
// 并绑定 recorder 快照;传输封闭由层 A 收尾声明(P1 无层 B,barrier
@@ -599,8 +592,10 @@ pub(crate) async fn generate_text_from_language_model_prompt(
model.provider(),
model.model_id(),
);
- ctx.recorder
- .record_provider(&ctx.call_id, &model.config_snapshot());
+ ctx.recorder.record_provider(
+ &ctx.call_id,
+ &crate::recording::ProviderRecord::from_model(model.provider(), model.model_id()),
+ );
// 层 A 早发 closed(defense-in-depth):无 HTTP 的调用(mock/local)也
// 能完成 barrier;真实 HTTP 的骨架 finalized=false 仍会挡写,由层 B
// 结束再发 closed 完成。双发幂等。
@@ -941,11 +936,7 @@ pub async fn stream_text(
.map(|duration_ms| timeout::TimeoutDeadline::from_now("First chunk", duration_ms))
.transpose()?;
let abort_signal = call_options.abort_signal.clone();
- let retries = retry::prepare_retries(
- call_options.max_retries,
- model.retry_config(),
- abort_signal.clone(),
- );
+ let retries = retry::prepare_retries(call_options.max_retries, abort_signal.clone());
// 2a. RFC-0023: 关闭时零成本(M2 评审)——仅在录制开启时生成 call_id
// 并绑定 recorder 快照;传输封闭由层 A 收尾声明(P1 无层 B,barrier
@@ -961,8 +952,10 @@ pub async fn stream_text(
model.provider(),
model.model_id(),
);
- ctx.recorder
- .record_provider(&ctx.call_id, &model.config_snapshot());
+ ctx.recorder.record_provider(
+ &ctx.call_id,
+ &crate::recording::ProviderRecord::from_model(model.provider(), model.model_id()),
+ );
// 层 A 早发 closed(defense-in-depth):无 HTTP 的调用(mock/local)也
// 能完成 barrier;真实 HTTP 的骨架 finalized=false 仍会挡写,由层 B
// 结束再发 closed 完成。双发幂等。
diff --git a/aimux-core/src/image_model.rs b/aimux-core/src/image_model.rs
index 2a1ae9418..159de19df 100644
--- a/aimux-core/src/image_model.rs
+++ b/aimux-core/src/image_model.rs
@@ -181,21 +181,12 @@ pub struct ImageResponse {
/// Aligned with V4 `ImageModelV4`.
#[async_trait]
pub trait ImageModel: Send + Sync {
- /// Specification version (always `"v4"`).
- fn specification_version(&self) -> &'static str {
- "v4"
- }
-
/// Provider name, e.g. `"openai"`.
fn provider(&self) -> &str;
/// Provider-specific model ID, e.g. `"dall-e-3"`.
fn model_id(&self) -> &str;
- fn retry_config(&self) -> crate::retry::RetryConfig {
- crate::retry::RetryConfig::default()
- }
-
/// Limit of how many images can be generated in a single API call.
///
/// `None` means there is no fixed limit. The TS spec allows this to be a
@@ -203,6 +194,16 @@ pub trait ImageModel: Send + Sync {
/// `Option`.
fn max_images_per_call(&self) -> Option;
+ /// Whether image file inputs are supported; `None` means unknown.
+ fn supports_file_inputs(&self) -> Option {
+ None
+ }
+
+ /// Whether mask inputs are supported; `None` means unknown.
+ fn supports_mask_inputs(&self) -> Option {
+ None
+ }
+
/// Generate an array of images.
///
/// Naming: the `do_` prefix prevents accidental direct usage by users.
@@ -220,11 +221,7 @@ pub async fn generate_image(
) -> Result {
let timeout = timeout::OperationTimeout::new(options.timeout.unwrap_or_default())?;
let abort_signal = options.abort_signal.clone();
- let retries = retry::prepare_retries(
- options.max_retries,
- model.retry_config(),
- abort_signal.clone(),
- );
+ let retries = retry::prepare_retries(options.max_retries, abort_signal.clone());
timeout::run(
retries.retry(|| model.do_generate(&options)),
abort_signal.as_ref(),
diff --git a/aimux-core/src/image_model_middleware.rs b/aimux-core/src/image_model_middleware.rs
new file mode 100644
index 000000000..c1789aa8c
--- /dev/null
+++ b/aimux-core/src/image_model_middleware.rs
@@ -0,0 +1,137 @@
+//! Image-model middleware and wrapping, aligned with the upstream V4 hooks.
+
+use std::sync::Arc;
+
+use async_trait::async_trait;
+use futures::future::BoxFuture;
+
+use crate::error::AiMuxError;
+use crate::image_model::{ImageCallOptions, ImageModel, ImageResult};
+
+/// Optional hooks for overriding model properties and wrapping generation.
+#[async_trait]
+pub trait ImageModelMiddleware: Send + Sync {
+ fn override_provider(&self, _model: &dyn ImageModel) -> Option {
+ None
+ }
+
+ fn override_model_id(&self, _model: &dyn ImageModel) -> Option {
+ None
+ }
+
+ fn override_max_images_per_call(&self, _model: &dyn ImageModel) -> Option {
+ None
+ }
+
+ /// `Some(None)` overrides support to unknown; `None` leaves it unchanged.
+ fn override_supports_file_inputs(&self, _model: &dyn ImageModel) -> Option