Skip to content

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 DynamicModelSelector CDI bean that chooses a model based on runtime data
  • Use the @ChatModelSupplier annotation to dynamically select a model per agent invocation
  • Understand how @ModelName qualifies named model injection in Quarkus LangChain4j
  • See how @CdiBean injects 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
Hold "Alt" / "Option" to enable pan & zoom

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:

application.properties
# 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:

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:

DispositionProposalAgent.java
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

  1. Navigate to the step-07 directory:
cd section-2/step-07
  1. Start the application:
./mvnw quarkus:dev
mvnw quarkus:dev
  1. Open http://localhost:8080

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:

quarkus.langchain4j.log-requests=true

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
Hold "Alt" / "Option" to enable pan & zoom

Key Takeaways

  • Named models in application.properties let you configure multiple LLM backends in the same application, each with its own model, temperature, and timeout settings
  • @ModelName qualifies which named model to inject into a CDI bean
  • @ChatModelSupplier is a static method on an agent interface that returns the ChatModel for each invocation — the framework calls it before every agent method execution
  • @CdiBean makes 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 carValue parameter reaching the agent is correctly formatted (e.g., "$42,500")
  • The parseValue method can extract digits from the format used by your PricingAgent
  • The threshold in DynamicModelSelector matches 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:

  1. Stop the running server by pressing Ctrl+C in the terminal where Quarkus is running

  2. Return to the root project directory:

    cd ..
    

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!

Continue to Step 08 - Using Remote Agents (A2A)