Files
windmill/docs/windmill-ai-refactor-plan.md
T
centdixandClaude Opus 4.6 26a6d1e4ce refactor: create windmill-ai crate (part 1 — types, traits, base modules) (#8530)
* refactor: create windmill-ai crate and move base AI types from windmill-common

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: move worker AI types to windmill-ai crate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: move QueryBuilder trait and StreamEventSink abstraction to windmill-ai

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add base64 dependency to windmill-ai for bedrock PDF support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add windmill-ai refactor plan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: address PR review — remove dead bedrock feature, add boxed_sink helper, move plan to docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 11:20:27 +00:00

231 lines
9.9 KiB
Markdown

# 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 (`ai.rs`, `google.rs`, `bedrock.rs`) with its own request building for Google/Bedrock, plus `AIRequestConfig::prepare_request` for auth/URL handling
- **windmill-worker** — agent execution (`ai/` module) with `QueryBuilder` trait, SSE parsers, provider implementations
The goal: a single `windmill-ai` crate with all AI provider logic. Both the API proxy and worker agent use `QueryBuilder` for every provider — no more duplicate logic.
## 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.
## 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 types
- `ai_providers.rs``AIProvider` enum, `AIPlatform`, base URLs, `ProviderConfig`
- `ai_google.rs` — Gemini types and OpenAI↔Gemini conversion
- `ai_bedrock.rs` — Bedrock SDK wrapper (feature-gated on `bedrock`)
- `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 types
- `TokenUsage` — token usage tracking
- `OutputType`, `SchemaType`, `AdditionalProperties` — output configuration
- `OpenAPISchema` — tool parameter schema (depends on `windmill-parser::Typ`)
- `Tool`, `Message`, `ResponseFormat`, `JsonSchemaFormat` — agent types
- `StreamingEvent` — SSE event enum
- `AIAgentArgs`, `AIAgentArgsRaw`, `AIAgentResult` — agent job args
- `Memory` — agent memory enum
- `S3ObjectWithType` — S3 image type
- `McpToolSource` stub (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`:
- `BuildRequestArgs` struct
- `ParsedResponse` enum
- `QueryBuilder` trait (with all existing methods)
New `StreamEventSink` trait in windmill-ai:
```rust
#[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`:
- `SSEParser` trait
- `OpenAISSEParser`, `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``AnthropicQueryBuilder`
- `openai.rs``OpenAIQueryBuilder`
- `google_ai.rs``GoogleAIQueryBuilder`
- `bedrock.rs``BedrockQueryBuilder` (feature-gated)
- `other.rs``OtherQueryBuilder` (Mistral, DeepSeek, Groq, TogetherAI, CustomAI)
- `openrouter.rs``OpenRouterQueryBuilder`
- `mod.rs` with `create_query_builder` factory
Move utility functions providers depend on:
- `should_use_structured_output_tool` (from `utils.rs`)
- `extract_text_content` (from `utils.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 needed
- `prepare_messages_for_api` — no signature change needed
- `upload_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 proxy support to QueryBuilder — API uses QueryBuilder for all providers
This is the key unification step. Add a new method to the `QueryBuilder` trait:
```rust
/// 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,
raw_body: &[u8],
path: &str,
) -> Result<ProxyRequest, Error>;
```
Where `ProxyRequest` contains the transformed body, endpoint URL, and auth headers:
```rust
pub struct ProxyRequest {
pub url: String,
pub body: Vec<u8>,
pub auth_headers: Vec<(String, String)>,
pub is_sse: bool,
}
```
**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**: Convert OpenAI format → Gemini format (using existing `ai_google` functions). Replaces `windmill-api/src/google.rs`.
- **Bedrock**: Convert OpenAI format → Bedrock SDK calls. Replaces `windmill-api/src/bedrock.rs`.
**Refactor API proxy** (`windmill-api/src/ai.rs`):
1. Parse provider from headers, resolve credentials → `ProviderWithResource`
2. Create `QueryBuilder` via `create_query_builder`
3. Call `query_builder.build_proxy_request(body, path)``ProxyRequest`
4. Send the request, return response with SSE keepalive injection
**Remove** from windmill-api:
- `AIRequestConfig::prepare_request` — replaced by `QueryBuilder::build_proxy_request`
- `google.rs` — replaced by `GoogleAIQueryBuilder::build_proxy_request`
- `bedrock.rs` — replaced by `BedrockQueryBuilder::build_proxy_request`
- `transform_anthropic_for_vertex` — moved to `AnthropicQueryBuilder`
- `supports_native_fim`, `transform_fim_to_chat_completions` — moved to windmill-ai
**Keep** in API:
- `AIRequestConfig::new` credential resolution (or refactor to produce `ProviderWithResource`)
- HTTP routes, audit logging, request caching
- `inject_keepalives`, `is_sse_response` helpers
- `AIConfig`, `ExpiringAIRequestConfig` caching types
---
### Step 9: Unify credential resolution
Merge `AIRequestConfig` (API-side) and `ProviderWithResource` (worker-side) into a single type in windmill-ai.
Both currently carry: api_key, base_url, region, platform, custom_headers, AWS credentials. The API's `AIRequestConfig::new` resolves credentials from DB (workspace/instance settings). The worker's `ProviderWithResource` gets credentials from the flow module definition.
Create `windmill_ai::ProviderCredentials` that both can produce:
```rust
pub struct ProviderCredentials {
pub provider: AIProvider,
pub api_key: Option<String>,
pub base_url: 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 custom_headers: HashMap<String, String>,
pub organization_id: Option<String>,
pub enable_1m_context: bool,
}
```
The `create_query_builder` factory takes `&ProviderCredentials` instead of `&ProviderWithResource`.
---
## Final Crate Structure
```
windmill-ai/src/
├── lib.rs # re-exports, AI_HTTP_HEADERS
├── 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 # ProviderCredentials, TokenUsage, Tool, OpenAPISchema, etc.
├── query_builder.rs # QueryBuilder trait, BuildRequestArgs, ParsedResponse, ProxyRequest, 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 + build_proxy_request
├── bedrock.rs # build_request + build_proxy_request (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.