Skip to content

Commit f93ac7e

Browse files
feat(dotAI): custom properties via providerConfig settings section (#35531)
## Summary Introduces a `settings` block inside `providerConfig` JSON as the single source of truth for all dotAI behavioral configuration (prompts, temperature, embeddings tuning, etc.), replacing the previous individual App Secret fields and Additional Parameters. - **`AppKeys.java`**: Added `settingsKey` field to each enum constant — the camelCase JSON key within `providerConfig.settings`. Keys like `ROLE_PROMPT`, `COMPLETION_TEMPERATURE`, and all embeddings tuning keys now map directly to a `settingsKey`. Keys that are not configurable via settings (`API_KEY`, `API_URL`, `PROVIDER_CONFIG`) retain `settingsKey = null`. - **`AppConfig.java`**: Added `settingsValues` (`Map<String, String>`) parsed from `providerConfig.settings` at construction time. `getConfig(AppKeys)` now checks `settingsValues` first, then falls back to `AppKeys.defaultValue`. Old `configValues`/secret lookups removed — `providerConfig` is the only source of truth. - **`LangChain4jAIClient.java`**: Fixed image size override — the `size` field in the request payload now overrides `ProviderConfig.size()` at call time. Size is included in the model cache key so each distinct size gets its own cached instance. Extracted `applyRequestSize()` as a `@VisibleForTesting` static method. - **`dotAI.yml`**: Removed all legacy hidden fields. Retained only `providerConfig` with the required `hint` field. - **`DotAiConfigDetailComponent`**: Updated the example JSON to reflect the new `settings` section structure. - **Tests**: Added 5 tests to `AppConfigTest` covering `settings` overrides, fallback behavior, and prompt field population. Added 3 tests to `LangChain4jAIClientTest` covering `applyRequestSize` with size present, absent, and blank. - **Postman**: Removed stale `com.dotcms.ai.debug.logging` assertion from Config GET test. ## Configuration The `settings` block is optional. Fields not included fall back to their documented defaults. ```json { "chat": { "provider": "openai", "apiKey": "sk-...", "model": "gpt-4o", "maxTokens": 16384, "temperature": 1.0, "maxRetries": 3 }, "embeddings": { "provider": "openai", "apiKey": "sk-...", "model": "text-embedding-ada-002" }, "image": { "provider": "openai", "apiKey": "sk-...", "model": "dall-e-3" }, "settings": { "rolePrompt": "You are dotCMSbot, an AI assistant to help content creators.", "textPrompt": "Use Descriptive writing style.", "imagePrompt": "Use 16:9 aspect ratio.", "imageSize": "1792x1024", "listenerIndexer": { "default": "blog,news,webPageContent" }, "temperature": 1.0, "embeddingsSplitAtTokens": 512, "embeddingsSearchThreshold": 0.25, "debugLogging": false } } ``` ## Breaking Change Customers with existing dotAI configuration must migrate to the new `providerConfig` JSON format. Individual App Secret fields are no longer read. See the [DotAI-Migration-Guide.md](https://github.com/user-attachments/files/27261982/DotAI-Migration-Guide.md) included in this PR. This PR fixes #35183 [EPIC: dotAI Multi-Provider Support #33970](#33970) > [!NOTE] > **Medium Risk** > Configuration shape change affects all existing dotAI deployments. No automatic migration — customers must re-enter configuration using the new JSON format. Runtime behavior is unchanged for deployments that migrate correctly.
1 parent 1fa9b9e commit f93ac7e

9 files changed

Lines changed: 207 additions & 94 deletions

File tree

core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/dot-ai-config-detail.component.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,24 @@ const EXAMPLE_CONFIG = {
2626
model: 'gpt-4o',
2727
maxTokens: 16384,
2828
temperature: 1.0,
29-
maxRetries: 3,
30-
rolePrompt: 'You are dotCMSbot, an AI assistant to help content creators.',
31-
textPrompt: 'Use Descriptive writing style.'
29+
maxRetries: 3
3230
},
3331
embeddings: {
3432
provider: 'openai',
3533
apiKey: 'sk-...',
36-
model: 'text-embedding-ada-002',
37-
listenerIndexer: { default: 'blog,news,webPageContent' }
34+
model: 'text-embedding-ada-002'
3835
},
3936
image: {
4037
provider: 'openai',
4138
apiKey: 'sk-...',
42-
model: 'dall-e-3',
43-
size: '1792x1024',
44-
imagePrompt: 'Use 16:9 aspect ratio.'
39+
model: 'dall-e-3'
40+
},
41+
settings: {
42+
rolePrompt: 'You are dotCMSbot, an AI assistant to help content creators.',
43+
textPrompt: 'Use Descriptive writing style.',
44+
imagePrompt: 'Use 16:9 aspect ratio.',
45+
imageSize: '1792x1024',
46+
listenerIndexer: { default: 'blog,news,webPageContent' }
4547
}
4648
};
4749

dotCMS/src/main/java/com/dotcms/ai/app/AppConfig.java

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public class AppConfig implements Serializable {
5353
private final String providerConfig;
5454
private final String providerConfigHash;
5555
private final transient JsonNode providerConfigRoot;
56+
private final Map<String, String> settingsValues;
5657
private final Map<String, Secret> configValues;
5758

5859
public AppConfig(final String host, final Map<String, Secret> secrets) {
@@ -80,11 +81,13 @@ public AppConfig(final String host, final Map<String, Secret> secrets) {
8081
embeddingsModel = AIModel.NOOP_MODEL;
8182
}
8283

83-
rolePrompt = getFromSection(providerConfigRoot, "chat", "rolePrompt", AppKeys.ROLE_PROMPT.defaultValue);
84-
textPrompt = getFromSection(providerConfigRoot, "chat", "textPrompt", AppKeys.TEXT_PROMPT.defaultValue);
85-
imagePrompt = getFromSection(providerConfigRoot, "image", "imagePrompt", AppKeys.IMAGE_PROMPT.defaultValue);
86-
imageSize = getFromSection(providerConfigRoot, "image", "size", AppKeys.IMAGE_SIZE.defaultValue);
87-
listenerIndexer = getFromSection(providerConfigRoot, "embeddings", "listenerIndexer", AppKeys.LISTENER_INDEXER.defaultValue);
84+
settingsValues = parseSettings(providerConfigRoot);
85+
86+
rolePrompt = getFromSettings(settingsValues, "rolePrompt", AppKeys.ROLE_PROMPT.defaultValue);
87+
textPrompt = getFromSettings(settingsValues, "textPrompt", AppKeys.TEXT_PROMPT.defaultValue);
88+
imagePrompt = getFromSettings(settingsValues, "imagePrompt", AppKeys.IMAGE_PROMPT.defaultValue);
89+
imageSize = getFromSettings(settingsValues, "imageSize", AppKeys.IMAGE_SIZE.defaultValue);
90+
listenerIndexer = getFromSettings(settingsValues, "listenerIndexer", AppKeys.LISTENER_INDEXER.defaultValue);
8891

8992
configValues = secrets.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
9093

@@ -235,8 +238,7 @@ public String getListenerIndexer() {
235238
* @return the integer configuration value
236239
*/
237240
public int getConfigInteger(final AppKeys appKey) {
238-
String value = Try.of(() -> configValues.get(appKey.key).getString()).getOrElse(appKey.defaultValue);
239-
return Try.of(() -> Integer.parseInt(value)).getOrElse(0);
241+
return Try.of(() -> Integer.parseInt(getConfig(appKey))).getOrElse(0);
240242
}
241243

242244
/**
@@ -246,8 +248,7 @@ public int getConfigInteger(final AppKeys appKey) {
246248
* @return the float configuration value
247249
*/
248250
public float getConfigFloat(final AppKeys appKey) {
249-
String value = Try.of(() -> configValues.get(appKey.key).getString()).getOrElse(appKey.defaultValue);
250-
return Try.of(() -> Float.parseFloat(value)).getOrElse(0f);
251+
return Try.of(() -> Float.parseFloat(getConfig(appKey))).getOrElse(0f);
251252
}
252253

253254
/**
@@ -257,8 +258,7 @@ public float getConfigFloat(final AppKeys appKey) {
257258
* @return the boolean configuration value
258259
*/
259260
public boolean getConfigBoolean(final AppKeys appKey) {
260-
final String value = Try.of(() -> configValues.get(appKey.key).getString()).getOrElse(appKey.defaultValue);
261-
return Try.of(() -> Boolean.parseBoolean(value)).getOrElse(false);
261+
return Try.of(() -> Boolean.parseBoolean(getConfig(appKey))).getOrElse(false);
262262
}
263263

264264
/**
@@ -279,8 +279,11 @@ public String[] getConfigArray(final AppKeys appKey) {
279279
* @return the configuration value
280280
*/
281281
public String getConfig(final AppKeys appKey) {
282-
if (configValues.containsKey(appKey.key)) {
283-
return Try.of(() -> configValues.get(appKey.key).getString()).getOrElse(appKey.defaultValue);
282+
if (appKey.settingsKey != null) {
283+
final String fromSettings = settingsValues.get(appKey.settingsKey);
284+
if (StringUtils.isNotBlank(fromSettings)) {
285+
return fromSettings;
286+
}
284287
}
285288
return appKey.defaultValue;
286289
}
@@ -355,23 +358,27 @@ public boolean isEnabled() {
355358
return true;
356359
}
357360

358-
private static String getFromSection(final JsonNode root, final String section,
359-
final String field, final String defaultValue) {
360-
try {
361-
final JsonNode sectionNode = root.get(section);
362-
if (sectionNode == null || sectionNode.isNull()) {
363-
return defaultValue;
364-
}
365-
final JsonNode fieldNode = sectionNode.get(field);
366-
if (fieldNode == null || fieldNode.isNull()) {
367-
return defaultValue;
361+
private static Map<String, String> parseSettings(final JsonNode root) {
362+
final Map<String, String> result = new java.util.HashMap<>();
363+
final JsonNode settings = root.get("settings");
364+
if (settings == null || !settings.isObject()) {
365+
return result;
366+
}
367+
final java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = settings.fields();
368+
while (fields.hasNext()) {
369+
final java.util.Map.Entry<String, JsonNode> entry = fields.next();
370+
final JsonNode value = entry.getValue();
371+
if (!value.isNull()) {
372+
result.put(entry.getKey(), value.isContainerNode() ? value.toString() : value.asText());
368373
}
369-
// Container nodes (object/array) are serialized back to a JSON string (e.g. listenerIndexer)
370-
final String value = fieldNode.isContainerNode() ? fieldNode.toString() : fieldNode.asText();
371-
return StringUtils.isNotBlank(value) ? value : defaultValue;
372-
} catch (final Exception e) {
373-
return defaultValue;
374374
}
375+
return result;
376+
}
377+
378+
private static String getFromSettings(final Map<String, String> settings,
379+
final String key, final String defaultValue) {
380+
final String value = settings.get(key);
381+
return StringUtils.isNotBlank(value) ? value : defaultValue;
375382
}
376383

377384
@com.google.common.annotations.VisibleForTesting

dotCMS/src/main/java/com/dotcms/ai/app/AppKeys.java

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,41 +11,51 @@ public enum AppKeys {
1111
ROLE_PROMPT(
1212
"rolePrompt",
1313
"You are dotCMSbot, and AI assistant to help content" +
14-
" creators generate and rewrite content in their content management system."),
15-
TEXT_PROMPT("textPrompt", "Use Descriptive writing style."),
16-
IMAGE_PROMPT("imagePrompt", "Use 16:9 aspect ratio."),
17-
IMAGE_SIZE("imageSize", "1024x1024"),
18-
EMBEDDINGS_SPLIT_AT_TOKENS("com.dotcms.ai.embeddings.split.at.tokens", "512"),
19-
EMBEDDINGS_MINIMUM_TEXT_LENGTH_TO_INDEX("com.dotcms.ai.embeddings.minimum.text.length", "64"),
20-
EMBEDDINGS_MINIMUM_FILE_SIZE_TO_INDEX("com.dotcms.ai.embeddings.minimum.file.size", "1024"),
21-
EMBEDDINGS_FILE_EXTENSIONS_TO_EMBED("com.dotcms.ai.embeddings.build.for.file.extensions", "pdf,doc,docx,txt,html"),
22-
EMBEDDINGS_SEARCH_DEFAULT_THRESHOLD("com.dotcms.ai.embeddings.search.default.threshold", ".25"),
23-
EMBEDDINGS_THREADS("com.dotcms.ai.embeddings.threads", "3"),
24-
EMBEDDINGS_THREADS_MAX("com.dotcms.ai.embeddings.threads.max", "6"),
25-
EMBEDDINGS_THREADS_QUEUE("com.dotcms.ai.embeddings.threads.queue", "10000"),
26-
EMBEDDINGS_CACHE_TTL_SECONDS("com.dotcms.ai.embeddings.cache.ttl.seconds", "600"),
27-
EMBEDDINGS_CACHE_SIZE("com.dotcms.ai.embeddings.cache.size", "1000"),
28-
EMBEDDINGS_DB_DELETE_OLD_ON_UPDATE("com.dotcms.ai.embeddings.delete.old.on.update", "true"),
29-
DEBUG_LOGGING("com.dotcms.ai.debug.logging", StringPool.FALSE),
30-
COMPLETION_TEMPERATURE("com.dotcms.ai.completion.default.temperature", "1"),
14+
" creators generate and rewrite content in their content management system.",
15+
"rolePrompt"),
16+
TEXT_PROMPT("textPrompt", "Use Descriptive writing style.", "textPrompt"),
17+
IMAGE_PROMPT("imagePrompt", "Use 16:9 aspect ratio.", "imagePrompt"),
18+
IMAGE_SIZE("imageSize", "1024x1024", "imageSize"),
19+
EMBEDDINGS_SPLIT_AT_TOKENS("com.dotcms.ai.embeddings.split.at.tokens", "512", "embeddingsSplitAtTokens"),
20+
EMBEDDINGS_MINIMUM_TEXT_LENGTH_TO_INDEX("com.dotcms.ai.embeddings.minimum.text.length", "64", "embeddingsMinimumTextLength"),
21+
EMBEDDINGS_MINIMUM_FILE_SIZE_TO_INDEX("com.dotcms.ai.embeddings.minimum.file.size", "1024", "embeddingsMinimumFileSize"),
22+
EMBEDDINGS_FILE_EXTENSIONS_TO_EMBED("com.dotcms.ai.embeddings.build.for.file.extensions", "pdf,doc,docx,txt,html", "embeddingsFileExtensions"),
23+
EMBEDDINGS_SEARCH_DEFAULT_THRESHOLD("com.dotcms.ai.embeddings.search.default.threshold", ".25", "embeddingsSearchThreshold"),
24+
EMBEDDINGS_THREADS("com.dotcms.ai.embeddings.threads", "3", "embeddingsThreads"),
25+
EMBEDDINGS_THREADS_MAX("com.dotcms.ai.embeddings.threads.max", "6", "embeddingsThreadsMax"),
26+
EMBEDDINGS_THREADS_QUEUE("com.dotcms.ai.embeddings.threads.queue", "10000", "embeddingsThreadsQueue"),
27+
EMBEDDINGS_CACHE_TTL_SECONDS("com.dotcms.ai.embeddings.cache.ttl.seconds", "600", "embeddingsCacheTtlSeconds"),
28+
EMBEDDINGS_CACHE_SIZE("com.dotcms.ai.embeddings.cache.size", "1000", "embeddingsCacheSize"),
29+
EMBEDDINGS_DB_DELETE_OLD_ON_UPDATE("com.dotcms.ai.embeddings.delete.old.on.update", "true", "embeddingsDeleteOldOnUpdate"),
30+
DEBUG_LOGGING("com.dotcms.ai.debug.logging", StringPool.FALSE, "debugLogging"),
31+
COMPLETION_TEMPERATURE("com.dotcms.ai.completion.default.temperature", "1", "temperature"),
3132
COMPLETION_ROLE_PROMPT(
3233
"com.dotcms.ai.completion.role.prompt",
33-
"You are a helpful assistant with a descriptive writing style."),
34+
"You are a helpful assistant with a descriptive writing style.",
35+
"completionRolePrompt"),
3436
COMPLETION_TEXT_PROMPT(
3537
"com.dotcms.ai.completion.text.prompt",
3638
"Answer this question\\n\\\"$!{prompt}?\\\"\\n\\nby using only the information in" +
37-
" the following text:\\n\"\"\"\\n$!{supportingContent} \\n\"\"\"\\n"),
38-
LISTENER_INDEXER("listenerIndexer", "{}"),
39+
" the following text:\\n\"\"\"\\n$!{supportingContent} \\n\"\"\"\\n",
40+
"completionTextPrompt"),
41+
LISTENER_INDEXER("listenerIndexer", "{}", "listenerIndexer"),
3942
PROVIDER_CONFIG("providerConfig", null);
4043

4144
public static final String APP_KEY = "dotAI";
4245

4346
public final String key;
4447
public final String defaultValue;
48+
/** JSON key in {@code providerConfig.settings}; {@code null} for keys not in settings. */
49+
public final String settingsKey;
4550

4651
AppKeys(final String key, final String defaultValue) {
52+
this(key, defaultValue, null);
53+
}
54+
55+
AppKeys(final String key, final String defaultValue, final String settingsKey) {
4756
this.key = key;
4857
this.defaultValue = defaultValue;
58+
this.settingsKey = settingsKey;
4959
}
5060

5161
}

dotCMS/src/main/java/com/dotcms/ai/client/langchain4j/LangChain4jAIClient.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,11 +309,22 @@ private String executeEmbeddingRequest(final String cacheKeyPrefix, final String
309309
private String executeImageRequest(final String cacheKeyPrefix, final String providerConfigJson, final JSONObject payload) {
310310
final ProviderConfig baseConfig = parseSection(providerConfigJson, "image");
311311
final String prompt = payload.getString(AiKeys.PROMPT);
312-
return executeWithFallback(cacheKeyPrefix, "image", baseConfig, imageModelCache,
312+
final ProviderConfig imageConfig = applyRequestSize(baseConfig, payload);
313+
final String sizeSuffix = imageConfig.size() != null ? ":" + imageConfig.size() : "";
314+
return executeWithFallback(cacheKeyPrefix + sizeSuffix, "image", imageConfig, imageModelCache,
313315
LangChain4jModelFactory::buildImageModel,
314316
model -> toImageResponseJson(model.generate(prompt).content()));
315317
}
316318

319+
@VisibleForTesting
320+
static ProviderConfig applyRequestSize(final ProviderConfig baseConfig, final JSONObject payload) {
321+
final String size = payload.optString(AiKeys.SIZE, null);
322+
if (size != null && !size.isBlank()) {
323+
return ImmutableProviderConfig.copyOf(baseConfig).withSize(size);
324+
}
325+
return baseConfig;
326+
}
327+
317328
@VisibleForTesting
318329
<M> String executeWithFallback(
319330
final String cacheKeyPrefix,

dotCMS/src/main/resources/apps/dotAI.yml

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ name: "dotAI"
22
iconUrl: "https://static.dotcms.com/assets/icons/apps/chatgpt_logo.svg"
33
allowExtraParameters: true
44
description: |
5-
Credentials and options for using OpenAI/ChatGPT with dotCMS. Each provider section
6-
(chat, embeddings, image) is declared independently. We recommend you have a single config
7-
for all your sites, by placing all config on the SYSTEM_HOST or provide a configuration per site.
5+
Credentials and options for using AI providers with dotCMS. Each provider section
6+
(chat, embeddings, image) declares its own provider independently. Prompts, embeddings
7+
behavior, and other settings belong in the "settings" section.
8+
We recommend a single config for all sites via SYSTEM_HOST, or per-site overrides.
89
Full documentation: https://dev.dotcms.com/docs/dotai
910
1011
params:
@@ -13,35 +14,5 @@ params:
1314
hidden: true
1415
type: "STRING"
1516
label: "Provider Config (JSON)"
16-
hint: |
17-
Single JSON object that configures all AI capabilities.
18-
Each section (chat, embeddings, image) declares its own provider independently.
19-
Prompts and settings belong inside their respective section.
20-
21-
Example (OpenAI):
22-
{
23-
"chat": {
24-
"provider": "openai",
25-
"apiKey": "sk-...",
26-
"model": "gpt-4o",
27-
"maxTokens": 16384,
28-
"temperature": 1.0,
29-
"maxRetries": 3,
30-
"rolePrompt": "You are dotCMSbot, an AI assistant to help content creators.",
31-
"textPrompt": "Use Descriptive writing style."
32-
},
33-
"embeddings": {
34-
"provider": "openai",
35-
"apiKey": "sk-...",
36-
"model": "text-embedding-ada-002",
37-
"listenerIndexer": { "default": "blog,news,webPageContent" }
38-
},
39-
"image": {
40-
"provider": "openai",
41-
"apiKey": "sk-...",
42-
"model": "dall-e-3",
43-
"size": "1792x1024",
44-
"imagePrompt": "Use 16:9 aspect ratio."
45-
}
46-
}
17+
hint: "Provider Config (JSON)"
4718
required: false

dotCMS/src/test/java/com/dotcms/ai/app/AppConfigTest.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,72 @@ public void test_appConfig_withNullProviderConfig_isNotEnabled() {
201201
assertFalse(config.isEnabled());
202202
}
203203

204+
// -------------------------------------------------------------------------
205+
// getConfig — settings section overrides defaults
206+
// -------------------------------------------------------------------------
207+
208+
private static final String CONFIG_WITH_SETTINGS =
209+
"{\n" +
210+
" \"chat\": {\"provider\": \"openai\", \"apiKey\": \"sk-test\", \"model\": \"gpt-4o-mini\"},\n" +
211+
" \"settings\": {\n" +
212+
" \"rolePrompt\": \"Custom role prompt\",\n" +
213+
" \"textPrompt\": \"Custom text prompt\",\n" +
214+
" \"imagePrompt\": \"Custom image prompt\",\n" +
215+
" \"imageSize\": \"1792x1024\",\n" +
216+
" \"temperature\": \"0.5\"\n" +
217+
" }\n" +
218+
"}";
219+
220+
@Test
221+
public void test_getConfig_settingsValue_overridesDefault() {
222+
final AppConfig config = buildAppConfig(CONFIG_WITH_SETTINGS);
223+
224+
assertEquals("Custom role prompt", config.getConfig(AppKeys.ROLE_PROMPT));
225+
assertEquals("Custom text prompt", config.getConfig(AppKeys.TEXT_PROMPT));
226+
assertEquals("Custom image prompt", config.getConfig(AppKeys.IMAGE_PROMPT));
227+
assertEquals("1792x1024", config.getConfig(AppKeys.IMAGE_SIZE));
228+
assertEquals("0.5", config.getConfig(AppKeys.COMPLETION_TEMPERATURE));
229+
}
230+
231+
@Test
232+
public void test_getConfig_settingsValueAbsent_returnsFallback() {
233+
// Config has settings section but NOT these keys → should return AppKeys defaults
234+
final AppConfig config = buildAppConfig(CONFIG_WITH_SETTINGS);
235+
236+
assertEquals(AppKeys.LISTENER_INDEXER.defaultValue, config.getConfig(AppKeys.LISTENER_INDEXER));
237+
assertEquals(AppKeys.EMBEDDINGS_SPLIT_AT_TOKENS.defaultValue,
238+
config.getConfig(AppKeys.EMBEDDINGS_SPLIT_AT_TOKENS));
239+
}
240+
241+
@Test
242+
public void test_getConfig_noSettingsSection_returnsDefault() {
243+
// Clean config with no settings block at all
244+
final AppConfig config = buildAppConfig(CLEAN_PROVIDER_CONFIG);
245+
246+
assertEquals(AppKeys.ROLE_PROMPT.defaultValue, config.getConfig(AppKeys.ROLE_PROMPT));
247+
assertEquals(AppKeys.IMAGE_SIZE.defaultValue, config.getConfig(AppKeys.IMAGE_SIZE));
248+
assertEquals(AppKeys.COMPLETION_TEMPERATURE.defaultValue, config.getConfig(AppKeys.COMPLETION_TEMPERATURE));
249+
}
250+
251+
@Test
252+
public void test_getConfig_keyWithNoSettingsKey_alwaysReturnsDefault() {
253+
// PROVIDER_CONFIG and API_URL have settingsKey=null — settings section never consulted
254+
final AppConfig config = buildAppConfig(CONFIG_WITH_SETTINGS);
255+
256+
assertNull(config.getConfig(AppKeys.PROVIDER_CONFIG));
257+
assertEquals(AppKeys.API_URL.defaultValue, config.getConfig(AppKeys.API_URL));
258+
}
259+
260+
@Test
261+
public void test_appConfig_settingsSection_populatesPromptFields() {
262+
final AppConfig config = buildAppConfig(CONFIG_WITH_SETTINGS);
263+
264+
assertEquals("Custom role prompt", config.getRolePrompt());
265+
assertEquals("Custom text prompt", config.getTextPrompt());
266+
assertEquals("Custom image prompt", config.getImagePrompt());
267+
assertEquals("1792x1024", config.getImageSize());
268+
}
269+
204270
// -------------------------------------------------------------------------
205271
// Helpers
206272
// -------------------------------------------------------------------------

0 commit comments

Comments
 (0)