Fix OpenAI auto-config when only sub-prefix api-key is set#5990
Fix OpenAI auto-config when only sub-prefix api-key is set#5990jewoodev wants to merge 1 commit into
Conversation
5de82d6 to
73edf9f
Compare
|
|
||
| <dependency> | ||
| <groupId>uk.org.webcompere</groupId> | ||
| <artifactId>system-stubs-jupiter</artifactId> | ||
| <version>2.1.7</version> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| </dependencies> |
There was a problem hiding this comment.
Note on new test dependency (updated after a closer look)
I initially flagged this as something to bring up; after tracing the module structure I now think the dep is the right call, but I'm laying out the full rationale in case you see it differently. The argument hinges on one claim — that OpenAiSetup is intentionally framework-agnostic — so I'll lead with the evidence for that.
OpenAiSetup looks deliberately framework-agnostic
OpenAiSetup in models/spring-ai-openai:
- Has zero
org.springframework.*imports in the file. All imports arejava.*,com.openai.*,org.jspecify.*,org.slf4j.*. - JavaDoc explicitly states the pattern is borrowed from a non-Spring project — LangChain4j's
dev.langchain4j.model.openaiofficial.InternalOpenAiOfficialHelper, by the same author (Julien Dubois). - Has two call sites: the Spring Boot autoconfig path and plain Java via
OpenAiChatModel's builder. The plain-Java path has no SpringEnvironmentavailable, so env-var fallback necessarily goes through JDK-standardSystem.getenv(). - Sits on the model-library side of the Spring AI module split (
models/spring-ai-<vendor>↔auto-configurations/.../spring-ai-autoconfigure-model-<vendor>), which is where every other vendor module keeps its Spring-free helpers.
Why the condition has to mirror that
If the condition diverged — e.g. read via Spring Environment instead — setting OPENAI_API_KEY only as a Spring property (e.g. in application.yml) would make the condition match while OpenAiSetup still threw IllegalStateException: At least one credential source must be specified. That's the exact GH-1818 failure mode this PR is meant to prevent, just relocated.
Why system-stubs-jupiter
Because the condition must call System.getenv() directly, exercising its env-var fallback in OpenAiConnectionConditionAutoConfigurationTests requires mutating OS env from the test JVM. system-stubs-jupiter is the smallest JUnit 5 extension I'm aware of that does this cleanly.
Alternatives I'd accept
If a new test dep is still a blocker:
- Swap
system-stubs-jupiterforjunit-pioneer(same idea, possibly more familiar in this ecosystem). - Drop
openAiApiKeyEnvVarEnablesAllSixModelsentirely and rely on existingOpenAiSetupTestscoverage of the env-fallback branch — loses direct condition-level coverage of that path but introduces no new dependency.
Let me know which way you'd like to take it.
Add OpenAiConnectionCondition to gate each of the six OpenAI sub-config
model beans (chat, image, embedding, audio.speech, audio.transcription,
moderation) on a usable credential source: common api-key, sub-prefix
api-key, programmatic Credential object, OPENAI_API_KEY env, GITHUB_TOKEN
env, or Microsoft Foundry passwordless auth. Previously, configuring any
sub-prefix key alone (e.g. spring.ai.openai.image.api-key) registered
all sub-configs and OpenAiSetup failed fast on the unconfigured ones
with IllegalStateException("At least one credential source must be
specified: credential (apiKey), workloadIdentity, or adminApiKey"),
taking the whole context down.
The condition lives on each @bean method (not at class level) so that
@EnableConfigurationProperties and the existing model-selector
@ConditionalOnProperty remain active. Match logic uses independent
short-circuit branches inside getMatchOutcome to avoid Spring Boot's
@ConditionalOnProperty(name={a,b}) AND semantics.
Signed-off-by: jewoodev <jewoos15@naver.com>
73edf9f to
e8fde7f
Compare
|
@sdeleuze Before further review, could I get a quick design call? The PR adds Trade-off: under D, Either fixes #5989. I'd rather not pick unilaterally since this introduces a new gating primitive that stays in the codebase. If D (or a third shape I missed) fits the codebase taste better, I'll push a D-shaped commit on top of this branch for direct comparison. |
Summary
Closes #1818, closes #5989.
Why
Each of the six sub-model beans (chat / embedding / image / audio.speech / audio.transcription / moderation) is registered as soon as its
spring.ai.model.<kind>selector picksopenai, and registration runs the commonspring.ai.openai.api-keycheck unconditionally. Sub-prefix-only credentials (and other valid sources — programmaticCredential,OPENAI_API_KEY, passwordless Foundry) are never consulted at the gate, so context startup aborts even when a usable credential exists.Fix
Introduce
OpenAiConnectionConditionwith one nested class per sub-config and annotate each sub-model@Beanwith@Conditional(OpenAiConnectionCondition.X.class). EachgetMatchOutcomereturns a match if any of the relevant credential sources is present: common api-key, sub-prefix api-key, programmaticCredentialon common/sub*Properties,OPENAI_API_KEYenv var, GitHub Models token, or Microsoft Foundry passwordless auth. Conditions sit on@Beanmethods so@EnableConfigurationPropertiesand the existing@ConditionalOnProperty(spring.ai.model.<kind>)selector keep working unchanged.Tests
Added regression coverage in
OpenAiConnectionConditionAutoConfigurationTestsfor:spring.ai.model.chat=noneselector suppresses chat even when an api-key is setOpenAiChatModelbean is preserved when no api-key is setOPENAI_API_KEYenv var fallback enables all sixmicrosoft-foundry=trueenables chat without an api-keyScope
This PR covers the six OpenAI sub-model auto-configs. Other provider auto-configs with the same shape are outside this PR.