@@ -83,6 +83,26 @@ public MessageConverter(ObjectMapper objectMapper) {
8383 * @return A Spring AI Prompt
8484 */
8585 public Prompt toLlmPrompt (LlmRequest llmRequest ) {
86+ return toLlmPrompt (llmRequest , null );
87+ }
88+
89+ /**
90+ * Converts an ADK LlmRequest to a Spring AI Prompt, using the target model's own default options
91+ * as the base for the prompt options.
92+ *
93+ * <p>Provider-specific chat models (for example Spring AI OpenAI {@code 2.0.0}) cast {@code
94+ * Prompt.getOptions()} directly to their own options type (e.g. {@code OpenAiChatOptions}) in
95+ * {@code createRequest(...)}. Passing provider-neutral options such as {@code
96+ * DefaultToolCallingChatOptions} therefore triggers a {@link ClassCastException}. To stay
97+ * compatible with any provider, the prompt options are built on top of the model's own default
98+ * options (obtained via {@code ChatModel.getOptions()}) so the resulting options keep the
99+ * concrete type the provider expects, while overlaying the ADK tools and generation config.
100+ *
101+ * @param llmRequest The ADK request to convert
102+ * @param modelDefaultOptions The target model's default options, or {@code null} if unavailable
103+ * @return A Spring AI Prompt
104+ */
105+ public Prompt toLlmPrompt (LlmRequest llmRequest , ChatOptions modelDefaultOptions ) {
86106 List <Message > messages = new ArrayList <>();
87107 List <String > allSystemMessages = new ArrayList <>();
88108
@@ -119,56 +139,90 @@ public Prompt toLlmPrompt(LlmRequest llmRequest) {
119139 // Add all non-system messages
120140 messages .addAll (nonSystemMessages );
121141
122- // Convert config to ChatOptions
123- ChatOptions chatOptions = configMapper . toSpringAiChatOptions ( llmRequest . config ());
142+ return new Prompt ( messages , buildChatOptions ( llmRequest , modelDefaultOptions ));
143+ }
124144
125- // Convert ADK tools to Spring AI ToolCallback and add to ChatOptions
145+ /**
146+ * Builds the Spring AI {@link ChatOptions} for a request by overlaying the ADK generation config
147+ * and tools on top of the model's own default options.
148+ *
149+ * @param llmRequest The ADK request being converted
150+ * @param modelDefaultOptions The target model's default options, or {@code null} if unavailable
151+ * @return The chat options to attach to the prompt, or {@code null} when there is nothing
152+ * ADK-specific to configure (letting the model apply its own defaults)
153+ */
154+ private ChatOptions buildChatOptions (LlmRequest llmRequest , ChatOptions modelDefaultOptions ) {
155+ // ADK generation config (temperature, max tokens, ...) as provider-neutral options.
156+ ChatOptions adkChatOptions = configMapper .toSpringAiChatOptions (llmRequest .config ());
157+
158+ // ADK tools converted to Spring AI tool callbacks.
159+ List <ToolCallback > toolCallbacks = List .of ();
126160 if (llmRequest .tools () != null && !llmRequest .tools ().isEmpty ()) {
127- List <ToolCallback > toolCallbacks = toolConverter .convertToSpringAiTools (llmRequest .tools ());
128- if (!toolCallbacks .isEmpty ()) {
129- // Create new ChatOptions with tools included
130- ToolCallingChatOptions .Builder optionsBuilder = ToolCallingChatOptions .builder ();
161+ toolCallbacks = toolConverter .convertToSpringAiTools (llmRequest .tools ());
162+ }
131163
132- // Always set tool callbacks
133- optionsBuilder . toolCallbacks ( toolCallbacks ) ;
164+ boolean hasTools = ! toolCallbacks . isEmpty ();
165+ boolean hasAdkConfig = adkChatOptions != null ;
134166
135- // Copy existing chat options properties if present
136- if (chatOptions != null ) {
137- // Copy all relevant properties from existing ChatOptions
138- if (chatOptions .getTemperature () != null ) {
139- optionsBuilder .temperature (chatOptions .getTemperature ());
140- }
141- if (chatOptions .getMaxTokens () != null ) {
142- optionsBuilder .maxTokens (chatOptions .getMaxTokens ());
143- }
144- if (chatOptions .getTopP () != null ) {
145- optionsBuilder .topP (chatOptions .getTopP ());
146- }
147- if (chatOptions .getTopK () != null ) {
148- optionsBuilder .topK (chatOptions .getTopK ());
149- }
150- if (chatOptions .getStopSequences () != null ) {
151- optionsBuilder .stopSequences (chatOptions .getStopSequences ());
152- }
153- // Copy model name if present
154- if (chatOptions .getModel () != null ) {
155- optionsBuilder .model (chatOptions .getModel ());
156- }
157- // Copy frequency penalty if present
158- if (chatOptions .getFrequencyPenalty () != null ) {
159- optionsBuilder .frequencyPenalty (chatOptions .getFrequencyPenalty ());
160- }
161- // Copy presence penalty if present
162- if (chatOptions .getPresencePenalty () != null ) {
163- optionsBuilder .presencePenalty (chatOptions .getPresencePenalty ());
164- }
165- }
167+ // Nothing ADK-specific to add: let the provider fall back to its own default options.
168+ if (!hasTools && !hasAdkConfig ) {
169+ return null ;
170+ }
166171
167- chatOptions = optionsBuilder .build ();
172+ // Preferred path: start from the model's own options so the resulting options keep the concrete
173+ // provider type (e.g. OpenAiChatOptions) and preserve provider-specific settings such as the
174+ // API key, base URL and model name. This avoids the ClassCastException thrown by providers
175+ // (like Spring AI OpenAI 2.0.0) that cast Prompt.getOptions() to their own options type.
176+ if (modelDefaultOptions instanceof ToolCallingChatOptions ) {
177+ ToolCallingChatOptions .Builder <?> optionsBuilder =
178+ ((ToolCallingChatOptions ) modelDefaultOptions ).mutate ();
179+ if (hasTools ) {
180+ optionsBuilder .toolCallbacks (toolCallbacks );
168181 }
182+ applyGenerationConfig (optionsBuilder , adkChatOptions );
183+ return optionsBuilder .build ();
169184 }
170185
171- return new Prompt (messages , chatOptions );
186+ // Fallback: the model's default options are unavailable or not tool-calling capable. Preserve
187+ // the provider-neutral behavior, which works for providers that normalize generic options.
188+ if (hasTools ) {
189+ ToolCallingChatOptions .Builder <?> optionsBuilder = ToolCallingChatOptions .builder ();
190+ optionsBuilder .toolCallbacks (toolCallbacks );
191+ applyGenerationConfig (optionsBuilder , adkChatOptions );
192+ return optionsBuilder .build ();
193+ }
194+ return adkChatOptions ;
195+ }
196+
197+ /** Copies the non-null generation parameters from {@code source} onto {@code builder}. */
198+ private void applyGenerationConfig (ChatOptions .Builder <?> builder , ChatOptions source ) {
199+ if (source == null ) {
200+ return ;
201+ }
202+ if (source .getTemperature () != null ) {
203+ builder .temperature (source .getTemperature ());
204+ }
205+ if (source .getMaxTokens () != null ) {
206+ builder .maxTokens (source .getMaxTokens ());
207+ }
208+ if (source .getTopP () != null ) {
209+ builder .topP (source .getTopP ());
210+ }
211+ if (source .getTopK () != null ) {
212+ builder .topK (source .getTopK ());
213+ }
214+ if (source .getStopSequences () != null ) {
215+ builder .stopSequences (source .getStopSequences ());
216+ }
217+ if (source .getModel () != null ) {
218+ builder .model (source .getModel ());
219+ }
220+ if (source .getFrequencyPenalty () != null ) {
221+ builder .frequencyPenalty (source .getFrequencyPenalty ());
222+ }
223+ if (source .getPresencePenalty () != null ) {
224+ builder .presencePenalty (source .getPresencePenalty ());
225+ }
172226 }
173227
174228 /**
0 commit comments