Skip to content

Commit 2a68147

Browse files
committed
Restored OpenAiModelsConfig.kt from main branch
1 parent d594187 commit 2a68147

File tree

2 files changed

+101
-99
lines changed
  • embabel-agent-autoconfigure/models/embabel-agent-openai-autoconfigure/src

2 files changed

+101
-99
lines changed

embabel-agent-autoconfigure/models/embabel-agent-openai-autoconfigure/src/main/kotlin/com/embabel/agent/config/models/openai/OpenAiModelsConfig.kt

Lines changed: 98 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,23 @@
1515
*/
1616
package com.embabel.agent.config.models.openai
1717

18-
1918
import com.embabel.agent.api.models.OpenAiModels
2019
import com.embabel.agent.openai.OpenAiCompatibleModelFactory
2120
import com.embabel.agent.spi.common.RetryProperties
21+
import com.embabel.common.ai.autoconfig.LlmAutoConfigMetadataLoader
2222
import com.embabel.common.ai.model.*
2323
import com.embabel.common.util.ExcludeFromJacocoGeneratedReport
2424
import com.embabel.common.util.loggerFor
2525
import io.micrometer.observation.ObservationRegistry
26+
import jakarta.annotation.PostConstruct
2627
import org.springframework.ai.openai.OpenAiChatOptions
2728
import org.springframework.beans.factory.ObjectProvider
2829
import org.springframework.beans.factory.annotation.Value
30+
import org.springframework.beans.factory.config.ConfigurableBeanFactory
2931
import org.springframework.boot.context.properties.ConfigurationProperties
30-
import org.springframework.context.annotation.Bean
3132
import org.springframework.context.annotation.Configuration
3233
import 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+
*/
189186
internal 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
}

embabel-agent-autoconfigure/models/embabel-agent-openai-autoconfigure/src/test/kotlin/com/embabel/agent/config/models/openai/LLMStreamingIT.kt

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -128,21 +128,13 @@ class LLMStreamingIT(
128128

129129
assertTrue(runner.supportsStreaming(), "Test LLM should support streaming") // ADD THIS DEBUG BLOCK:
130130

131-
132131
// When: Subscribe with real reactive callbacks
133132
val receivedEvents = mutableListOf<String>()
134133
var errorOccurred: Throwable? = null
135134
var completionCalled = false
136-
var prompt = """
137-
What is the most hottest month in Florida.
138-
minimum 2 sentences.
139-
first sentence should include month only,
140-
no other description.
141-
every sentence per new line with new-line character
142-
""".trimIndent()
143-
// above detailed prompt is not required, see format in [StreamingJacksonConverterJ
144-
prompt = """
145-
What is the most hottest month in Florida.
135+
136+
val prompt = """
137+
What are two the most hottest months in Florida.
146138
""".trimIndent()
147139

148140
val results = runner.asStreaming()

0 commit comments

Comments
 (0)