Providers
Aigentic provides a consistent interface for working with different Large Language Model (LLM) providers. This allows you to easily switch between models or use multiple models in your application.
Supported Models
Aigentic supports several LLM providers out of the box:
- OpenAI
- Gemini
- Ollama
- VertexAI
agent<String, Answer> {
openAIModel {
apiKey("your-api-key")
modelIdentifier(OpenAIModelIdentifier.GPT5_5)
}
// ...
}
agent<String, Answer> {
geminiModel {
apiKey("your-api-key")
modelIdentifier(GeminiModelIdentifier.Gemini3_5Flash)
}
// ...
}
agent<String, Answer> {
ollamaModel {
apiUrl("http://localhost:11434/v1/")
modelIdentifier(OpenAIModelIdentifier.Custom("llama3"))
generationConfig {
// Use default settings
}
}
// ...
}
agent<String, Answer> {
vertexAIModel {
project("your-project-id")
location("your-location")
modelIdentifier(VertexAIModelIdentifier.Gemini3_5Flash)
}
// ...
}
The VertexAI provider runs on the JVM only. It doesn't take an API key: it signs in with Google Application Default Credentials, for example after gcloud auth application-default login.
ollamaModel connects to http://localhost:11434/v1/ unless you set apiUrl().
Add logLevel(LogLevel.DEBUG) to geminiModel or openAIModel to log every HTTP request and response, which helps when a provider returns an error you don't understand. LogLevel is in community.flock.aigentic.core.model.
Timeouts
The Gemini provider streams every response, so data keeps arriving while the model writes a long answer. Two timeouts control how long Aigentic waits:
requestTimeout(ms): the maximum time for the whole request, including the stream. Raise it when a long structured output takes more than 5 minutes.socketTimeout(ms): the maximum time without any data. The model sends nothing while it thinks, which can take more than a minute on a high thinking level. Unless you set it, the socket timeout is the same as the request timeout.
geminiModel {
apiKey("your-api-key")
modelIdentifier(GeminiModelIdentifier.Gemini3_8Flash)
requestTimeout(10 * 60_000)
}
Without either setting, both timeouts are 5 minutes. Gemini retries a request once after a server error and up to twice after a network error or a timeout, so a run can take longer than requestTimeout. Gemini also sends at most 50 requests per minute per model and waits when it reaches that limit.
The other providers:
- VertexAI:
requestTimeout(ms), 5 minutes by default. VertexAI doesn't stream, so there's nosocketTimeout. - OpenAI and Ollama: a fixed socket timeout of 60 seconds.
When a request still fails, for example because of a timeout or a dropped connection, the run ends with Outcome.Fatal instead of throwing an exception.
Attachment resolution
Providers read every image and PDF page as a limited number of tokens. For dense documents or scans with small print, the default can be too little, and the model may misread a character, for example an 8 as a 6. Each provider has its own setting to read your files at a higher resolution, with its own levels.
Aigentic sends the setting with every file the model gets: the attachments you pass to start() and the files in the agent's context. Without the setting, the provider's default stays.
- Gemini
- VertexAI
- OpenAI
import community.flock.aigentic.gemini.model.GeminiMediaResolution
agent<String, Answer> {
geminiModel {
apiKey("your-api-key")
modelIdentifier(GeminiModelIdentifier.Gemini3_8Flash)
mediaResolution(GeminiMediaResolution.HIGH)
}
// ...
}
By default, Gemini reads an image at 1120 tokens and a PDF page at 560.
GeminiMediaResolution | Tokens per image | Tokens per PDF page |
|---|---|---|
LOW | 280 | 280 |
MEDIUM | 560 | 560 |
HIGH | 1120 | 1120 |
ULTRA_HIGH | 2240 | 1120 (sent as HIGH) |
These are the counts Gemini documents for Gemini 3 models. A PDF page with a text layer also counts the tokens of that text. Gemini accepts ULTRA_HIGH for images only and rejects it on a PDF with 400 INVALID_ARGUMENT, so with ULTRA_HIGH a PDF attachment is sent at HIGH, the highest level Gemini accepts for it.
Gemini only supports a media resolution per file on Gemini 3 and newer models. Setting mediaResolution() on a Gemini 2.x model, or on a Custom identifier with major version 2 or lower, throws an AigenticException as soon as the model is built. A Custom identifier without a version match is passed through as-is.
import community.flock.aigentic.vertexai.VertexAIMediaResolution
agent<String, Answer> {
vertexAIModel {
project("your-project-id")
location("your-location")
modelIdentifier(VertexAIModelIdentifier.Gemini3_8Flash)
mediaResolution(VertexAIMediaResolution.HIGH)
}
// ...
}
Vertex AI runs the same Gemini models, so VertexAIMediaResolution has the same levels, token counts and model rules as Gemini.
import community.flock.aigentic.openai.model.OpenAIImageDetail
agent<String, Answer> {
openAIModel {
apiKey("your-api-key")
modelIdentifier(OpenAIModelIdentifier.GPT5_5)
imageDetail(OpenAIImageDetail.ORIGINAL)
}
// ...
}
OpenAIImageDetail | What OpenAI does |
|---|---|
AUTO | Picks the detail level itself, the same as leaving it unset |
LOW | Reads a low-resolution version, for the fewest tokens |
HIGH | Reads more detail, for small text and dense images |
ORIGINAL | Reads the image at full resolution, up to 10.24 million pixels or 6000 pixels on the longest side |
ORIGINAL is only available from gpt-5.4 on. Setting it on an older model throws an AigenticException as soon as the model is built. A Custom identifier is passed through as-is.
imageDetail() only applies to images. The OpenAI provider doesn't support PDF attachments yet, see Running an Agent with Attachments.
Ollama has no resolution setting, and like OpenAI it doesn't support PDF attachments yet.
Thinking configuration
Reasoning models let you control how much they think before answering. Aigentic exposes this through two generationConfig DSL functions: thinkingBudget(tokens) and thinkingLevel(level). They are mutually exclusive; configuring both throws an AigenticException. Leaving both unset uses the model's default behaviour.
Migrating from 0.11.0: thinkingBudget() on a Gemini or VertexAI 3.x model identifier used to be forwarded as-is; it now throws an AigenticException (use thinkingLevel() instead). thinkingBudget() on an OpenAI or Ollama model used to be silently ignored; it now throws too. Sampling defaults are no longer sent to Gemini and VertexAI 3.x models or to OpenAI reasoning models, and an explicit temperature() or topP() on an OpenAI reasoning model now throws, see Sampling parameters.
Thinking errors, like thinkingBudget() on a model that only supports thinkingLevel(), are raised as soon as the model is built, for example inside geminiModel { ... }. You get them before any request is sent, not later when the agent runs. Every ThinkingLevel works on a model that supports thinkingLevel(): a level the model doesn't have is mapped to one it does, as described below.
AigenticException is internal to Aigentic, so catch Exception if you want to handle these errors yourself.
- Gemini
- VertexAI
- OpenAI
- Ollama
import community.flock.aigentic.core.model.ThinkingLevel
agent<String, Answer> {
geminiModel {
apiKey("your-api-key")
modelIdentifier(GeminiModelIdentifier.Gemini3_5Flash)
generationConfig {
thinkingLevel(ThinkingLevel.LOW)
}
}
// ...
}
Gemini 2.x models only support thinkingBudget() (a token budget, -1 for dynamic, 0 to disable except on Pro). Gemini 3.x models only support thinkingLevel().
| Model | Default level | Supported levels |
|---|---|---|
gemini-2.5-pro / gemini-2.5-flash / gemini-2.5-flash-lite | n/a | thinkingBudget only |
gemini-3.8-flash | medium | LOW, MEDIUM, HIGH (MINIMAL maps to LOW) |
gemini-3.7-flash | medium | LOW, MEDIUM, HIGH (MINIMAL maps to LOW) |
gemini-3.6-flash | medium | MINIMAL, LOW, MEDIUM, HIGH |
gemini-3.5-flash | medium | MINIMAL, LOW, MEDIUM, HIGH |
gemini-3.5-flash-lite | minimal | MINIMAL, LOW, MEDIUM, HIGH |
gemini-3.1-pro-preview | high | LOW, MEDIUM, HIGH (MINIMAL maps to LOW) |
gemini-3.1-flash-lite | minimal | MINIMAL, LOW, MEDIUM, HIGH |
gemini-3-flash-preview | high | MINIMAL, LOW, MEDIUM, HIGH |
ThinkingLevel is an aigentic-level concept; each provider maps it to what the underlying model supports. MINIMAL maps to the lowest level the model supports: low on gemini-3.8-flash, gemini-3.7-flash and gemini-3.1-pro-preview, since those models don't have a MINIMAL level of their own.
For a Custom(identifier) model identifier, Aigentic infers the same rules from the identifier's major version number, parsed with (?:^|/)gemini-(\d+) (so "gemini-2.5-flash", "models/gemini-2.5-flash", and a full resource path like "publishers/google/models/gemini-2.5-flash" or "projects/.../locations/.../publishers/google/models/gemini-2.5-flash" all match): a major version of 2 or lower only supports thinkingBudget() (a thinkingLevel() throws), a major version of 3 or higher only supports thinkingLevel() with all four levels, MINIMAL forwarded as-is (a thinkingBudget() throws). An identifier with no version match (a tuned endpoint, a proxy name, or an experimental name like gemini-exp-1206) is passed through without validation — both thinkingBudget() and thinkingLevel() are accepted as-is.
import community.flock.aigentic.core.model.ThinkingLevel
agent<String, Answer> {
vertexAIModel {
project("your-project-id")
location("your-location")
modelIdentifier(VertexAIModelIdentifier.Gemini3_5Flash)
generationConfig {
thinkingLevel(ThinkingLevel.LOW)
}
}
// ...
}
Same rules as Gemini: 2.x models only support thinkingBudget(), 3.x models only support thinkingLevel(), using the same model → default level → supported levels table above, the same Custom(identifier) major-version rules, and the same MINIMAL-maps-to-LOW behavior on gemini-3.8-flash, gemini-3.7-flash and gemini-3.1-pro-preview.
import community.flock.aigentic.core.model.ThinkingLevel
agent<String, Answer> {
openAIModel {
apiKey("your-api-key")
modelIdentifier(OpenAIModelIdentifier.O3)
generationConfig {
thinkingLevel(ThinkingLevel.LOW)
}
}
// ...
}
thinkingLevel() maps to OpenAI's reasoning_effort request field and is only supported on reasoning models (the o-series, the gpt-5 family, and any Custom model identifier). Configuring it on a non-reasoning model, or configuring thinkingBudget() on any OpenAI-compatible model, throws an AigenticException.
MINIMAL maps to the lowest reasoning_effort value the model actually accepts, since not every model has a minimal value of its own:
| Model | Accepted reasoning_effort values | MINIMAL / LOW maps to |
|---|---|---|
gpt-5, gpt-5-mini, gpt-5-nano | minimal, low, medium, high | minimal (unchanged) |
gpt-5.4, gpt-5.4-mini, gpt-5.4-nano, gpt-5.5 | none, low, medium, high, xhigh | low (no minimal, and none isn't a ThinkingLevel) |
gpt-5.4-pro, gpt-5.5-pro | medium, high, xhigh | medium (no minimal/none/low) |
o1, o1-pro, o3, o3-pro, o3-mini, o4-mini | low, medium, high | low (no minimal) |
Custom | model-dependent | passed through as minimal |
Aigentic's ThinkingLevel enum only has MINIMAL/LOW/MEDIUM/HIGH; it does not expose OpenAI's none or xhigh values.
import community.flock.aigentic.core.model.ThinkingLevel
agent<String, Answer> {
ollamaModel {
apiUrl("http://localhost:11434/v1/")
modelIdentifier(OpenAIModelIdentifier.Custom("llama3"))
generationConfig {
thinkingLevel(ThinkingLevel.LOW)
}
}
// ...
}
Ollama models go through the OpenAI-compatible request path: thinkingLevel() maps to reasoning_effort, and thinkingBudget() always throws an AigenticException since Ollama has no budget-based thinking control.
Ollama's /v1 OpenAI-compatibility layer maps the incoming reasoning_effort onto its native think field: minimal becomes low, low/medium/high pass through as-is, xhigh becomes max, and none disables thinking (source: thinkFromReasoningEffort in openai/openai.go, ollama/ollama, since v0.11.5). Aigentic itself only ever sends minimal/low/medium/high, since Custom model identifiers (which is what all Ollama models use) pass MINIMAL through unchanged and the DSL has no none/xhigh level.
Sampling parameters
generationConfig also exposes temperature(value), topK(value) and topP(value). All three are nullable and default to unset. Reasoning models don't accept custom sampling parameters, so Aigentic only fills in a default when the underlying model actually uses one.
| Model class | Default when unset | Behavior when explicitly set |
|---|---|---|
Gemini 2.x, VertexAI 2.x, a Custom identifier with major version ≤ 2 or no version match | temperature=0.0, topK=1, topP=0.1 (plus candidate_count=1) | Sent as configured |
Gemini 3.x, VertexAI 3.x, a Custom identifier with major version ≥ 3 | Omitted from the request entirely | Sent as configured |
OpenAI/Ollama reasoning models (the o-series and the gpt-5 family only, by explicit identifier) | Omitted from the request | Throws an AigenticException when the model is built |
OpenAI non-reasoning models (gpt-4o, gpt-4.1, gpt-3.5-turbo, ...) and every Custom identifier (including Ollama) | temperature=0.0, topP=0.1 | Sent as configured |
On Gemini and VertexAI 3.x models, an explicitly set topK()/topP() is forwarded as-is and accepted by the API (200); Google recommends leaving them unset on 3.x for best reasoning quality.
topK is never sent to OpenAI-compatible models (OpenAI or Ollama), even when configured — the client has never supported it there, and it is not validated either.
Like the thinking-configuration errors above, an OpenAI reasoning model rejecting an explicit temperature()/topP() is raised client-side as soon as the model is built, not later when the agent runs.
Output length
maxOutputTokens(tokens) in generationConfig caps how many tokens the model writes per response. Leave it unset to use the provider's limit. When Gemini stops at the limit, the run ends with Outcome.Fatal and a message that tells you to raise it.
Structured output (Gemini & Vertex AI)
You don’t need structured output to use output types. With any provider you can define agent<Input, Output> and Aigentic will parse the model’s response into your Output type in the normal chat/tool‑calling flow.
Structured output is an optional mode for Gemini and Vertex AI that uses the provider’s native JSON schema support. Aigentic will use it automatically when:
- You specify an output type on your agent (e.g.,
agent<Input, Output>), and - You don’t add any tools to the agent
Why use it
- Handles complex, deeply nested objects more reliably
When not to use it
- You need tools (structured output can’t run tools as a Gemini limitation)
- Your output is simple enough for the normal flow
Troubleshooting
- If you see Gemini errors like
MALFORMED_FUNCTION_CALL, try removing tools and rely on structured output by specifying an output type.
@AigenticParameter
data class Summary(val title: String, val bullets: List<String>)
val vertexAgent = agent<String, Summary> {
vertexAIModel {
project("your-project-id")
location("your-location")
modelIdentifier(VertexAIModelIdentifier.Gemini3_5Flash)
}
task("Summarize the text as title + bullets") {
addInstruction("Return a title and bullet points")
}
// No tools added → structured output request
}
Bring your own model
All model implementations in Aigentic implement the Model interface, which defines the contract for interacting with LLM providers:
interface Model {
val modelIdentifier: ModelIdentifier
val generationSettings: GenerationSettings
val mediaResolutionLevel: String? get() = null
suspend fun sendRequest(
messages: List<Message>,
tools: List<ToolDescription>,
structuredOutputParameter: Parameter?,
): ModelResponse
}
You can implement this interface to connect to your own model. Override mediaResolutionLevel if your model reads attachments at a configurable resolution, so it's reported to the platform with each run.
GenerationSettings.temperature, topK and topP changed from Float/Int/Float to Float?/Int?/Float?. If your custom Model implementation reads these fields directly (e.g. generationSettings.temperature.toDouble()), update it to handle null — it now means "not configured" rather than a provider-specific default.