Step 07 - Dynamic Model Selection
New Requirement: Smarter Decisions for High-Value Vehicles
In Step 6, you enhanced the system with multimodal image analysis, giving the workflow visual context alongside textual feedback. The system now makes well-informed disposition decisions, but there’s a catch: every decision uses the same LLM model, regardless of how much is at stake.
The Miles of Smiles management team has noticed that disposition proposals for high-value vehicles deserve more careful reasoning. A $50,000 car scrapped by mistake is far more costly than a $3,000 one. They want the system to automatically use a more capable (and more expensive) model when the stakes are high, while keeping costs down for routine decisions on lower-value vehicles.
In this step you’ll implement dynamic model selection: the DispositionProposalAgent will automatically switch to an advanced LLM when the vehicle’s estimated value exceeds $30,000.
What You’ll Learn
In this step, you will:
- Configure multiple named AI models in a single Quarkus application
- Create a
DynamicModelSelectorCDI bean that chooses a model based on runtime data - Use the
@ChatModelSupplierannotation to dynamically select a model per agent invocation - Understand how
@ModelNamequalifies named model injection in Quarkus LangChain4j - See how
@CdiBeaninjects CDI beans into a static supplier method on an agent interface
Understanding Dynamic Model Selection
Why Not Use the Same Model for Everything?
Using a single model for all decisions is simple, but it forces a trade-off between cost and quality. A cheaper model handles most routine decisions perfectly well, but may miss nuances on complex, high-stakes cases. A more capable model produces better reasoning, but running it for every request is wasteful when most cars in the fleet are low-to-mid-value vehicles.
Dynamic model selection lets you have both: route high-value decisions to a stronger model and keep everything else on the cost-effective default.
How It Works in LangChain4j
LangChain4j’s @ChatModelSupplier annotation marks a static method on an agent interface that returns the ChatModel to use for each invocation. The method receives:
- CDI beans via
@CdiBean— for accessing application services like model selectors - Agent method parameters — matched by type, giving the supplier access to the same inputs the agent receives
This means the model choice can depend on the actual data flowing through the agent at runtime, not just static configuration.
graph LR
Call["Agent method called<br/>with carValue"] --> Supplier["@ChatModelSupplier<br/>inspects carValue"]
Supplier -->|"≤ $30k"| Base["gpt-4o-mini<br/>(default model)"]
Supplier -->|"> $30k"| Advanced["gpt-4o<br/>(advanced model)"]
Base --> LLM["LLM processes<br/>disposition proposal"]
Advanced --> LLM
Prerequisites
Before starting:
- Completed Step 06 — This step builds on Step 6’s architecture
- Application from Step 06 is stopped (Ctrl+C)
- Understanding of Step 5’s disposition workflow (DispositionProposalAgent, HumanApprovalAgent)
Configure the Named Models
The first change is in application.properties. In previous steps, the application had a single unnamed model configuration that every agent shared. Now we configure two models: a default model used by all agents unless overridden, and a named advancedModel used selectively for high-value decisions.
Open src/main/resources/application.properties and update the AI model configuration:
# Default AI model configuration
quarkus.langchain4j.chat-model.provider=openai
quarkus.langchain4j.openai.api-key=${OPENAI_API_KEY}
quarkus.langchain4j.openai.chat-model.model-name=gpt-4o-mini
quarkus.langchain4j.openai.chat-model.temperature=0
quarkus.langchain4j.openai.timeout=PT180S
# Advanced AI model for high-value decisions
quarkus.langchain4j.advancedModel.chat-model.provider=openai
quarkus.langchain4j.openai.advancedModel.api-key=${OPENAI_API_KEY}
quarkus.langchain4j.openai.advancedModel.chat-model.model-name=gpt-4o
quarkus.langchain4j.openai.advancedModel.chat-model.temperature=0
quarkus.langchain4j.openai.advancedModel.timeout=PT180S
The default (unnamed) model uses gpt-4o-mini — a cost-effective model that handles routine disposition decisions well. The named advancedModel uses gpt-4o, which provides stronger reasoning for complex cases. The naming convention follows the Quarkus LangChain4j pattern: quarkus.langchain4j.openai.<modelName>.* defines a model you can later inject with @ModelName("<modelName>").
All other agents in the application (FeedbackAnalysisAgent, PricingAgent, FleetSupervisorAgent, etc.) continue to use the default model automatically — no changes needed anywhere else.
Create the DynamicModelSelector
The model selection logic lives in a dedicated CDI bean. This keeps the decision rule in one place and makes it easy to test, adjust the threshold, or add more sophisticated selection logic later.
Create src/main/java/com/carmanagement/agentic/agents/DynamicModelSelector.java:
package com.carmanagement.agentic.agents;
import dev.langchain4j.model.chat.ChatModel;
import io.quarkiverse.langchain4j.ModelName;
import io.quarkus.logging.Log;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
@Singleton
public class DynamicModelSelector {
private static final int HIGH_VALUE_THRESHOLD = 30000;
@Inject
ChatModel baseModel;
@Inject
@ModelName("advancedModel")
ChatModel advancedModel;
public ChatModel select(String carValue) {
if (parseValue(carValue) > HIGH_VALUE_THRESHOLD) {
Log.info("Selecting advanced model for high-value car estimated " + carValue);
return advancedModel;
}
return baseModel;
}
static int parseValue(String carValue) {
if (carValue == null) {
return 0;
}
String digits = carValue.replaceAll("[^0-9]", "");
if (digits.isEmpty()) {
return 0;
}
return Integer.parseInt(digits);
}
}
The selector injects two ChatModel instances. The first, with no qualifier, resolves to the default unnamed model (gpt-4o-mini). The second uses @ModelName("advancedModel") to inject the named model configuration (gpt-4o).
The select method parses the car value string (which arrives formatted as something like "$42,500") and compares it against the $30,000 threshold. Values above the threshold route to the advanced model; everything else stays on the default.
Update the DispositionProposalAgent
The DispositionProposalAgent is the agent that creates disposition proposals — SCRAP, SELL, DONATE, or KEEP — based on the vehicle’s value, condition, age, and damage. It is the natural place for dynamic model selection because the quality of its reasoning directly determines whether a valuable car is handled correctly.
Open src/main/java/com/carmanagement/agentic/agents/DispositionProposalAgent.java and add the @ChatModelSupplier method:
package com.carmanagement.agentic.agents;
import dev.langchain4j.agentic.Agent;
import dev.langchain4j.agentic.declarative.ChatModelSupplier;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.agentic.runtime.CdiBean;
/**
* Agent that creates disposition proposals for vehicles requiring disposition.
* This agent analyzes the vehicle and creates a proposal that will be reviewed
* by the HumanApprovalAgent if the vehicle value exceeds the threshold.
*/
public interface DispositionProposalAgent {
@SystemMessage("""
You are a car disposition specialist for a car rental company.
Your job is to create a disposition proposal based on the car's value, condition, age, and damage.
Disposition Options:
- SCRAP: Car is beyond economical repair or has severe safety concerns
- SELL: Car has value but is aging out of the fleet or has moderate damage
- DONATE: Car has minimal value but could serve a charitable purpose
- KEEP: Car is worth keeping in the fleet
Decision Criteria:
- If estimated repair cost > 50% of car value: Consider SCRAP or SELL
- If car is over 5 years old with significant damage: SCRAP
- If car is 3-5 years old in fair condition: SELL
- If car has low value (<$5,000) but functional: DONATE
- If car is valuable and damage is minor: KEEP
Your response must include:
1. Proposed Action with unique marker: __SCRAP__ or __SELL__ or __DONATE__ or __KEEP__
2. Reasoning: Clear explanation of your recommendation
Format your response as:
Proposed Action: __[SCRAP/SELL/DONATE/KEEP]__
Reasoning: [Your detailed explanation]
CRITICAL: Use double underscores around the action (e.g., __KEEP__ not KEEP)
""")
@UserMessage("""
Create a disposition proposal for this vehicle:
- Make: {carMake}
- Model: {carModel}
- Year: {carYear}
- Car Number: {carNumber}
- Current Condition: {carCondition}
- Estimated Value: {carValue}
- Damage/Feedback: {feedback}
Provide your disposition proposal with clear reasoning.
""")
@Agent(outputKey = "dispositionProposal", description = "Creates disposition proposals for vehicles requiring disposition")
String createDispositionProposal(
String carMake,
String carModel,
Integer carYear,
Integer carNumber,
String carCondition,
String carValue,
String feedback);
@ChatModelSupplier
static ChatModel chatModel(@CdiBean DynamicModelSelector modelSelector, String carValue) {
return modelSelector.select(carValue);
}
}
The key addition is the static method at the bottom of the interface:
@ChatModelSupplier
static ChatModel chatModel(@CdiBean DynamicModelSelector modelSelector, String carValue) {
return modelSelector.select(carValue);
}
This method is called by the framework before each invocation of createDispositionProposal. It receives the DynamicModelSelector CDI bean via @CdiBean and the carValue parameter — the same carValue that was passed to the agent method. The String carValue parameter is matched by type against the agent method’s parameters, so it automatically receives the car’s estimated value.
The returned ChatModel is the model used for that specific invocation. A $10,000 car goes to gpt-4o-mini; a $45,000 car goes to gpt-4o. Every other aspect of the agent — its system message, user message template, output key — stays the same.
Try It Out
Start the Application
- Navigate to the step-07 directory:
- Start the application:
Test with a Standard-Value Vehicle
Find the Honda Civic in the Fleet Status grid and enter feedback describing severe damage:
The car was involved in a severe collision, with heavy front-end damage, rear bumper damage, and possible frame damage. It may not be safe to drive.
Click Return.
This feedback is intentionally severe so the disposition analysis is activated. The Honda Civic should still be below the $30,000 advanced-model threshold, so the DispositionProposalAgent will use the default gpt-4o-mini model. Check the application logs. You should see the disposition proposal generated without the advanced model being invoked.
Test with a High-Value Vehicle
Now try a scenario where the PricingAgent estimates a value above $30,000. Find the Mercedes-Benz C-Class in the Fleet Status grid and enter feedback describing severe damage:
The car was involved in a severe multi-car rear-end collision, with heavy damage to both the front and rear bumpers and possible structural damage.
Click Return.
This time, when the PricingAgent returns a high estimated value (above $30,000), the DispositionProposalAgent will automatically switch to the gpt-4o model. The more capable model provides stronger reasoning for the disposition decision on this valuable vehicle. In this case you should also notice that the logs indicate the advanced model was selected:
INFO [co.ca.ag.ag.DynamicModelSelector] (executor-thread-1) Selecting advanced model for high-value car estimated $30,500
Verify the Model Switch
Enable request logging to see which model is being used:
In the logs, look for the model name in the request payload. You’ll see gpt-4o-mini for low-value vehicles and gpt-4o for high-value ones.
How It All Works Together
sequenceDiagram
participant Supervisor as FleetSupervisorAgent
participant Pricing as PricingAgent
participant Supplier as @ChatModelSupplier
participant Selector as DynamicModelSelector
participant Proposal as DispositionProposalAgent
participant LLM as LLM
Supervisor->>Pricing: Estimate vehicle value
Pricing-->>Supervisor: carValue = "$45,000"
Supervisor->>Proposal: createDispositionProposal(..., carValue)
rect rgb(212, 237, 218)
Note over Proposal,Selector: Model Selection
Proposal->>Supplier: chatModel(selector, "$45,000")
Supplier->>Selector: select("$45,000")
Selector-->>Supplier: advancedModel (gpt-4o)
end
Proposal->>LLM: Generate proposal using gpt-4o
LLM-->>Proposal: Detailed disposition proposal
Proposal-->>Supervisor: dispositionProposal
Key Takeaways
- Named models in
application.propertieslet you configure multiple LLM backends in the same application, each with its own model, temperature, and timeout settings @ModelNamequalifies which named model to inject into a CDI bean@ChatModelSupplieris a static method on an agent interface that returns theChatModelfor each invocation — the framework calls it before every agent method execution@CdiBeanmakes CDI beans available inside the static supplier method, bridging the gap between the declarative agent interface and the application’s dependency injection context- Parameter matching by type gives the supplier access to the same inputs the agent receives, enabling data-driven model selection without extra plumbing
- The pattern keeps model selection logic separate from agent logic — the agent’s prompts and output handling don’t change at all
Experiment Further
1. Adjust the Threshold
Change the HIGH_VALUE_THRESHOLD in DynamicModelSelector to a lower value (e.g., 10000) and observe how more vehicles get routed to the advanced model. Watch the logs to confirm the switch.
2. Add a Third Model Tier
Configure a third named model (e.g., premiumModel using gpt-5.1) for vehicles above $100,000 and update the selector to use a three-tier selection:
quarkus.langchain4j.premiumModel.chat-model.provider=openai
quarkus.langchain4j.openai.premiumModel.chat-model.model-name=gpt-5.1
quarkus.langchain4j.openai.premiumModel.api-key=${OPENAI_API_KEY}
quarkus.langchain4j.openai.premiumModel.timeout=PT180S
3. Apply Dynamic Selection to Other Agents
Try adding @ChatModelSupplier to other agents in the system. For example, the FleetSupervisorAgent could use a more capable model when the overall disposition involves multiple damaged vehicles.
4. Make the Threshold Configurable
Replace the hardcoded HIGH_VALUE_THRESHOLD with a Quarkus configuration property using @ConfigProperty, so it can be tuned without recompilation:
@ConfigProperty(name = "car-management.advanced-model.threshold", defaultValue = "30000")
int highValueThreshold;
Troubleshooting
Bean resolution error for advancedModel
If you see UnsatisfiedResolutionException for the @ModelName("advancedModel") injection, verify that the named model is fully configured in application.properties. The name in @ModelName("advancedModel") must match the segment in quarkus.langchain4j.openai.advancedModel.* exactly.
All requests using the same model
If you see the same model in all requests regardless of car value, check that:
- The
carValueparameter reaching the agent is correctly formatted (e.g.,"$42,500") - The
parseValuemethod can extract digits from the format used by your PricingAgent - The threshold in
DynamicModelSelectormatches your test data
ChatModelSupplier method not being called
The @ChatModelSupplier method must be static and declared directly on the agent interface. Verify the method signature returns ChatModel and that the @CdiBean and parameter types match what’s available.
Cleanup
Before moving to the next step:
-
Stop the running server by pressing
Ctrl+Cin the terminal where Quarkus is running -
Return to the root project directory:
What’s Next?
You’ve successfully implemented dynamic model selection, giving the system cost-effective reasoning for routine decisions and more capable analysis for high-value vehicles.
The system now:
- Configures multiple named AI models in a single application
- Automatically selects the appropriate model based on the vehicle’s estimated value
- Uses a stronger model for high-value disposition decisions without changing agent prompts or workflow structure
Key Progression:
- Step 4: Sophisticated local orchestration with Supervisor Pattern
- Step 5: Human-in-the-Loop for safe, controlled autonomous decisions
- Step 6: Multimodal image analysis for enriched feedback
- Step 7: Dynamic model selection for cost-effective, risk-aware decisions
In Step 08, you’ll learn about Agent-to-Agent (A2A) communication — converting the local PricingAgent into a remote service that runs in a separate system, demonstrating how to distribute agent workloads across multiple applications!