Skip to content

Commit d6772a7

Browse files
JanCizmarclaude
andcommitted
feat: resolve LLM provider fallbacks in Prompt API responses
When a provider is delisted and replaced by a successor with a fallback configured, the stored provider name becomes stale. Resolve it through the fallback chain at the service level so the frontend sees a valid provider name. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d44dab3 commit d6772a7

2 files changed

Lines changed: 42 additions & 4 deletions

File tree

ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/controllers/PromptController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ class PromptController(
114114
fun getPrompt(
115115
@PathVariable promptId: Long,
116116
): PromptModel {
117-
val result = promptService.findPrompt(projectHolder.project.id, promptId)
117+
val result = promptService.findPromptWithResolvedProvider(projectHolder.project.id, promptId)
118118
return promptModelAssembler.toModel(result)
119119
}
120120

ee/backend/app/src/main/kotlin/io/tolgee/ee/service/prompt/PromptServiceEeImpl.kt

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import io.tolgee.dtos.LlmParams
99
import io.tolgee.dtos.request.prompt.PromptDto
1010
import io.tolgee.dtos.request.prompt.PromptRunDto
1111
import io.tolgee.ee.component.PromptLazyMap
12+
import io.tolgee.ee.service.LlmProviderResolver
1213
import io.tolgee.ee.service.LlmProviderService
1314
import io.tolgee.ee.service.prompt.PromptResultParser.ParsedResult
1415
import io.tolgee.events.OnAfterMachineTranslationEvent
1516
import io.tolgee.events.OnBeforeMachineTranslationEvent
1617
import io.tolgee.exceptions.BadRequestException
1718
import io.tolgee.exceptions.FailedDependencyException
19+
import io.tolgee.exceptions.LlmProviderNotFoundException
1820
import io.tolgee.exceptions.LlmRateLimitedException
1921
import io.tolgee.exceptions.NotFoundException
2022
import io.tolgee.exceptions.TooManyRequestsException
@@ -26,6 +28,7 @@ import io.tolgee.service.PromptService
2628
import io.tolgee.service.key.KeyService
2729
import io.tolgee.service.machineTranslation.MtServiceConfigService
2830
import io.tolgee.service.project.ProjectService
31+
import jakarta.persistence.EntityManager
2932
import org.springframework.context.ApplicationContext
3033
import org.springframework.context.annotation.Lazy
3134
import org.springframework.context.annotation.Primary
@@ -49,13 +52,15 @@ class PromptServiceEeImpl(
4952
private val applicationContext: ApplicationContext,
5053
private val promptParamsHelper: PromptParamsHelper,
5154
private val objectMapper: ObjectMapper,
55+
private val llmProviderResolver: LlmProviderResolver,
56+
private val entityManager: EntityManager,
5257
) : PromptService {
5358
fun getAllPaged(
5459
projectId: Long,
5560
pageable: Pageable,
5661
search: String?,
5762
): Page<Prompt> {
58-
return promptRepository.getAllPaged(projectId, pageable, search)
63+
return promptRepository.getAllPaged(projectId, pageable, search).map { withResolvedProviderName(it) }
5964
}
6065

6166
fun createPrompt(
@@ -71,7 +76,7 @@ class PromptServiceEeImpl(
7176
basicPromptOptions = dto.basicPromptOptions?.toTypedArray(),
7277
)
7378
promptRepository.save(prompt)
74-
return prompt
79+
return withResolvedProviderName(prompt)
7580
}
7681

7782
override fun findPromptOrDefaultDto(
@@ -98,6 +103,13 @@ class PromptServiceEeImpl(
98103
return promptRepository.findPrompt(projectId, promptId) ?: throw NotFoundException(Message.PROMPT_NOT_FOUND)
99104
}
100105

106+
fun findPromptWithResolvedProvider(
107+
projectId: Long,
108+
promptId: Long,
109+
): Prompt {
110+
return withResolvedProviderName(findPrompt(projectId, promptId))
111+
}
112+
101113
override fun deleteAllByProjectId(projectId: Long) {
102114
return promptRepository.deleteAllByProjectId(projectId)
103115
}
@@ -113,7 +125,7 @@ class PromptServiceEeImpl(
113125
prompt.providerName = dto.providerName
114126
prompt.basicPromptOptions = dto.basicPromptOptions?.toTypedArray()
115127
promptRepository.save(prompt)
116-
return prompt
128+
return withResolvedProviderName(prompt)
117129
}
118130

119131
fun deletePrompt(
@@ -272,4 +284,30 @@ class PromptServiceEeImpl(
272284
OnAfterMachineTranslationEvent(this, organizationId, actualPriceInCents),
273285
)
274286
}
287+
288+
/**
289+
* Resolves the provider name through the fallback chain before returning to the API.
290+
*
291+
* Prompts store the original provider name in the DB. When a provider is delisted and
292+
* replaced by a successor (e.g. "gpt-4o" -> "gpt-4.1") with a fallback configured,
293+
* the stored name becomes stale. This method resolves it so the frontend sees a provider
294+
* name that actually exists among the available providers.
295+
*
296+
* The entity is detached before mutation to prevent OSIV from flushing the resolved name
297+
* back to the DB — the stored value must remain the original.
298+
*/
299+
private fun withResolvedProviderName(prompt: Prompt): Prompt {
300+
if (prompt.providerName.isEmpty()) return prompt
301+
// Access lazy association while still managed
302+
val organizationId = prompt.project.organizationOwner.id
303+
val resolvedName =
304+
try {
305+
llmProviderResolver.resolveProviderName(organizationId, prompt.providerName)
306+
} catch (e: LlmProviderNotFoundException) {
307+
prompt.providerName
308+
}
309+
entityManager.detach(prompt)
310+
prompt.providerName = resolvedName
311+
return prompt
312+
}
275313
}

0 commit comments

Comments
 (0)