# Design Decisions — langchain-pollinations

## 1) Minimal Public Interface

- **Reduced Surface Area**: The library exposes a deliberately small set of entry points in `__init__.py` via `__all__`: the four main classes (`ChatPollinations`, `ImagePollinations`, `AccountInformation`, `ModelInformation`), the typed account structures (`AccountProfile`, `AccountTier`, `AccountUsageRecord`, `AccountUsageResponse`), and a single exception class (`PollinationsAPIError`). Account types are exported specifically to enable downstream type annotations on responses without requiring internal imports.
- **Complexity Hiding**: Internal modules (`_client`, `_auth`, `_sse`, `_openai_compat`, `_errors`) handle the complexity of transport, authentication, content normalization, and error parsing. They are invisible to the end user by naming convention (underscore prefix) and are absent from `__all__`.
- **Exception Hierarchy**: A two-level hierarchy is defined. `PollinationsError` (inherits `RuntimeError`) serves as the package base. `PollinationsAPIError` (slots `dataclass`) inherits from it and carries all structured fields parsed from the backend JSON error envelope: `status_code`, `message`, `body`, `error_code`, `request_id`, `timestamp`, `details`, and `cause`. This isolates consumers from `httpx` internals entirely.

## 2) Deep LangChain Integration (Chat)

- **Native Compatibility**: `ChatPollinations` inherits from `BaseChatModel`, implementing `_generate`, `_stream`, `_agenerate`, and `_astream`. This gives it full compatibility with all LangChain composition patterns: `invoke`, `stream`, `ainvoke`, `astream`, `batch`, `abatch`, `bind_tools`, and `with_structured_output`.
- **Expanded Multimodal Normalization**: `_openai_compat.py` normalizes all multimodal input variants before they reach the API. The supported content block types are: `text`, `image_url`, `input_audio`, `video_url`, `file`, `thinking`, and `redacted_thinking`. Each has a dedicated normalizer (`_normalize_input_audio_part`, `_normalize_video_url_part`, `_normalize_file_part`) that accepts both the canonical nested form and flat variants from external SDKs. The central dispatcher `_normalize_content_part` also strips the `id` field universally, as the Pollinations gateway rejects it inside `MessageContentPart`.
- **Reasoning Model Support**: Explicit support is built in for thinking blocks (`thinking` and `redacted_thinking`). These are preserved or filtered depending on configuration, allowing access to the reasoning chain of advanced models. The `ChatPollinationsConfig` exposes three independent reasoning controls: `thinking` (`ThinkingConfig`), `reasoning_effort` (`ReasoningEffort` literal), and `thinking_budget` (integer token cap), each targeting different model families.
- **Provider-Specific Message Adaptation**: `ToolMessage` objects are serialized without the `name` field, which the Pollinations gateway rejects. `AIMessage.invalid_tool_calls` are converted and included alongside valid tool calls to preserve round-trip fidelity in multi-turn agent conversations.

## 3) Strictly Typed Configuration (Pydantic)

- **Asymmetric Extra Policies**: Configuration and input models use `extra="forbid"` (`ChatPollinationsConfig`, `ImagePromptParams`, `AccountUsageParams`, `AccountUsageDailyParams`, `AudioConfig`, `ToolFunction`). Response and output models use `extra="allow"` (`AccountProfile`, `AccountUsageRecord`, `AccountUsageResponse`, `ThinkingConfig`, `StreamOptions`). This distinction is deliberate: input strictness catches typos at construction time, while output permissiveness tolerates new fields added by the backend API without breaking validation.
- **Runtime Validation**: All parameters for both the chat and image endpoints are defined as Pydantic models. `ChatPollinationsConfig` covers the full `/v1/chat/completions` parameter surface, including annotated constraint types (`FloatTemperature`, `SeedInt`, `TopLogprobsInt`, etc.) that encode API-level bounds directly in the schema.
- **Constructor Flexibility in ChatPollinations**: `ChatPollinations` accepts `ChatPollinationsConfig` fields as direct constructor kwargs (e.g., `ChatPollinations(model="openai", temperature=0.7)`) or as a single `request_defaults=ChatPollinationsConfig(...)` argument. Mixing both raises `ValueError` immediately, preventing ambiguous configuration at instantiation time.
- **camelCase-to-snake_case Mapping**: Response models that mirror camelCase API fields (e.g., `AccountProfile`) use `Field(alias=...)` combined with `populate_by_name=True`. This gives Python consumers idiomatic attribute names (`github_username`, `created_at`, `next_reset_at`) while remaining compatible with the raw API JSON via `model_validate`.

## 4) Image Generation: Fluent Interface

- **Immutability and Composition**: `ImagePollinations` implements a fluent interface via `with_params()`. The method serializes the current instance with `model_dump(by_alias=True)`, merges the provided overrides, re-injects `api_key` (which is excluded from the dump because `exclude=True`), and constructs a new instance. The original is never mutated. This makes it straightforward to create families of specialized generators from a shared base configuration.
- **Query Parameter Abstraction**: `ImagePromptParams` validates and serializes the full set of URL query parameters before each request. Its `to_query()` method calls `model_dump(by_alias=True, exclude_none=True)`, ensuring only explicitly set values are sent to the API and that the `aspectRatio` camelCase alias is applied correctly.
- **LangChain Runnable Compatibility**: `ImagePollinations` implements `invoke` and `ainvoke` as thin wrappers over `generate` and `agenerate`, making it composable in LangChain pipelines and LangGraph graphs without inheriting from `BaseTool` or any other LangChain base class.

## 5) Dynamic Model Catalogs

- **Lazy Remote Loading**: Both `chat.py` and `image.py` maintain a module-level model ID cache, initialized from a static fallback list (`_FALLBACK_TEXT_MODEL_IDS`, `_FALLBACK_IMAGE_MODEL_IDS`). On the first instantiation of `ChatPollinations` or `ImagePollinations` in a process, the corresponding `_load_xxx_model_ids()` function fetches the live catalog from the API (`/text/models` or `/image/models`) and updates the cache in place.
- **Thread Safety via Double-Checked Locking**: The load function uses a module-level `threading.Lock` and a boolean guard (`_xxx_model_ids_loaded`). A first check outside the lock handles the common case (already loaded) without contention. A second check inside the lock resolves the race condition between threads that pass the first check concurrently.
- **Silent Failure with Fallback**: If the API call fails for any reason (network error, invalid key, empty response), the cache retains its current value and `_xxx_model_ids_loaded` is set to `True` anyway, preventing repeated failed attempts on every instantiation. A `force=True` parameter allows explicit catalog refreshes at runtime.
- **Non-Blocking Validation**: The `model` field validator emits a `UserWarning` when the provided model ID is absent from the loaded catalog, but never blocks the request. This allows newly added models to function immediately without requiring a library update.
- **Circular Import Prevention**: The `ModelInformation` import inside `_load_xxx_model_ids()` is local (deferred) rather than at module level. Since `chat.py`, `image.py`, and `models.py` all reside in the same package, a top-level cross-import between them would create a circular dependency during package initialization.

## 6) Robust and Centralized HTTP Transport

- **Unified Client**: `PollinationsHttpClient` centralizes all network logic, managing both synchronous (`httpx.Client`) and asynchronous (`httpx.AsyncClient`) connections. Both clients are instantiated in `__init__` with their respective event hook sets and share the same timeout and base URL configuration from `HttpConfig`.
- **Structured Error Parsing**: The free function `_parse_error_response` inspects the `Content-Type` header before attempting JSON deserialization. For JSON responses, it traverses the `{status, success, error: {code, message, requestId, timestamp, details, cause}}` envelope and populates `PollinationsAPIError` with all available structured fields. Non-JSON error bodies are surfaced as plain message text. This means consumers always receive a uniformly structured exception regardless of the API's response format.
- **Credential Safety in Logging**: HTTP debug logging is activated by the `POLLINATIONS_HTTP_DEBUG` environment variable. All log hooks (`_log_request`, `_log_response_sync`, and async variants) replace the `Authorization` header value with `"Bearer ***REDACTED***"` before writing, preventing API key leakage in debug output or log aggregation systems.
- **Streaming Context Managers**: `stream_post_json` and `astream_post_json` return `httpx` stream context managers rather than full responses. This allows `ChatPollinations._stream` and `_astream` to consume the response body incrementally without buffering the entire payload.

## 7) SSE Streaming — Two-Tier Architecture

- **Batch Parser (`_sse.py`)**: `iter_sse_events_from_text` is a minimalist parser for complete SSE text blocks. It splits on `\n\n`, extracts `data:` lines, and yields `SSEEvent` dataclasses. It is used in contexts where the full response body is available as a string.
- **Real-Time Inline Parsers (`chat.py`)**: `_iter_sse_json_events_sync` and `_iter_sse_json_events_async` operate directly on `httpx` response iterators (`iter_lines()` / `aiter_lines()`). They parse JSON from each `data:` line as it arrives, yielding dicts immediately. This separation keeps the `_sse.py` module general-purpose while giving the chat streaming path full control over low-latency incremental parsing.
- **Complex Delta Reconstruction**: `_delta_content_from_delta_dict` handles three content shapes from streaming deltas: a plain `content` string, a `content_blocks` list (for reasoning-capable models that emit `thinking` and `text` blocks interleaved), and `audio.transcript` for audio-generating models. `_tool_call_chunks_from_delta` reconstructs `ToolCallChunk` objects from fragmented `index`/`id`/`function.name`/`function.arguments` delta fields.
- **Multimodal Delta Preservation**: The `preserve_multimodal_deltas` constructor flag on `ChatPollinations` controls whether rich multimodal content (audio data, image data) is retained in streaming chunks. When enabled, `AIMessageChunk.additional_kwargs` carries the same audio and content block metadata as the non-streaming path.

## 8) Adaptive Tool Calling

- **Multi-Strategy Schema Inference**: `tool_to_openai_tool()` in `_openai_compat.py` applies a prioritized fallback chain to convert any tool definition to the `{type: "function", function: {name, description, parameters}}` schema: (1) pass-through for dicts already in OpenAI format; (2) LangChain's `convert_to_openai_tool` (primary route, handles most cases); (3) `args_schema.model_json_schema()`; (4) `tool_call_schema.model_json_schema()`; (5) `get_input_schema()`; (6) `BaseModel.model_json_schema()`; (7) `TypeAdapter(tool).json_schema()` for `TypedDict` and other typing constructs. An empty schema guard (`_is_schema_empty`) prevents promoting a vacuous schema from an earlier step when a later step could produce a meaningful one.
- **Builtin Platform Tools as Pass-Through**: Provider-native tools (`google_search`, `code_execution`, `google_maps`, `url_context`, `computer_use`, `file_search`) are passed as raw dicts and returned unmodified by `tool_to_openai_tool`. `bind_tools` forwards them directly into the `tools` list in the request payload. `BuiltinToolType` is defined as a `Literal` union to provide editor completion while keeping the schema open.
- **Bidirectional Conversion**: `_lc_tool_call_to_openai_tool_call` handles the reverse path, converting LangChain tool call structures back to OpenAI format for `AIMessage` serialization in multi-turn conversations. Both `tool_calls` and `invalid_tool_calls` are converted and included, preserving all call metadata through agent loops.
- **Provider-Specific Serialization Rules**: `ToolMessage` is serialized with `_extract_text_from_parts` to guarantee its `content` is always a plain string, and without the `name` field. Both constraints reflect documented incompatibilities of the Pollinations gateway that differ from the standard OpenAI behavior.

## 9) Typed Account Information

- **Structured Response Models**: `get_profile()` and `aget_profile()` return a validated `AccountProfile` Pydantic instance instead of a raw dict. `get_usage()` and `aget_usage()` return `AccountUsageResponse` (which wraps `list[AccountUsageRecord]` and `count`) for JSON format, or a plain `str` for CSV. This gives callers type-checked access to usage records with full IDE completion, while the CSV path retains the raw string for downstream processing (e.g., pandas ingestion).
- **Content-Type-Driven Parsing**: The static method `_parse_response` in `AccountInformation` inspects the `Content-Type` header to decide whether to call `.json()` or `.text()`. This makes the account client robust to format changes and allows both JSON and CSV to be requested through the same method interface by varying `AccountUsageParams.format`.
- **Open Float Types for Token Counts**: `AccountUsageRecord` declares all token count fields as `float` rather than `int`. The Pollinations API spec defines them as JSON `number`, which is technically a float in JSON Schema. Using `float` prevents silent coercion errors and remains logically correct since the values are always whole numbers in practice.
