Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ open class LlmProviderSimpleModel(
var name: String,
var source: String?,
var type: LlmProviderType,
var tokenPriceInCreditsInput: Double?,
var tokenPriceInCreditsOutput: Double?,
) : RepresentationModel<LlmProviderModel>()
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ class LlmProperties : MachineTranslationServiceProperties {

@DocProperty(
description = """
List of LLM providers. Example:

List of LLM providers. When `provider-defaults` is also set, list entries are merged with the
matching map entry (by name). List values override map defaults only when explicitly set
(non-null for nullable fields, non-default for `type` / `maxTokens`).
`enabled` is always taken from the list entry.

``` yaml
providers:
- name: openai-gpt-4o-mini
Expand All @@ -34,9 +37,9 @@ class LlmProperties : MachineTranslationServiceProperties {
model: gpt-4o-mini
format: "json_schema"
```

or using environment variables:

```
TOLGEE_LLM_PROVIDERS_0_NAME=MySuperDuperAI
TOLGEE_LLM_PROVIDERS_0_TYPE=OPENAI
Expand All @@ -45,12 +48,45 @@ class LlmProperties : MachineTranslationServiceProperties {
TOLGEE_LLM_PROVIDERS_0_MODEL=gpt-4o-mini
TOLGEE_LLM_PROVIDERS_0_FORMAT=json_schema
```

Check [llm providers documentation](/platform/projects_and_organizations/llm-providers#self-hosted-server-configuration) for more information.
""",
)
var providers: MutableList<LlmProvider> = mutableListOf()

@DocProperty(
description = """
Map of provider defaults keyed by provider name. Use this to separate non-secret configuration
(model, prices, type) from secrets (API keys) in Kubernetes deployments.

``` yaml
provider-defaults:
gpt-5-mini:
type: OPENAI
model: gpt-5-mini
token-price-in-credits-input: 2.0
token-price-in-credits-output: 1.5
```

or using environment variables (in a ConfigMap):

```
TOLGEE_LLM_PROVIDER_DEFAULTS_GPT_5_MINI_TYPE=OPENAI
TOLGEE_LLM_PROVIDER_DEFAULTS_GPT_5_MINI_MODEL=gpt-5-mini
TOLGEE_LLM_PROVIDER_DEFAULTS_GPT_5_MINI_TOKEN_PRICE_IN_CREDITS_INPUT=2.0
TOLGEE_LLM_PROVIDER_DEFAULTS_GPT_5_MINI_TOKEN_PRICE_IN_CREDITS_OUTPUT=1.5
```

Then supply only the API key via the `providers` list (in a Secret):

```
TOLGEE_LLM_PROVIDERS_0_NAME=gpt-5-mini
TOLGEE_LLM_PROVIDERS_0_API_KEY=sk-proj-...
```
""",
)
var providerDefaults: MutableMap<String, LlmProviderDefaults> = mutableMapOf()

@DocProperty(
description = """
Named fallback mapping. When a provider is not found, Tolgee will try the fallback provider.
Expand Down Expand Up @@ -133,4 +169,61 @@ class LlmProperties : MachineTranslationServiceProperties {
const val MAX_TOKENS_DEFAULT: Long = 2000
}
}

class LlmProviderDefaults(
@DocProperty(description = "Enable/disable provider")
var enabled: Boolean = true,
@DocProperty(description = "Provider type, an API type")
var type: LlmProviderType = LlmProviderType.OPENAI,
@DocProperty(description = "Provider API Key (optional for some providers)")
var apiKey: String? = null,
@DocProperty(description = "Provider API Url")
var apiUrl: String? = null,
@DocProperty(description = "Provider model (optional for some providers)")
var model: String? = null,
@DocProperty(description = "Provider deployment (optional for some providers)")
var deployment: String? = null,
@DocProperty(
description = """Maximum number of tokens to generate.
`max_completion_tokens` option for OpenAI API.
`max_tokens` for Anthropic API.""",
)
var maxTokens: Long? = null,
@DocProperty(description = "ChatGPT reasoning effort")
var reasoningEffort: String? = null,
@DocProperty(description = "Set to `json_schema` if the API supports JSON Schema")
var format: String? = null,
@DocProperty(
description = "Load-balancing instruction HIGH = used for suggestions, LOW = used for batch operations",
)
var priority: LlmProviderPriority? = null,
@DocProperty(
description =
"Specify attempts timeout(s) (Example: [30, 30] - Tolgee will make two attempts, each with timeout of 30s)",
)
var attempts: List<Int>? = null,
@DocProperty(hidden = true)
var tokenPriceInCreditsInput: Double? = null,
@DocProperty(hidden = true)
var tokenPriceInCreditsOutput: Double? = null,
) {
fun toLlmProvider(name: String): LlmProvider {
return LlmProvider(
enabled = enabled,
name = name,
type = type,
apiKey = apiKey,
apiUrl = apiUrl,
model = model,
deployment = deployment,
maxTokens = maxTokens ?: LlmProvider.MAX_TOKENS_DEFAULT,
reasoningEffort = reasoningEffort,
format = format,
priority = priority,
attempts = attempts,
tokenPriceInCreditsInput = tokenPriceInCreditsInput,
tokenPriceInCreditsOutput = tokenPriceInCreditsOutput,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.tolgee.service
import io.tolgee.api.EeSubscriptionProvider
import io.tolgee.configuration.tolgee.machineTranslation.LlmProperties
import io.tolgee.configuration.tolgee.machineTranslation.LlmProperties.LlmProvider
import io.tolgee.configuration.tolgee.machineTranslation.LlmProperties.LlmProviderDefaults
import io.tolgee.exceptions.InvalidStateException
import io.tolgee.model.enums.LlmProviderType
import org.springframework.stereotype.Service
Expand All @@ -25,9 +26,9 @@ class LlmPropertiesService(
}

fun getProviders(): List<LlmProvider> {
val result = llmProperties.providers.toMutableList()
val result = getMergedProviders().toMutableList()
if (subscriptionActive()) {
val hasTolgeeConfig = llmProperties.providers.find { it.type == LlmProviderType.TOLGEE } != null
val hasTolgeeConfig = result.find { it.type == LlmProviderType.TOLGEE } != null
if (!hasTolgeeConfig) {
result.add(
LlmProvider(
Expand All @@ -40,4 +41,61 @@ class LlmPropertiesService(
}
return result.filter { it.enabled }
}

fun getMergedProviders(): List<LlmProvider> {
val defaults = llmProperties.providerDefaults
if (defaults.isEmpty()) {
return llmProperties.providers.toList()
}

val result = mutableListOf<LlmProvider>()
val matchedDefaultNames = mutableSetOf<String>()

for (listEntry in llmProperties.providers) {
val mapEntry = defaults[listEntry.name]
if (mapEntry != null) {
matchedDefaultNames.add(listEntry.name)
result.add(mergeProviderWithDefaults(mapEntry, listEntry))
} else {
result.add(listEntry)
}
}

// Add map-only entries not matched by any list entry
for ((name, mapEntry) in defaults) {
if (name !in matchedDefaultNames) {
result.add(mapEntry.toLlmProvider(name))
}
}

return result
}

private fun mergeProviderWithDefaults(
defaults: LlmProviderDefaults,
listEntry: LlmProvider,
): LlmProvider {
val base = defaults.toLlmProvider(listEntry.name)
// enabled is always taken from the list entry
base.enabled = listEntry.enabled
// Nullable fields: override only if list value is non-null
listEntry.apiKey?.let { base.apiKey = it }
listEntry.apiUrl?.let { base.apiUrl = it }
listEntry.model?.let { base.model = it }
listEntry.deployment?.let { base.deployment = it }
listEntry.reasoningEffort?.let { base.reasoningEffort = it }
listEntry.format?.let { base.format = it }
listEntry.priority?.let { base.priority = it }
listEntry.attempts?.let { base.attempts = it }
listEntry.tokenPriceInCreditsInput?.let { base.tokenPriceInCreditsInput = it }
listEntry.tokenPriceInCreditsOutput?.let { base.tokenPriceInCreditsOutput = it }
// Non-nullable fields: override only if different from default
if (listEntry.type != LlmProviderType.OPENAI) {
base.type = listEntry.type
}
if (listEntry.maxTokens != LlmProvider.MAX_TOKENS_DEFAULT) {
base.maxTokens = listEntry.maxTokens
}
return base
}
}
Loading
Loading