* refactor: use provider credentials for worker builders * refactor: resolve api proxy credentials directly * fix: lazy load frontend eval modes
21 KiB
Refactor Plan: windmill-ai Crate
Context
AI provider logic is currently split across three crates with duplicate code:
- windmill-common — base types (
ai_types,ai_providers,ai_google,ai_bedrock,ai_cache) - windmill-api — chat proxy routes (
ai.rs), audit logging, caching, and DB-backed credential resolution intoProviderCredentials - windmill-worker — agent execution (
ai/module) withQueryBuildertrait, SSE parsers, provider implementations
The goal: a single windmill-ai crate with all AI provider logic. Worker agent execution uses QueryBuilder; the API proxy uses QueryBuilder::build_proxy_request for HTTP-forwarding providers and native proxy handlers for providers that need response conversion or SDK execution.
Dependency Direction
windmill-ai → windmill-common (for DB, Error, AgentAction, AuthedClient, etc.)
→ windmill-types (for S3Object)
→ windmill-parser (for Typ, used in OpenAPISchema)
windmill-api → windmill-ai
windmill-worker → windmill-ai
windmill-common does NOT re-export from windmill-ai (would be circular). All consumers update imports.
Reviewer Note: Keep API Proxy Unification Split
The crate boundary, shared utilities, SSE parsers, image handling, worker provider implementations, provider-specific API proxy transformations, and resolved runtime credential shape are now in windmill-ai. Raw API resources and worker agent provider payloads remain separate input/deserialization shapes and convert into ProviderCredentials at execution boundaries.
Do not jump directly from the current state to full proxy and credential unification in one PR. The API proxy combines request transformation, endpoint selection, auth headers, custom headers, OAuth user injection, Azure URL handling, Anthropic Vertex handling, Bedrock SDK calls, and SSE keepalive behavior. Split the work by risk:
- Introduce shared proxy request and credential types first.
- Move the OpenAI-compatible proxy path into
windmill-ainext, while keeping provider-native behavior unchanged. - Move Anthropic/Vertex, Google AI, and Bedrock in separate follow-up PRs.
- Unify credential resolution only after all proxy request builders use the shared shape.
Avoid adding modules whose only purpose is to re-export moved code. Direct imports from windmill_ai make ownership and dependency direction clearer at each call site.
Also do not make build_proxy_request(raw_body, path) too narrow. The proxy path needs method, incoming headers, resolved credentials, base URL/platform, organization/user fields, custom headers, and Bedrock/Azure/Vertex-specific context. Introduce a structured ProxyBuildArgs/ProviderCredentials shape before deleting AIRequestConfig::prepare_request, google.rs, or bedrock.rs.
Completed Phase: Proxy Contract + OpenAI-Compatible Proxy ✅
Goal: introduce the shared API proxy contract in windmill-ai and move the OpenAI-compatible proxy request builder there without changing provider behavior.
Suggested PR title: refactor(ai): move openai-compatible proxy building to windmill-ai.
Scope:
- Add
windmill-ai/src/proxy.rsand export it fromlib.rs. - Define
ProviderCredentials,ProxyBuildArgs, andProxyRequest. - Include all context known to be needed by the current API proxy path: method, path, incoming headers, body, provider, base URL, API key, OAuth access token, organization/user fields, platform, 1M context flag, custom headers, region, and AWS credentials.
- Add a conversion from API-side
AIRequestConfigtoProviderCredentials. - Add
QueryBuilder::build_proxy_requestwith a default unsupported-provider implementation. - Implement
build_proxy_requestfor OpenAI-compatible providers (OpenAI,AzureOpenAI,Mistral,DeepSeek,Groq,OpenRouter,TogetherAI,CustomAI). - Route workspace and global API proxy requests for OpenAI-compatible providers through
windmill-ai. - Keep FIM transformation in
windmill-apibefore calling the proxy builder. - Keep
AIRequestConfig::prepare_requestfor Anthropic/Vertex and remaining fallback paths.
Out of scope:
- Do not move Anthropic/Vertex proxy behavior yet.
- Do not move Google AI or Bedrock proxy behavior yet.
- Do not change credential resolution, audit logging, cache behavior, SSE keepalive behavior, or Bedrock/Google special cases.
- Do not remove
windmill-api/src/google.rs,windmill-api/src/bedrock.rs, orAIRequestConfig::prepare_request.
Validation:
cargo test -p windmill-ai proxycargo test -p windmill-api invalidates_all_cached_providers_for_workspacecargo check -p windmill-ai -p windmill-apicargo check -p windmill-ai -p windmill-api --features bedrock
Follow-up status: Anthropic/Vertex proxy handling has since moved into
windmill-ai, and the dead AIRequestConfig::prepare_request fallback has
been removed.
Completed Phase: Proxy Execution Mode + Google AI Proxy Migration ✅
Goal: introduce a shared provider execution classifier before moving Google AI
and Bedrock. ProxyRequest is a good contract for HTTP-forwarding providers
such as OpenAI-compatible providers and Anthropic, but Google AI also converts
responses back to OpenAI shape and Bedrock uses SDK execution. Model that split
explicitly before moving those providers, then move the Google AI proxy
transformation into windmill-ai as the first native-provider migration.
Suggested PR title: refactor(ai): add provider proxy execution mode.
Scope:
- Add
ProxyExecutionModeinwindmill-ai::proxy. - Classify providers as HTTP-forwarding, native Google AI, or native Bedrock.
- Make
supports_query_builder_proxyderive from the shared execution mode. - Use the shared execution mode in
windmill-api/src/ai.rsfor workspace proxy routing. - Move Google AI workspace proxy request conversion, streaming/non-streaming response conversion, and model-list normalization into
windmill-ai::providers::google_ai. - Share Google AI
GeminiTextRequestand generation-config construction between worker agent requests and API proxy requests. - Delete the API-local
windmill-api/src/google.rsmodule. - Keep global proxy behavior, Bedrock native handling, credential resolution, audit logging, caching, and SSE keepalive behavior unchanged.
Out of scope:
- Do not move
windmill-api/src/bedrock.rs. - Do not unify
AIRequestConfigandProviderWithResource.
Validation:
cargo test -p windmill-ai google_aicargo test -p windmill-ai proxycargo test -p windmill-api invalidates_all_cached_providers_for_workspacecargo test -p windmill-ai anthropic
Follow-up status: Bedrock native proxy handling has since moved into
windmill-ai, and the API-local windmill-api/src/bedrock.rs module has been
removed.
Completed Phase: Bedrock Native Proxy Migration ✅
Goal: move the remaining native-provider API proxy execution out of
windmill-api and into windmill-ai, while leaving API-owned routing,
credential resolution, auditing, cache behavior, and Axum response conversion in
windmill-api.
Suggested PR title: refactor(ai): move bedrock proxy handling to windmill-ai.
Scope:
- Move Bedrock control-plane proxy calls (
foundation-models,inference-profiles) intowindmill-ai::providers::bedrock. - Move Bedrock chat proxy OpenAI request parsing, Converse request execution,
streaming SSE conversion, non-streaming OpenAI-shaped response conversion, and
auth selection into
windmill-ai::providers::bedrock. - Add an Axum-free
BedrockProxyResponseshape inwindmill-ai; the API route converts it into an Axum body. - Move the optional
aws-sdk-bedrockdependency fromwindmill-apitowindmill-ai. - Delete the API-local
windmill-api/src/bedrock.rsmodule.
Out of scope:
- Do not unify
AIRequestConfigandProviderWithResource. - Do not change Bedrock credential resolution, audit logging, request caching, or non-Bedrock proxy behavior.
Validation:
cargo test -p windmill-ai bedrock --features bedrockcargo check -p windmill-ai -p windmill-apicargo check -p windmill-ai -p windmill-api --features bedrock
Known Follow-Ups
These are not blockers for the current migration PR because they either preserve existing behavior or need a separate product decision, but they should stay visible for later hardening work.
- Google AI/Gemini native proxy custom headers: the native Google AI proxy
path intentionally does not apply
AI_HTTP_HEADERSor resource-level custom headers today. Decide whether and how env/resource custom-header injection should apply to Google AI once the proxy behavior is unified further. - Bedrock SSE tool-call indexing: Bedrock streaming currently increments
the OpenAI tool-call index on every Bedrock
ContentBlockStop, including text content blocks. This behavior existed before the move fromwindmill-apitowindmill-ai, but a later cleanup should advance the index only when the stopped block was a tool-use block. - Bedrock SSE keepalives: Bedrock native SSE streams are still returned
directly without the API proxy keepalive injection used by other SSE paths.
This also preserves the pre-move behavior. A later cleanup can generalize the
keepalive wrapper so it works for both
reqwest::Errorstreams and Bedrock's SDK-backedstd::io::Errorstreams.
Completed Phase: Credential Unification Phase 1 ✅
Goal: make ProviderCredentials the shared resolved runtime credential shape
without overloading it with raw resource input or model-selection state.
AIRequestConfig and ProviderWithResource are not equivalent concepts:
AIRequestConfig is API-side resolved state after DB, variable, OAuth, and
resource handling, while ProviderWithResource is worker-side raw agent input
that also carries the selected model. Keep raw/deserialization types separate and
convert them into ProviderCredentials at execution boundaries.
Suggested PR title: refactor(ai): use provider credentials for worker builders.
Scope:
- Add a worker-side conversion from
ProviderWithResourcetoProviderCredentials. - Keep
modeloutsideProviderCredentials; it remains agent request data. - Keep
ProviderWithResourceas the backward-compatible deserialization type for existing agent payloads. - Use
ProviderCredentialsfor worker query-builder creation. - Collapse
create_query_builderandcreate_proxy_query_builderinto onecreate_query_builder(&ProviderCredentials)factory.
Out of scope:
- Do not remove API-local
AIRequestConfigyet. - Do not change API request-cache behavior.
- Do not change worker agent payload shape or serialized field names.
Validation:
cargo check -p windmill-ai -p windmill-api -p windmill-workercargo check -p windmill-ai -p windmill-api -p windmill-worker --features bedrock
Completed Phase: Credential Unification Phase 2 ✅
Goal: remove the API-local resolved credential wrapper after worker execution already uses the shared shape.
Suggested PR title: refactor(ai): resolve api proxy credentials directly.
Scope:
- Change API credential resolution to return
ProviderCredentialsdirectly. - Replace
ExpiringAIRequestConfigwith an expiringProviderCredentialscache entry. - Remove
AIRequestConfig::into_provider_credentials. - Delete
AIRequestConfigentirely if no API-only behavior remains.
Out of scope:
- Do not merge raw worker resource input into
ProviderCredentials. - Do not put model selection into
ProviderCredentials.
Validation:
cargo check -p windmill-ai -p windmill-api -p windmill-workercargo check -p windmill-ai -p windmill-api -p windmill-worker --features bedrockcargo test -p windmill-api invalidates_all_cached_providers_for_workspace
Step-by-Step Plan
Each step produces a compiling, working backend.
Step 1: Create windmill-ai crate, move base types from windmill-common ✅
Create backend/windmill-ai/Cargo.toml and backend/windmill-ai/src/lib.rs.
Move from windmill-common/src/ to windmill-ai/src/:
ai_types.rs— OpenAI-compatible message typesai_providers.rs—AIProviderenum,AIPlatform, base URLs,ProviderConfigai_google.rs— Gemini types and OpenAI↔Gemini conversionai_bedrock.rs— Bedrock SDK wrapper (feature-gated onbedrock)ai_cache.rs— instance AI config revision tracking
Update all imports (windmill_common::ai_* → windmill_ai::ai_*).
Step 2: Move worker AI types to windmill-ai ✅
Move from windmill-worker/src/ai/types.rs to windmill-ai/src/types.rs:
ProviderWithResource,ProviderResource— credential typesTokenUsage— token usage trackingOutputType,SchemaType,AdditionalProperties— output configurationOpenAPISchema— tool parameter schema (depends onwindmill-parser::Typ)Tool,Message,ResponseFormat,JsonSchemaFormat— agent typesStreamingEvent— SSE event enumAIAgentArgs,AIAgentArgsRaw,AIAgentResult— agent job argsMemory— agent memory enumS3ObjectWithType— S3 image typeMcpToolSourcestub (with same#[cfg(feature = "mcp")]pattern)
Worker ai/types.rs becomes a re-export: pub use windmill_ai::types::*.
Step 3: Move QueryBuilder trait, ParsedResponse, and StreamEventSink abstraction to windmill-ai ✅
Move from windmill-worker/src/ai/query_builder.rs to windmill-ai/src/query_builder.rs:
BuildRequestArgsstructParsedResponseenumQueryBuildertrait (with all existing methods)
New StreamEventSink trait in windmill-ai:
#[async_trait]
pub trait StreamEventSink: Send + Sync {
async fn send(&self, event: StreamingEvent, events_str: &mut String) -> Result<(), Error>;
}
StreamEventSink abstracts the worker's StreamEventProcessor so windmill-ai doesn't depend on windmill-queue or the worker's job logger. The worker's StreamEventProcessor implements StreamEventSink. All provider parse_streaming_response methods and SSE parsers accept Box<dyn StreamEventSink>.
Step 4: Move SSE parsers to windmill-ai ✅
Move from windmill-worker/src/ai/sse.rs to windmill-ai/src/sse.rs:
SSEParsertraitOpenAISSEParser,AnthropicSSEParser,GeminiSSEParser,OpenAIResponsesSSEParser- All associated types (delta types, usage types, etc.)
Step 5: Move provider implementations to windmill-ai ✅
Move from windmill-worker/src/ai/providers/ to windmill-ai/src/providers/:
anthropic.rs—AnthropicQueryBuilderopenai.rs—OpenAIQueryBuildergoogle_ai.rs—GoogleAIQueryBuilderbedrock.rs—BedrockQueryBuilder(feature-gated)other.rs—OtherQueryBuilder(Mistral, DeepSeek, Groq, TogetherAI, CustomAI)openrouter.rs—OpenRouterQueryBuildermod.rswithcreate_query_builderfactory
Move utility functions providers depend on:
should_use_structured_output_tool(fromutils.rs)extract_text_content(fromutils.rs)
Step 6: Move image_handler to windmill-ai ✅
Move from windmill-worker/src/ai/image_handler.rs to windmill-ai/src/image_handler.rs:
download_and_encode_s3_image— no signature change neededprepare_messages_for_api— no signature change neededupload_image_to_s3— refactor:(base64_image, workspace_id, job_id, client)instead of(base64_image, &MiniPulledJob, client)to remove windmill-queue dependency
Step 7: Move shared utilities to windmill-ai ✅
Move AI_HTTP_HEADERS lazy_static (currently duplicated in windmill-api/src/ai.rs and windmill-worker/src/ai_executor.rs) to windmill_ai::utils. Both consumers import from windmill-ai.
Step 8: Add API proxy execution support to windmill-ai ✅
This is the key proxy unification step. HTTP-forwarding providers use
QueryBuilder::build_proxy_request:
/// Build a request from a raw OpenAI-format proxy request.
/// Used by the API chat proxy. Handles format conversion for non-OpenAI providers.
fn build_proxy_request(
&self,
args: &ProxyBuildArgs<'_>,
) -> Result<ProxyRequest, Error>;
Where ProxyBuildArgs carries the API proxy context that provider implementations need:
pub struct ProxyBuildArgs<'a> {
pub method: &'a http::Method,
pub path: &'a str,
pub headers: &'a http::HeaderMap,
pub body: &'a [u8],
pub credentials: &'a ProviderCredentials,
}
And ProxyRequest contains the transformed request:
pub struct ProxyRequest {
pub method: http::Method,
pub url: String,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
Provider implementations:
- OpenAI-compatible (OpenAI, Mistral, DeepSeek, Groq, TogetherAI, CustomAI, OpenRouter): Minimal transformation — pass body through, build URL and auth headers.
- Anthropic: Handle standard vs Vertex AI. For Vertex: transform body (extract model, add anthropic_version). For standard: pass through with appropriate headers.
- Google AI: Native execution mode converts OpenAI format → Gemini format and Gemini responses → OpenAI shape. Replaces
windmill-api/src/google.rs. - Bedrock: Native execution mode converts OpenAI format → Bedrock SDK calls and SDK responses → OpenAI shape. Replaces
windmill-api/src/bedrock.rs.
Refactor API proxy (windmill-api/src/ai.rs):
- Parse provider from headers, resolve credentials →
ProviderCredentials - Create
QueryBuilderviacreate_query_builder - Dispatch by
ProxyExecutionMode:- HTTP-forwarding providers call
query_builder.build_proxy_request(&proxy_args)→ProxyRequest - Google AI and Bedrock call native handlers in
windmill-ai
- HTTP-forwarding providers call
- Convert the provider response to the API response body
Remove from windmill-api:
AIRequestConfig::prepare_request— replaced byQueryBuilder::build_proxy_requestgoogle.rs— replaced bywindmill_ai::providers::google_ainative proxy handlersbedrock.rs— replaced bywindmill_ai::providers::bedrocknative proxy handlerstransform_anthropic_for_vertex— moved toAnthropicQueryBuildersupports_native_fim,transform_fim_to_chat_completions— moved to windmill-ai
Keep in API:
- credential resolution from DB, workspace settings, instance settings, variables, and OAuth into
ProviderCredentials - HTTP routes, audit logging, request caching
inject_keepalives,is_sse_responsehelpersAIConfig,ExpiringProviderCredentialscaching types
Step 9: Unify credential resolution
Make ProviderCredentials the single resolved runtime credential shape in
windmill-ai, while keeping raw API and worker input/deserialization types at
their boundaries.
The API's resolve_provider_credentials resolves credentials from DB, workspace
or instance settings, variables, and OAuth. The worker's ProviderWithResource
gets raw credentials from the flow module definition and also carries the
selected model. Convert both paths into ProviderCredentials; do not make
ProviderCredentials carry raw resource state or the model.
Extend windmill_ai::proxy::ProviderCredentials as needed so both can produce it:
pub struct ProviderCredentials {
pub provider: AIProvider,
pub base_url: String,
pub api_key: Option<String>,
pub access_token: Option<String>,
pub organization_id: Option<String>,
pub user: Option<String>,
pub platform: AIPlatform,
pub region: Option<String>,
pub aws_access_key_id: Option<String>,
pub aws_secret_access_key: Option<String>,
pub aws_session_token: Option<String>,
pub enable_1m_context: bool,
pub custom_headers: HashMap<String, String>,
}
The create_query_builder factory takes &ProviderCredentials instead of &ProviderWithResource.
Final Crate Structure
windmill-ai/src/
├── lib.rs # module exports
├── ai_types.rs # OpenAI-compatible message types
├── ai_providers.rs # AIProvider enum, base URLs, config
├── ai_google.rs # Gemini types and conversions
├── ai_bedrock.rs # Bedrock SDK wrapper (feature: bedrock)
├── ai_cache.rs # Instance AI config revision
├── types.rs # TokenUsage, Tool, OpenAPISchema, etc.
├── proxy.rs # ProviderCredentials, ProxyBuildArgs, ProxyRequest
├── query_builder.rs # QueryBuilder trait, BuildRequestArgs, ParsedResponse, StreamEventSink
├── sse.rs # SSE parsers (OpenAI, Anthropic, Gemini, Responses)
├── image_handler.rs # S3 image upload/download
├── utils.rs # extract_text_content, should_use_structured_output_tool
└── providers/
├── mod.rs # create_query_builder factory
├── anthropic.rs # build_request + build_proxy_request
├── openai.rs # build_request + build_proxy_request
├── google_ai.rs # build_request + native proxy handlers
├── bedrock.rs # build_request + native proxy handlers (feature: bedrock)
├── other.rs # build_request + build_proxy_request
└── openrouter.rs # build_request + build_proxy_request
windmill-worker keeps: ai_executor.rs, ai/tools.rs, ai/utils.rs (flow/conversation/MCP logic), StreamEventProcessor (impl of StreamEventSink).
windmill-api keeps: HTTP routes (ai.rs proxy endpoints), audit logging, caching, credential resolution from DB. google.rs and bedrock.rs deleted.