Skip to content

Fix OpenAI auto-config when only sub-prefix api-key is set#5990

Open
jewoodev wants to merge 1 commit into
spring-projects:mainfrom
jewoodev:fix/gh-1818-image-only-api-key
Open

Fix OpenAI auto-config when only sub-prefix api-key is set#5990
jewoodev wants to merge 1 commit into
spring-projects:mainfrom
jewoodev:fix/gh-1818-image-only-api-key

Conversation

@jewoodev
Copy link
Copy Markdown

@jewoodev jewoodev commented May 9, 2026

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 picks openai, and registration runs the common spring.ai.openai.api-key check unconditionally. Sub-prefix-only credentials (and other valid sources — programmatic Credential, OPENAI_API_KEY, passwordless Foundry) are never consulted at the gate, so context startup aborts even when a usable credential exists.

Fix

Introduce OpenAiConnectionCondition with one nested class per sub-config and annotate each sub-model @Bean with @Conditional(OpenAiConnectionCondition.X.class). Each getMatchOutcome returns a match if any of the relevant credential sources is present: common api-key, sub-prefix api-key, programmatic Credential on common/sub *Properties, OPENAI_API_KEY env var, GitHub Models token, or Microsoft Foundry passwordless auth. Conditions sit on @Bean methods so @EnableConfigurationProperties and the existing @ConditionalOnProperty(spring.ai.model.<kind>) selector keep working unchanged.

Tests

Added regression coverage in OpenAiConnectionConditionAutoConfigurationTests for:

./mvnw -pl auto-configurations/models/spring-ai-autoconfigure-model-openai test \
    -Dtest='OpenAiConnectionConditionAutoConfigurationTests'

Scope

This PR covers the six OpenAI sub-model auto-configs. Other provider auto-configs with the same shape are outside this PR.

@jewoodev jewoodev force-pushed the fix/gh-1818-image-only-api-key branch 3 times, most recently from 5de82d6 to 73edf9f Compare May 10, 2026 02:00
Copy link
Copy Markdown
Author

@jewoodev jewoodev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll check this new dependency is necessary one more time. ➝ The detailed check is complete!

Comment on lines +131 to 138

<dependency>
<groupId>uk.org.webcompere</groupId>
<artifactId>system-stubs-jupiter</artifactId>
<version>2.1.7</version>
<scope>test</scope>
</dependency>
</dependencies>
Copy link
Copy Markdown
Author

@jewoodev jewoodev May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 are java.*, 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 Spring Environment available, so env-var fallback necessarily goes through JDK-standard System.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:

  1. Swap system-stubs-jupiter for junit-pioneer (same idea, possibly more familiar in this ecosystem).
  2. Drop openAiApiKeyEnvVarEnablesAllSixModels entirely and rely on existing OpenAiSetupTests coverage 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>
@jewoodev jewoodev force-pushed the fix/gh-1818-image-only-api-key branch from 73edf9f to e8fde7f Compare May 13, 2026 06:12
@jewoodev
Copy link
Copy Markdown
Author

@sdeleuze Before further review, could I get a quick design call?

The PR adds OpenAiConnectionCondition as a 220-line file with six nested condition classes (one per sub-model bean). A lighter alternative — call it D — collapses these into a single top-level OnAvailableOpenAiConnection condition plus a credential-check helper on OpenAiAutoConfigurationUtil (or AbstractOpenAiProperties), with the sub-prefix resolved from the bean method's properties parameter type. ~80 lines, no per-sub-bean wrapper.

Trade-off: under D, @Conditional(OnAvailableOpenAiConnection.class) no longer names the sub-prefix at the call site, so the wiring is slightly less self-documenting than the current @Conditional(...Chat.class) shape.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant