1515 */
1616package com.embabel.agent.config.models.openai
1717
18-
1918import com.embabel.agent.api.models.OpenAiModels
2019import com.embabel.agent.openai.OpenAiCompatibleModelFactory
2120import com.embabel.agent.spi.common.RetryProperties
21+ import com.embabel.common.ai.autoconfig.LlmAutoConfigMetadataLoader
2222import com.embabel.common.ai.model.*
2323import com.embabel.common.util.ExcludeFromJacocoGeneratedReport
2424import com.embabel.common.util.loggerFor
2525import io.micrometer.observation.ObservationRegistry
26+ import jakarta.annotation.PostConstruct
2627import org.springframework.ai.openai.OpenAiChatOptions
2728import org.springframework.beans.factory.ObjectProvider
2829import org.springframework.beans.factory.annotation.Value
30+ import org.springframework.beans.factory.config.ConfigurableBeanFactory
2931import org.springframework.boot.context.properties.ConfigurationProperties
30- import org.springframework.context.annotation.Bean
3132import org.springframework.context.annotation.Configuration
3233import java.time.LocalDate
3334
34-
3535/* *
3636 * Configuration properties for OpenAI model settings.
3737 * These properties can be set in application.properties/yaml using the
@@ -61,9 +61,9 @@ class OpenAiProperties : RetryProperties {
6161}
6262
6363/* *
64- * Configuration for well-known OpenAI language and embedding models.
65- * Provides bean definitions for various GPT models with their corresponding
66- * capabilities, knowledge cutoff dates, and pricing models .
64+ * Configuration for OpenAI language and embedding models.
65+ * This class dynamically loads and registers OpenAI models from YAML configuration,
66+ * similar to the Anthropic and Bedrock configuration patterns .
6767 */
6868@Configuration(proxyBeanMethods = false )
6969@ExcludeFromJacocoGeneratedReport(reason = " OpenAi configuration can't be unit tested" )
@@ -78,6 +78,8 @@ class OpenAiModelsConfig(
7878 embeddingsPath : String? ,
7979 observationRegistry : ObjectProvider <ObservationRegistry >,
8080 private val properties : OpenAiProperties ,
81+ private val configurableBeanFactory : ConfigurableBeanFactory ,
82+ private val modelLoader : LlmAutoConfigMetadataLoader <OpenAiModelDefinitions > = OpenAiModelLoader (),
8183) : OpenAiCompatibleModelFactory(
8284 baseUrl = baseUrl,
8385 apiKey = apiKey,
@@ -87,105 +89,100 @@ class OpenAiModelsConfig(
8789) {
8890
8991 init {
90- logger.info(" Open AI models are available: {}" , properties)
92+ logger.info(" OpenAI models are available: {}" , properties)
9193 }
9294
93- @Bean
94- fun gpt5 (): Llm {
95- return openAiCompatibleLlm(
96- model = OpenAiModels .GPT_5 ,
97- provider = OpenAiModels .PROVIDER ,
98- knowledgeCutoffDate = LocalDate .of(2024 , 10 , 1 ),
99- pricingModel = PerTokenPricingModel (
100- usdPer1mInputTokens = 1.25 ,
101- usdPer1mOutputTokens = 10.0 ,
102- ),
103- retryTemplate = properties.retryTemplate(OpenAiModels .GPT_5 ),
104- optionsConverter = Gpt5ChatOptionsConverter ,
105- )
106- }
95+ @PostConstruct
96+ fun registerModelBeans () {
97+ val definitions = modelLoader.loadAutoConfigMetadata()
98+
99+ // Register LLM models
100+ definitions.models.forEach { modelDef ->
101+ try {
102+ val llm = createOpenAiLlm(modelDef)
103+ configurableBeanFactory.registerSingleton(modelDef.name, llm)
104+ logger.info(
105+ " Registered OpenAI model bean: {} -> {}" ,
106+ modelDef.name, modelDef.modelId
107+ )
108+ } catch (e: Exception ) {
109+ logger.error(
110+ " Failed to create model: {} ({})" ,
111+ modelDef.name, modelDef.modelId, e
112+ )
113+ throw e
114+ }
115+ }
107116
108- @Bean
109- fun gpt5mini (): Llm {
110- return openAiCompatibleLlm(
111- model = OpenAiModels .GPT_5_MINI ,
112- provider = OpenAiModels .PROVIDER ,
113- knowledgeCutoffDate = LocalDate .of(2024 , 5 , 31 ),
114- pricingModel = PerTokenPricingModel (
115- usdPer1mInputTokens = .25 ,
116- usdPer1mOutputTokens = 2.0 ,
117- ),
118- retryTemplate = properties.retryTemplate(OpenAiModels .GPT_5_MINI ),
119- optionsConverter = Gpt5ChatOptionsConverter ,
120- )
117+ // Register embedding models
118+ definitions.embeddingModels.forEach { embeddingDef ->
119+ try {
120+ val embeddingService = createOpenAiEmbedding(embeddingDef)
121+ configurableBeanFactory.registerSingleton(embeddingDef.name, embeddingService)
122+ logger.info(
123+ " Registered OpenAI embedding model bean: {} -> {}" ,
124+ embeddingDef.name, embeddingDef.modelId
125+ )
126+ } catch (e: Exception ) {
127+ logger.error(
128+ " Failed to create embedding model: {} ({})" ,
129+ embeddingDef.name, embeddingDef.modelId, e
130+ )
131+ throw e
132+ }
133+ }
121134 }
122135
123- @Bean
124- fun gpt5nano (): Llm {
125- return openAiCompatibleLlm(
126- model = OpenAiModels .GPT_5_NANO ,
127- provider = OpenAiModels .PROVIDER ,
128- knowledgeCutoffDate = LocalDate .of(2024 , 5 , 31 ),
129- pricingModel = PerTokenPricingModel (
130- usdPer1mInputTokens = .05 ,
131- usdPer1mOutputTokens = .40 ,
132- ),
133- optionsConverter = Gpt5ChatOptionsConverter ,
134- retryTemplate = properties.retryTemplate(OpenAiModels .GPT_5_NANO ),
135- )
136- }
136+ /* *
137+ * Creates an individual OpenAI LLM from configuration.
138+ * Uses custom Llm constructor when pricing model is not available.
139+ */
140+ private fun createOpenAiLlm (modelDef : OpenAiModelDefinition ): Llm {
141+ // Determine the appropriate options converter based on model configuration
142+ val optionsConverter = if (modelDef.specialHandling?.supportsTemperature == false ) {
143+ Gpt5ChatOptionsConverter
144+ } else {
145+ StandardOpenAiOptionsConverter
146+ }
137147
138- @Bean
139- fun gpt41mini (): Llm {
140- return openAiCompatibleLlm(
141- model = OpenAiModels .GPT_41_MINI ,
142- provider = OpenAiModels .PROVIDER ,
143- knowledgeCutoffDate = LocalDate .of(2024 , 7 , 18 ),
144- pricingModel = PerTokenPricingModel (
145- usdPer1mInputTokens = .40 ,
146- usdPer1mOutputTokens = 1.6 ,
147- ),
148- retryTemplate = properties.retryTemplate(OpenAiModels .GPT_41_MINI ),
148+ val chatModel = chatModelOf(
149+ model = modelDef.modelId,
150+ retryTemplate = properties.retryTemplate(modelDef.modelId)
149151 )
150- }
151152
152- @Bean
153- fun gpt41 (): Llm {
154- return openAiCompatibleLlm(
155- model = OpenAiModels .GPT_41 ,
156- provider = OpenAiModels .PROVIDER ,
157- knowledgeCutoffDate = LocalDate .of(2024 , 8 , 6 ),
158- pricingModel = PerTokenPricingModel (
159- usdPer1mInputTokens = 2.0 ,
160- usdPer1mOutputTokens = 8.0 ,
161- ),
162- retryTemplate = properties.retryTemplate(OpenAiModels .GPT_41 ),
163- )
164- }
153+ // Create pricing model if present
154+ val pricingModel = modelDef.pricingModel?.let {
155+ PerTokenPricingModel (
156+ usdPer1mInputTokens = it.usdPer1mInputTokens,
157+ usdPer1mOutputTokens = it.usdPer1mOutputTokens,
158+ )
159+ }
165160
166- @Bean
167- fun gpt41nano (): Llm {
168- return openAiCompatibleLlm(
169- model = OpenAiModels . GPT_41_NANO ,
161+ // Use Llm constructor directly to handle nullable pricing model
162+ return Llm (
163+ name = modelDef.modelId,
164+ model = chatModel ,
170165 provider = OpenAiModels .PROVIDER ,
171- knowledgeCutoffDate = LocalDate .of(2024 , 8 , 6 ),
172- pricingModel = PerTokenPricingModel (
173- usdPer1mInputTokens = .1 ,
174- usdPer1mOutputTokens = .4 ,
175- ),
176- retryTemplate = properties.retryTemplate(OpenAiModels .GPT_41_NANO ),
166+ optionsConverter = optionsConverter,
167+ knowledgeCutoffDate = modelDef.knowledgeCutoffDate,
168+ pricingModel = pricingModel,
177169 )
178170 }
179171
180- @Bean
181- fun defaultOpenAiEmbeddingService (): EmbeddingService {
172+ /* *
173+ * Creates an embedding service from configuration.
174+ */
175+ private fun createOpenAiEmbedding (embeddingDef : OpenAiEmbeddingModelDefinition ): EmbeddingService {
182176 return openAiCompatibleEmbeddingService(
183- model = OpenAiModels . DEFAULT_TEXT_EMBEDDING_MODEL ,
177+ model = embeddingDef.modelId ,
184178 provider = OpenAiModels .PROVIDER ,
185179 )
186180 }
187181}
188182
183+ /* *
184+ * Options converter for GPT-5 models that don't support temperature adjustment.
185+ */
189186internal object Gpt5ChatOptionsConverter : OptionsConverter<OpenAiChatOptions> {
190187
191188 override fun convertOptions (options : LlmOptions ): OpenAiChatOptions {
@@ -200,9 +197,22 @@ internal object Gpt5ChatOptionsConverter : OptionsConverter<OpenAiChatOptions> {
200197 .maxTokens(options.maxTokens)
201198 .presencePenalty(options.presencePenalty)
202199 .frequencyPenalty(options.frequencyPenalty)
203- // .streamUsage(true) additional feature note
204- // .topP(options.topP)
205200 .build()
201+ }
202+ }
206203
204+ /* *
205+ * Standard options converter for OpenAI models that support all parameters.
206+ */
207+ internal object StandardOpenAiOptionsConverter : OptionsConverter<OpenAiChatOptions> {
208+
209+ override fun convertOptions (options : LlmOptions ): OpenAiChatOptions {
210+ return OpenAiChatOptions .builder()
211+ .temperature(options.temperature)
212+ .topP(options.topP)
213+ .maxTokens(options.maxTokens)
214+ .presencePenalty(options.presencePenalty)
215+ .frequencyPenalty(options.frequencyPenalty)
216+ .build()
207217 }
208218}
0 commit comments