Skip to content

Commit f9cfae0

Browse files
committed
feat: add map-based LLM provider configuration
Enable separating non-secret LLM provider config (model, prices, type) from secrets (API keys) in Kubernetes deployments via a new `provider-defaults` map that merges with the existing `providers` list.
1 parent 03cfcf9 commit f9cfae0

3 files changed

Lines changed: 451 additions & 7 deletions

File tree

backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/machineTranslation/LlmProperties.kt

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ class LlmProperties : MachineTranslationServiceProperties {
2323

2424
@DocProperty(
2525
description = """
26-
List of LLM providers. Example:
27-
26+
List of LLM providers. When `provider-defaults` is also set, list entries are merged with the
27+
matching map entry (by name). List values override map defaults only when explicitly set
28+
(non-null for nullable fields, non-default for `type` / `maxTokens`).
29+
`enabled` is always taken from the list entry.
30+
2831
``` yaml
2932
providers:
3033
- name: openai-gpt-4o-mini
@@ -34,9 +37,9 @@ class LlmProperties : MachineTranslationServiceProperties {
3437
model: gpt-4o-mini
3538
format: "json_schema"
3639
```
37-
40+
3841
or using environment variables:
39-
42+
4043
```
4144
TOLGEE_LLM_PROVIDERS_0_NAME=MySuperDuperAI
4245
TOLGEE_LLM_PROVIDERS_0_TYPE=OPENAI
@@ -45,12 +48,45 @@ class LlmProperties : MachineTranslationServiceProperties {
4548
TOLGEE_LLM_PROVIDERS_0_MODEL=gpt-4o-mini
4649
TOLGEE_LLM_PROVIDERS_0_FORMAT=json_schema
4750
```
48-
51+
4952
Check [llm providers documentation](/platform/projects_and_organizations/llm-providers#self-hosted-server-configuration) for more information.
5053
""",
5154
)
5255
var providers: MutableList<LlmProvider> = mutableListOf()
5356

57+
@DocProperty(
58+
description = """
59+
Map of provider defaults keyed by provider name. Use this to separate non-secret configuration
60+
(model, prices, type) from secrets (API keys) in Kubernetes deployments.
61+
62+
``` yaml
63+
provider-defaults:
64+
gpt-5-mini:
65+
type: OPENAI
66+
model: gpt-5-mini
67+
token-price-in-credits-input: 2.0
68+
token-price-in-credits-output: 1.5
69+
```
70+
71+
or using environment variables (in a ConfigMap):
72+
73+
```
74+
TOLGEE_LLM_PROVIDER_DEFAULTS_GPT_5_MINI_TYPE=OPENAI
75+
TOLGEE_LLM_PROVIDER_DEFAULTS_GPT_5_MINI_MODEL=gpt-5-mini
76+
TOLGEE_LLM_PROVIDER_DEFAULTS_GPT_5_MINI_TOKEN_PRICE_IN_CREDITS_INPUT=2.0
77+
TOLGEE_LLM_PROVIDER_DEFAULTS_GPT_5_MINI_TOKEN_PRICE_IN_CREDITS_OUTPUT=1.5
78+
```
79+
80+
Then supply only the API key via the `providers` list (in a Secret):
81+
82+
```
83+
TOLGEE_LLM_PROVIDERS_0_NAME=gpt-5-mini
84+
TOLGEE_LLM_PROVIDERS_0_API_KEY=sk-proj-...
85+
```
86+
""",
87+
)
88+
var providerDefaults: MutableMap<String, LlmProviderDefaults> = mutableMapOf()
89+
5490
@DocProperty(
5591
description = """
5692
Named fallback mapping. When a provider is not found, Tolgee will try the fallback provider.
@@ -133,4 +169,61 @@ class LlmProperties : MachineTranslationServiceProperties {
133169
const val MAX_TOKENS_DEFAULT: Long = 2000
134170
}
135171
}
172+
173+
class LlmProviderDefaults(
174+
@DocProperty(description = "Enable/disable provider")
175+
var enabled: Boolean = true,
176+
@DocProperty(description = "Provider type, an API type")
177+
var type: LlmProviderType = LlmProviderType.OPENAI,
178+
@DocProperty(description = "Provider API Key (optional for some providers)")
179+
var apiKey: String? = null,
180+
@DocProperty(description = "Provider API Url")
181+
var apiUrl: String? = null,
182+
@DocProperty(description = "Provider model (optional for some providers)")
183+
var model: String? = null,
184+
@DocProperty(description = "Provider deployment (optional for some providers)")
185+
var deployment: String? = null,
186+
@DocProperty(
187+
description = """Maximum number of tokens to generate.
188+
`max_completion_tokens` option for OpenAI API.
189+
`max_tokens` for Anthropic API.""",
190+
)
191+
var maxTokens: Long? = null,
192+
@DocProperty(description = "ChatGPT reasoning effort")
193+
var reasoningEffort: String? = null,
194+
@DocProperty(description = "Set to `json_schema` if the API supports JSON Schema")
195+
var format: String? = null,
196+
@DocProperty(
197+
description = "Load-balancing instruction HIGH = used for suggestions, LOW = used for batch operations",
198+
)
199+
var priority: LlmProviderPriority? = null,
200+
@DocProperty(
201+
description =
202+
"Specify attempts timeout(s) (Example: [30, 30] - Tolgee will make two attempts, each with timeout of 30s)",
203+
)
204+
var attempts: List<Int>? = null,
205+
@DocProperty(hidden = true)
206+
var tokenPriceInCreditsInput: Double? = null,
207+
@DocProperty(hidden = true)
208+
var tokenPriceInCreditsOutput: Double? = null,
209+
) {
210+
fun toLlmProvider(name: String): LlmProvider {
211+
return LlmProvider(
212+
enabled = enabled,
213+
name = name,
214+
type = type,
215+
apiKey = apiKey,
216+
apiUrl = apiUrl,
217+
model = model,
218+
deployment = deployment,
219+
maxTokens = maxTokens ?: LlmProvider.MAX_TOKENS_DEFAULT,
220+
reasoningEffort = reasoningEffort,
221+
format = format,
222+
priority = priority,
223+
attempts = attempts,
224+
tokenPriceInCreditsInput = tokenPriceInCreditsInput,
225+
tokenPriceInCreditsOutput = tokenPriceInCreditsOutput,
226+
)
227+
}
228+
}
136229
}

backend/data/src/main/kotlin/io/tolgee/service/LlmPropertiesService.kt

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.tolgee.service
33
import io.tolgee.api.EeSubscriptionProvider
44
import io.tolgee.configuration.tolgee.machineTranslation.LlmProperties
55
import io.tolgee.configuration.tolgee.machineTranslation.LlmProperties.LlmProvider
6+
import io.tolgee.configuration.tolgee.machineTranslation.LlmProperties.LlmProviderDefaults
67
import io.tolgee.exceptions.InvalidStateException
78
import io.tolgee.model.enums.LlmProviderType
89
import org.springframework.stereotype.Service
@@ -25,9 +26,9 @@ class LlmPropertiesService(
2526
}
2627

2728
fun getProviders(): List<LlmProvider> {
28-
val result = llmProperties.providers.toMutableList()
29+
val result = getMergedProviders().toMutableList()
2930
if (subscriptionActive()) {
30-
val hasTolgeeConfig = llmProperties.providers.find { it.type == LlmProviderType.TOLGEE } != null
31+
val hasTolgeeConfig = result.find { it.type == LlmProviderType.TOLGEE } != null
3132
if (!hasTolgeeConfig) {
3233
result.add(
3334
LlmProvider(
@@ -40,4 +41,61 @@ class LlmPropertiesService(
4041
}
4142
return result.filter { it.enabled }
4243
}
44+
45+
fun getMergedProviders(): List<LlmProvider> {
46+
val defaults = llmProperties.providerDefaults
47+
if (defaults.isEmpty()) {
48+
return llmProperties.providers.toList()
49+
}
50+
51+
val result = mutableListOf<LlmProvider>()
52+
val matchedDefaultNames = mutableSetOf<String>()
53+
54+
for (listEntry in llmProperties.providers) {
55+
val mapEntry = defaults[listEntry.name]
56+
if (mapEntry != null) {
57+
matchedDefaultNames.add(listEntry.name)
58+
result.add(mergeProviderWithDefaults(mapEntry, listEntry))
59+
} else {
60+
result.add(listEntry)
61+
}
62+
}
63+
64+
// Add map-only entries not matched by any list entry
65+
for ((name, mapEntry) in defaults) {
66+
if (name !in matchedDefaultNames) {
67+
result.add(mapEntry.toLlmProvider(name))
68+
}
69+
}
70+
71+
return result
72+
}
73+
74+
private fun mergeProviderWithDefaults(
75+
defaults: LlmProviderDefaults,
76+
listEntry: LlmProvider,
77+
): LlmProvider {
78+
val base = defaults.toLlmProvider(listEntry.name)
79+
// enabled is always taken from the list entry
80+
base.enabled = listEntry.enabled
81+
// Nullable fields: override only if list value is non-null
82+
listEntry.apiKey?.let { base.apiKey = it }
83+
listEntry.apiUrl?.let { base.apiUrl = it }
84+
listEntry.model?.let { base.model = it }
85+
listEntry.deployment?.let { base.deployment = it }
86+
listEntry.reasoningEffort?.let { base.reasoningEffort = it }
87+
listEntry.format?.let { base.format = it }
88+
listEntry.priority?.let { base.priority = it }
89+
listEntry.attempts?.let { base.attempts = it }
90+
listEntry.tokenPriceInCreditsInput?.let { base.tokenPriceInCreditsInput = it }
91+
listEntry.tokenPriceInCreditsOutput?.let { base.tokenPriceInCreditsOutput = it }
92+
// Non-nullable fields: override only if different from default
93+
if (listEntry.type != LlmProviderType.OPENAI) {
94+
base.type = listEntry.type
95+
}
96+
if (listEntry.maxTokens != LlmProvider.MAX_TOKENS_DEFAULT) {
97+
base.maxTokens = listEntry.maxTokens
98+
}
99+
return base
100+
}
43101
}

0 commit comments

Comments
 (0)