Skip to content

feat(dotAI): Dot AI LangChain4J integration (First provider/OpenAI)#35150

Open
ihoffmann-dot wants to merge 52 commits intomainfrom
dot-ai-langchain-integration
Open

feat(dotAI): Dot AI LangChain4J integration (First provider/OpenAI)#35150
ihoffmann-dot wants to merge 52 commits intomainfrom
dot-ai-langchain-integration

Conversation

@ihoffmann-dot
Copy link
Copy Markdown
Member

@ihoffmann-dot ihoffmann-dot commented Mar 27, 2026

Summary

Replaces the direct OpenAI HTTP client (OpenAIClient) with a LangChain4J abstraction layer, enabling multi-provider AI support in dotCMS. This PR covers Phase 1: OpenAI via LangChain4J.

Changes

  • LangChain4jAIClient.java: New AIProxiedClient implementation that delegates chat, embeddings, and image requests to LangChain4J models.
  • LangChain4jModelFactory.java: Factory that builds ChatModel, EmbeddingModel, and ImageModel instances from a ProviderConfig. Only place with provider-specific builder logic.
  • ProviderConfig.java: Deserializable POJO for the providerConfig JSON secret (per provider section: model, apiKey, endpoint, maxTokens, maxCompletionTokens, etc.).
  • AppConfig.java: Replaced legacy individual-field secrets (apiKey, model, etc.) with a single providerConfig JSON string. isEnabled() now only checks this field.
  • AIAppValidator.java: Removed the OpenAI /v1/models validation call, which is incompatible with the multi-provider architecture.
  • CompletionsResource.java: Updated /api/v1/ai/completions/config to derive model names and config values from AppConfig getters instead of iterating raw AppKeys.
  • dotAI.yml: Removed legacy hidden fields; added providerConfig as the single configuration entry point.
  • Tests: Added unit tests for ProviderConfig, LangChain4jModelFactory, and LangChain4jAIClient; updated AIProxyClientTest integration test to use providerConfig-based setup.

Motivation

The previous implementation was tightly coupled to OpenAI's API contract (hardcoded HTTP calls, OpenAI-specific parameters, model validation via /v1/models). LangChain4J provides a provider-agnostic model interface, allowing future phases to add Azure OpenAI, AWS Bedrock, and Vertex AI without touching the core request/response flow.

The providerConfig JSON secret replaces multiple individual secrets with a single structured configuration, supporting per-section (chat/embeddings/image) provider and model settings.

Related Issue

This PR fixes #35183
EPIC: dotAI Multi-Provider Support #33970


Note

High Risk
High risk because it replaces the core AI client/provider path (OpenAI HTTP + model fallback/validation) with a new LangChain4J-backed implementation and a new providerConfig secret format, impacting chat, embeddings, and image generation behavior and configuration compatibility.

Overview
dotAI now routes chat, embeddings, and image requests through a new LangChain4J-backed client (LangChain4jAIClient) and sets LangChain4J as the default provider, replacing the direct OpenAI HTTP client and removing the model-fallback strategy.

Configuration is migrated from many per-field secrets to a single providerConfig JSON (with hashing + per-host model caching), updating AppConfig.isEnabled(), dotAI.yml, and the /v1/ai/completions/config output (including credential redaction). Several OpenAI-specific model management/validation classes and tests are removed, and integration/unit tests are updated/added for the new providerConfig + LangChain4J flow.

Embeddings/image handling is adjusted: embeddings requests now send raw text (with token-count fallback when encoding is unavailable), the async thread pool key is renamed to AIThreadPool, max-token resolution is made more resilient, and image temp-file creation now supports base64 (b64_json) responses.

Reviewed by Cursor Bugbot for commit 31cb86e. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions github-actions bot added the Area : Backend PR changes Java/Maven backend code label Mar 27, 2026
@claude
Copy link
Copy Markdown
Contributor

claude bot commented Mar 27, 2026

Claude finished @ihoffmann-dot's task in 2m 56s —— View job


PR Review

  • Gather context
  • Review key changed files
  • Analyze issues and risks
  • Post findings

The LangChain4J abstraction layer is well-structured and the cache-key strategy (host + SHA-256 of providerConfig) is sound. The @Value.Redacted on credential fields and the isEnabled() guard are good. A few things need attention:


Bugs

1. latch.await() has no timeout — threads can block forever (LangChain4jAIClient.java:243)

If the LangChain4J streaming model hangs (network drop without proper error callback, provider bug), latch.await() never returns and the handling thread is permanently blocked. This will eventually starve the server's thread pool under sustained streaming load.

// Current — unbounded wait
latch.await();

// Fix — add a timeout matching the model's configured timeout (+ headroom)
if (!latch.await(120, TimeUnit.SECONDS)) {
    throw new DotAIClientConnectException("Streaming timed out after 120s");
}

Fix this →


2. buildOpenAiStreamingChatModel silently drops maxRetries (LangChain4jModelFactory.java:129-141)

The non-streaming builder uses applyCommonConfig (which wires maxRetries), but the streaming builder sets endpoint and timeout manually and never sets maxRetries. Transient failures in streaming requests will never be retried.

// Non-streaming — correct
applyCommonConfig(config, builder::baseUrl, builder::maxRetries, builder::timeout);

// Streaming — missing maxRetries
if (config.endpoint() != null) builder.baseUrl(config.endpoint());
if (config.timeout() != null) builder.timeout(Duration.ofSeconds(config.timeout()));
// config.maxRetries() is never applied

Fix this →


3. resolveForm called twice per streaming request (CompletionsResource.java:89 and line 262)

summarizeFromContent calls resolveForm at line 89 and stores the result, then passes the original formIn (not resolvedForm) to getResponse. Inside getResponse, resolveForm is called again at line 262. The first call's result is completely discarded. Auth token lookup and model resolution happen twice per streaming request.

// summarizeFromContent — resolvedForm is computed but never used
final CompletionsForm resolvedForm = resolveForm(request, response, formIn);
return getResponse(
        request, response,
        formIn,          // ← passes formIn, not resolvedForm
        ...

4. AppConfig.toString() has a stray ' on every model field (AppConfig.java:409-411)

"  model=" + model + "',\n" +   // ← produces: model=AIModel{...}',

The trailing ' was presumably a quote that lost its opening pair. Logs are garbled.


Design Issues

5. isEnabled() logs at INFO on every blocked request (AppConfig.java:341,345)

isEnabled() is called on every sendRequest (and potentially more frequently). Logging at INFO when it returns false will flood logs for unconfigured hosts. Should be Logger.debug.


6. Credential redaction is a hardcoded field-name allowlist (CompletionsResource.java:57)

private static final Set<String> CREDENTIAL_FIELDS = Set.of("apiKey", "secretAccessKey", "accessKeyId");

Future providers (Vertex AI service account tokens, GCP application credentials, etc.) will silently leak credentials in the /config response unless this set is updated. The ProviderConfig interface already has @Value.Redacted on sensitive fields — consider driving redaction from the Jackson-serialized form of ImmutableProviderConfig which will automatically omit @Redacted fields, rather than maintaining a parallel hardcoded list.


7. LangChain4jAIClient uses a bare new ObjectMapper() (line 68)

private static final ObjectMapper MAPPER = new ObjectMapper();

The rest of the codebase uses DotObjectMapperProvider.createDefaultMapper() for consistent deserialization settings (snake_case, date handling, etc.). A bare ObjectMapper could fail to deserialize ProviderConfig fields correctly if naming conventions differ.


8. Double sanitization of providerConfig (AppConfig.java:67 and CompletionsResource.java:197)

providerConfig is stripped of \r\n\t in AppConfig constructor, then stripped again in redactCredentials. Harmless but redundant — the second strip could be removed.


Informational

  • sha256Hex is a manual reimplementation of com.google.common.hash.Hashing.sha256() which is already on the classpath. Minor cleanup opportunity.

  • Rollback risk (as already flagged by prior automated reviews): isEnabled() now checks only providerConfig. Sites that had apiKey configured but haven't migrated to providerConfig will see AI silently disabled after rollback to N-1. The two-phase approach suggested previously (dual-check OR key) is the safe path.

  • The old apiKey/apiUrl/apiImageUrl/apiEmbeddingsUrl fields are still stored in AppConfig and exposed via getters but unused by the new client. Once the rollback window closes, these should be removed to avoid confusion.

@ihoffmann-dot ihoffmann-dot self-assigned this Mar 27, 2026
@claude
Copy link
Copy Markdown
Contributor

claude bot commented Mar 27, 2026

Pull Request Unsafe to Rollback!!!

  • Category: M-3 — REST / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM

  • Why it's unsafe: The /api/v1/ai/completions/config GET endpoint response shape has changed: the new code explicitly builds a narrow response map (replacing the previous full AppKeys iteration), so fields such as textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, and related model-config keys are no longer returned. Any cached Angular frontend page or integration client that was built against N's response shape will not find those fields on a rollback to N-1.

    Additionally, AppConfig.isEnabled() was changed to check only providerConfig (the new secret). If a user deploys N and configures only providerConfig (leaving the legacy apiKey empty), rolling back to N-1 causes isEnabled() to return false and the entire dotAI integration is silently disabled until the legacy apiKey field is manually re-populated.

  • Code that makes it unsafe:

    • dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.javagetConfig() now builds the response from explicit getters rather than iterating all AppKeys; many previously-returned keys are absent from the new response map.
    • dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.javaisEnabled() now returns StringUtils.isNotBlank(providerConfig) only (line ~220), dropping the legacy apiKey + URL check that N-1 uses to decide whether the AI app is active.
    • dotCMS/src/main/resources/apps/dotAI.yml — the apiKey param (previously required: true, hidden: true) was removed from the schema; providerConfig added as the sole activation field.
  • Alternative (if possible): Keep AppConfig.isEnabled() dual-checking: return true when either providerConfig or the legacy apiKey is set, so N-1 continues to work for sites that have not yet migrated. For the /config endpoint, preserve the previously-returned model-config keys (or alias them) alongside the new providerConfig field, so clients built against N-1's response contract do not receive unexpected nulls after rollback. Follow the two-phase removal pattern: ship N with both old and new config paths active, remove the legacy path in N+1 once N-2 is outside the rollback window.

@ihoffmann-dot ihoffmann-dot marked this pull request as ready for review March 30, 2026 23:15
host = new SiteDataGen().nextPersisted();
AiTest.aiAppSecretsWithProviderConfig(
host,
AiTest.providerConfigJson(AiTest.PORT, "gpt-4o-mini"));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

will this aiAppSecretsWithProviderConfig work when you implement new providers? will you have to call it for each provider?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes. This is just a test with an OpenAI model, but aiAppSecretsWithProviderConfig just saves a JSON string and doesn’t know about the provider. It will work with any model. To test other providers we only need to pass a different JSON in the providerConfigJson param.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I mean, this test was implemented to work with an OpenAI model. When you implement the other providers and want to include new tests for them, you will have to refactor it to make it scalable; otherwise, your test class will contain duplicate logic, become huge, and /or difficult to follow

final JSONObjectAIRequest request = textRequest(
invalidModel,
"gpt-4o-mini",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We really should be walking away from all those JSONObject; they are very much error-prone
The modern Java stack should prefer immutables bound with Jackson. But this is "fine" as this code was already like that. But we should probably modernize this API

if (config == null || config.getProvider() == null) {
return null;
}
switch (config.getProvider().toLowerCase()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Too few statements for a switch, don't you think ??
unless this is expected to grow

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It will grow in a few days, Fab. Ivan is adding one provider per PR to avoid huge PRs

@JsonIgnoreProperties(ignoreUnknown = true)
public class ProviderConfig {

private String provider;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

See how we use immutables e.g.
com/dotcms/content/model/Contentlet.java
Not sure, but perhaps at this point we could even use Java records

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks, I decided using Immutables to respect the codebase

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Apr 1, 2026

Pull Request Unsafe to Rollback!!!

  • Category: M-3 — REST / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM

  • Why it's unsafe: Two changes combine to make this unsafe to roll back:

    1. The /api/v1/ai/completions/config response shape changed. The new getConfig() builds a narrow response map from explicit getters rather than iterating all AppKeys. After deploying N, rolling back to N-1 changes the response from the new shape (with providerConfig, model, imageModel, embeddingsModel) back to the old shape (with textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, etc.). Any client (Angular admin or integration) built against N's response shape will find those fields absent after rollback.

    2. AppConfig.isEnabled() now only checks providerConfig. If a site deploys N and configures only providerConfig (leaving legacy apiKey empty), rolling back to N-1 — whose isEnabled() checks for apiKey + URL — silently returns false, disabling the entire dotAI integration until apiKey is manually re-populated.

  • Code that makes it unsafe:

    • dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java lines 324-326: isEnabled() returns StringUtils.isNotBlank(providerConfig) only, dropping the legacy apiKey + URL check that N-1 uses to determine whether AI is active.
    • dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.java lines 169-188: getConfig() now returns model, imageModel, embeddingsModel, and providerConfig keys; previously returned textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, and related model-config keys are no longer present.
    • dotCMS/src/main/resources/apps/dotAI.yml: the apiKey param (previously required, hidden) was removed from the schema; providerConfig added as the sole activation field.
  • Alternative (if possible): Keep AppConfig.isEnabled() dual-checking — return true when either providerConfig or the legacy apiKey is set, so N-1 continues to work for sites that have not yet migrated. For the /config endpoint, preserve the previously-returned model-config keys (or alias them) alongside the new providerConfig field. Follow the two-phase removal pattern: ship N with both old and new config paths active, remove the legacy path in N+1 once N-2 is outside the rollback window.

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 9, 2026

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Apr 9, 2026

Pull Request Unsafe to Rollback!!!

  • Category: M-3 — REST / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM

  • Why it's unsafe: Two changes combine to make this unsafe to roll back.

    1. The /api/v1/ai/completions/config response shape changed. The new getConfig() builds a narrow response map from explicit getters. After deploying N, rolling back to N-1 changes the response back to the old shape (with textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, etc.). Any client built against N's response (with providerConfig, model, imageModel, embeddingsModel) will find those fields absent after rollback.

    2. AppConfig.isEnabled() now only checks providerConfig. If a site deploys N and configures only providerConfig (leaving the legacy apiKey empty), rolling back to N-1 — whose isEnabled() checks for apiUrl + apiKey — silently returns false, disabling the entire dotAI integration until manually re-configured.

  • Code that makes it unsafe:

    • dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java lines 323-325: isEnabled() returns StringUtils.isNotBlank(providerConfig) only, dropping the legacy apiUrl + apiKey check that N-1 uses to decide if AI is active.
    • dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.java lines 179-191: getConfig() now returns model, imageModel, embeddingsModel, and providerConfig keys; previously-returned keys (textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, etc.) are no longer present.
    • dotCMS/src/main/resources/apps/dotAI.yml: the apiKey param (previously required, hidden) was removed from the schema; providerConfig added as the sole activation field.
  • Alternative (if possible): Keep AppConfig.isEnabled() dual-checking — return true when either providerConfig or the legacy apiKey is set, so N-1 continues to work for sites that have not yet migrated. For the /config endpoint, preserve the previously-returned model-config keys (or alias them) alongside the new providerConfig field. Follow the two-phase removal pattern: ship N with both old and new config paths active, remove the legacy path in N+1 once N-2 is outside the rollback window.

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 9, 2026

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Apr 9, 2026

Pull Request Unsafe to Rollback!!!

  • Category: M-3 — REST / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM

  • Why it's unsafe: Two changes combine to make this unsafe to roll back.

    1. The /api/v1/ai/completions/config response shape changed. The new getConfig() builds a narrow response map from explicit getters. After deploying N, rolling back to N-1 changes the response back to the old shape (with textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, availableModels, etc.). Any client built against N's response (with providerConfig, model, imageModel, embeddingsModel) will find those fields absent after rollback.

    2. AppConfig.isEnabled() now only checks providerConfig. If a site deploys N and configures only providerConfig (leaving the legacy apiKey empty), rolling back to N-1 — whose isEnabled() checks for apiUrl + apiKey — silently returns false, disabling the entire dotAI integration until manually re-configured.

  • Code that makes it unsafe:

    • dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java lines 323-325: isEnabled() returns StringUtils.isNotBlank(providerConfig) only, dropping the legacy apiUrl + apiKey check that N-1 uses to decide if AI is active.
    • dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.java lines 179-191: getConfig() now returns model, imageModel, embeddingsModel, and providerConfig keys; previously-returned keys (textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, availableModels, etc.) are no longer present.
    • dotCMS/src/main/resources/apps/dotAI.yml: the apiKey param (previously required, hidden) was removed from the schema; providerConfig added as the sole activation field.
  • Alternative (if possible): Keep AppConfig.isEnabled() dual-checking — return true when either providerConfig or the legacy apiKey is set, so N-1 continues to work for sites that have not yet migrated. For the /config endpoint, preserve the previously-returned model-config keys (or alias them) alongside the new providerConfig field. Follow the two-phase removal pattern: ship N with both old and new config paths active, remove the legacy path in N+1 once N-2 is outside the rollback window.

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 10, 2026

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Apr 10, 2026

Pull Request Unsafe to Rollback!!!

  • Category: M-3 — REST / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM

  • Why it's unsafe: Two changes combine to make this unsafe to roll back.

    1. The /api/v1/ai/completions/config response shape changed. The new getConfig() (commit 18c5b44d) explicitly builds a narrow response from specific getters. After deploying N, rolling back to N-1 changes the response back to the old shape (with textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, availableModels, etc.). Any client built against N's response (with providerConfig, rolePrompt, textPrompt, imagePrompt, imageSize, listenerIndexer) will find those fields absent after rollback.

    2. AppConfig.isEnabled() now only checks providerConfig. If a site deploys N and configures only providerConfig (leaving the legacy apiKey empty), rolling back to N-1 — whose isEnabled() checks for apiUrl + apiImageUrl + apiEmbeddingsUrl + apiKey — silently returns false, disabling the entire dotAI integration until manually re-configured.

  • Code that makes it unsafe:

    • dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java lines 323-325: isEnabled() returns StringUtils.isNotBlank(providerConfig) only, dropping the legacy 4-field check that N-1 uses to decide if AI is active.
    • dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.java lines 179-191: getConfig() now returns providerConfig, rolePrompt, textPrompt, imagePrompt, imageSize, listenerIndexer, and debugLogging only; previously-returned keys (textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, availableModels, etc.) are no longer present.
    • dotCMS/src/main/resources/apps/dotAI.yml: the apiKey param (previously required, hidden) was removed from the schema; providerConfig added as the sole activation field.
  • Alternative (if possible): Keep AppConfig.isEnabled() dual-checking — return true when either providerConfig or the legacy apiKey is set, so N-1 continues to work for sites that have not yet migrated. For the /config endpoint, preserve the previously-returned model-config keys (or alias them) alongside the new providerConfig field. Follow the two-phase removal pattern: ship N with both old and new config paths active, remove the legacy path in N+1 once N-2 is outside the rollback window.

@cursor
Copy link
Copy Markdown

cursor bot commented Apr 10, 2026

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Apr 13, 2026

Pull Request Unsafe to Rollback!!!

  • Category: M-3 — REST / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM

  • Why it's unsafe: Two changes combine to make this unsafe to roll back.

    1. The /api/v1/ai/completions/config response shape changed. The new getConfig() builds a narrow response map from explicit getters. After deploying N, rolling back to N-1 changes the response back to the old shape (with textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, availableModels, etc.). Any client built against N's response (with providerConfig, rolePrompt, textPrompt, imagePrompt, imageSize, listenerIndexer, debugLogging) will find those fields absent after rollback.

    2. AppConfig.isEnabled() now only checks providerConfig. If a site deploys N and configures only providerConfig (leaving the legacy apiUrl/apiKey empty), rolling back to N-1 — whose isEnabled() checks for ALL of apiUrl, apiImageUrl, apiEmbeddingsUrl, and apiKey via .allMatch() — silently returns false, disabling the entire dotAI integration until manually re-configured.

  • Code that makes it unsafe:

    • dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java lines 323-325: isEnabled() returns StringUtils.isNotBlank(providerConfig) only, dropping the 4-field .allMatch() check that N-1 uses to decide if AI is active.
    • dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.java lines 179-191: getConfig() now returns providerConfig, rolePrompt, textPrompt, imagePrompt, imageSize, listenerIndexer, and debugLogging only; previously-returned keys (textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, availableModels, etc.) are no longer present.
    • dotCMS/src/main/resources/apps/dotAI.yml: the apiKey param (previously required, hidden) was removed from the schema; providerConfig added as the sole activation field.
  • Alternative (if possible): Keep AppConfig.isEnabled() dual-checking — return true when either providerConfig or the legacy apiKey is set, so N-1 continues to work for sites that have not yet migrated. For the /config endpoint, preserve the previously-returned model-config keys (or alias them) alongside the new providerConfig field. Follow the two-phase removal pattern: ship N with both old and new config paths active, remove the legacy path in N+1 once N-2 is outside the rollback window.

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Apr 13, 2026

Pull Request Unsafe to Rollback!!!

  • Category: M-3 — REST / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM

  • Why it's unsafe: Two changes combine to make this unsafe to roll back.

    1. The /api/v1/ai/completions/config response shape changed. The new getConfig() builds a narrow response map from explicit getters. After deploying N, rolling back to N-1 changes the response back to the old shape (with textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, availableModels, etc.). Any client built against N's response (with providerConfig, rolePrompt, textPrompt, imagePrompt, imageSize, listenerIndexer, debugLogging) will find those fields absent after rollback.

    2. AppConfig.isEnabled() now only checks providerConfig. If a site deploys N and configures only providerConfig (leaving the legacy apiUrl/apiKey empty), rolling back to N-1 — whose isEnabled() checks for ALL of apiUrl, apiImageUrl, apiEmbeddingsUrl, and apiKey via .allMatch() — silently returns false, disabling the entire dotAI integration until manually re-configured.

  • Code that makes it unsafe:

    • dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java lines 323-325: isEnabled() returns StringUtils.isNotBlank(providerConfig) && (model != AIModel.NOOP_MODEL || imageModel != AIModel.NOOP_MODEL || embeddingsModel != AIModel.NOOP_MODEL) only, dropping the 4-field .allMatch() check that N-1 uses to decide if AI is active.
    • dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.java lines 179-191: getConfig() now returns providerConfig, rolePrompt, textPrompt, imagePrompt, imageSize, listenerIndexer, and debugLogging only; previously-returned keys (textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, availableModels, etc.) are no longer present.
    • dotCMS/src/main/resources/apps/dotAI.yml: the apiKey param (previously required, hidden) was removed from the schema; providerConfig added as the sole activation field.
  • Alternative (if possible): Keep AppConfig.isEnabled() dual-checking — return true when either providerConfig or the legacy apiKey is set, so N-1 continues to work for sites that have not yet migrated. For the /config endpoint, preserve the previously-returned model-config keys (or alias them) alongside the new providerConfig field. Follow the two-phase removal pattern: ship N with both old and new config paths active, remove the legacy path in N+1 once N-2 is outside the rollback window.

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Apr 14, 2026

Pull Request Unsafe to Rollback!!!

  • Category: M-3 — REST / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM

  • Why it's unsafe: Two changes combine to make this unsafe to roll back.

    1. The /api/v1/ai/completions/config response shape changed. The new getConfig() builds a narrow response map from explicit getters. After deploying N, rolling back to N-1 changes the response back to the old shape (with textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, availableModels, etc.). Any client built against N's response (with providerConfig, rolePrompt, textPrompt, imagePrompt, imageSize, listenerIndexer, debugLogging) will find those fields absent after rollback.

    2. AppConfig.isEnabled() now only checks providerConfig. If a site deploys N and configures only providerConfig (leaving the legacy apiUrl/apiKey empty), rolling back to N-1 — whose isEnabled() checks for ALL of apiUrl, apiImageUrl, apiEmbeddingsUrl, and apiKey via .allMatch() — silently returns false, disabling the entire dotAI integration until manually re-configured.

  • Code that makes it unsafe:

    • dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java isEnabled(): returns false when providerConfig is blank (dropping the legacy 4-field .allMatch() check that N-1 uses to decide if AI is active).
    • dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.java getConfig(): now returns providerConfig, rolePrompt, textPrompt, imagePrompt, imageSize, listenerIndexer, and debugLogging only; previously-returned keys (textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, availableModels, etc.) are no longer present.
    • dotCMS/src/main/resources/apps/dotAI.yml: the apiKey param (previously required, hidden) was removed from the schema; providerConfig added as the sole activation field.
  • Alternative (if possible): Keep AppConfig.isEnabled() dual-checking — return true when either providerConfig or the legacy apiKey is set, so N-1 continues to work for sites that have not yet migrated. For the /config endpoint, preserve the previously-returned model-config keys (or alias them) alongside the new providerConfig field. Follow the two-phase removal pattern: ship N with both old and new config paths active, remove the legacy path in N+1 once N-2 is outside the rollback window.

@ihoffmann-dot ihoffmann-dot force-pushed the dot-ai-langchain-integration branch from a6bc5c6 to 4aaa0fd Compare April 14, 2026 16:44
@claude
Copy link
Copy Markdown
Contributor

claude bot commented Apr 14, 2026

Pull Request Unsafe to Rollback!!!

  • Category: M-3 — REST / Headless API Contract Change

  • Risk Level: 🟡 MEDIUM

  • Why it's unsafe: Two changes combine to make this unsafe to roll back.

    1. The /api/v1/ai/completions/config response shape changed. The new getConfig() builds a narrow response map from explicit getters. After deploying N, rolling back to N-1 changes the response back to the old shape (with textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, availableModels, etc.). Any client built against N's response (with providerConfig, rolePrompt, textPrompt, imagePrompt, imageSize, listenerIndexer, debugLogging) will find those fields absent after rollback.

    2. AppConfig.isEnabled() now only checks providerConfig. If a site deploys N and configures only providerConfig (leaving the legacy apiUrl/apiKey empty), rolling back to N-1 — whose isEnabled() checks for ALL of apiUrl, apiImageUrl, apiEmbeddingsUrl, and apiKey via .allMatch() — silently returns false, disabling the entire dotAI integration until manually re-configured.

  • Code that makes it unsafe:

    • dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java isEnabled() (lines 339–349): returns false when providerConfig is blank, dropping the legacy 4-field .allMatch() check that N-1 uses to decide if AI is active.
    • dotCMS/src/main/java/com/dotcms/ai/rest/CompletionsResource.java getConfig() (lines 179–192): now returns providerConfig, rolePrompt, textPrompt, imagePrompt, imageSize, listenerIndexer, and debugLogging only; previously-returned keys (textModelNames, textModelTokensPerMinute, textModelApiPerMinute, imageModelNames, embeddingsModelNames, availableModels, etc.) are no longer present.
    • dotCMS/src/main/resources/apps/dotAI.yml: the apiKey param (previously required, hidden) was removed from the schema; providerConfig added as the sole activation field.
  • Alternative (if possible): Keep AppConfig.isEnabled() dual-checking — return true when either providerConfig or the legacy apiKey is set, so N-1 continues to work for sites that have not yet migrated. For the /config endpoint, preserve the previously-returned model-config keys (or alias them) alongside the new providerConfig field. Follow the two-phase removal pattern: ship N with both old and new config paths active, remove the legacy path in N+1 once N-2 is outside the rollback window.

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

Labels

AI: Not Safe To Rollback Area : Backend PR changes Java/Maven backend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[FEATURE] dotAI: LangChain4J integration — Phase 1 (OpenAI)

4 participants