From b447293886c1d4522f3e3a6c1e56e3d2c0d1c49e Mon Sep 17 00:00:00 2001 From: Hai Nguyen <3423575+haiphucnguyen@users.noreply.github.com> Date: Sun, 26 Jul 2026 18:01:52 -0700 Subject: [PATCH] Update --- .../askimo/ui/common/theme/AppComponents.kt | 4 + .../main/resources/i18n/messages.properties | 6 ++ .../resources/i18n/messages_de.properties | 6 ++ .../resources/i18n/messages_es.properties | 6 ++ .../resources/i18n/messages_fr.properties | 6 ++ .../resources/i18n/messages_ja_JP.properties | 6 ++ .../resources/i18n/messages_ko_KR.properties | 6 ++ .../resources/i18n/messages_pt_BR.properties | 6 ++ .../resources/i18n/messages_vi_VN.properties | 6 ++ .../resources/i18n/messages_zh_CN.properties | 6 ++ .../resources/i18n/messages_zh_TW.properties | 6 ++ .../settings/AIProviderSettingsSection.kt | 87 +++++++++++++++++++ .../settings/AdvancedSettingsSection.kt | 4 +- .../desktop/settings/SettingsConfigDialog.kt | 12 +++ .../askimo/core/providers/ProviderSettings.kt | 12 +++ .../io/askimo/core/providers/SettingField.kt | 12 +++ .../anthropic/AnthropicModelFactory.kt | 19 ++-- .../providers/anthropic/AnthropicSettings.kt | 27 ++++++ 18 files changed, 227 insertions(+), 10 deletions(-) diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/common/theme/AppComponents.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/common/theme/AppComponents.kt index 93f7a413d..686a83588 100644 --- a/desktop-shared/src/main/kotlin/io/askimo/ui/common/theme/AppComponents.kt +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/common/theme/AppComponents.kt @@ -230,6 +230,7 @@ object AppComponents { focusedTextColor: Color = MaterialTheme.colorScheme.onSurface, unfocusedTextColor: Color = MaterialTheme.colorScheme.onSurface, cursorColor: Color = MaterialTheme.colorScheme.onSurface, + containerColor: Color = MaterialTheme.colorScheme.surface, ): TextFieldColors = OutlinedTextFieldDefaults.colors( focusedBorderColor = focusedBorderColor, unfocusedBorderColor = unfocusedBorderColor, @@ -238,6 +239,9 @@ object AppComponents { focusedTextColor = focusedTextColor, unfocusedTextColor = unfocusedTextColor, cursorColor = cursorColor, + focusedContainerColor = containerColor, + unfocusedContainerColor = containerColor, + disabledContainerColor = containerColor, ) /** diff --git a/desktop-shared/src/main/resources/i18n/messages.properties b/desktop-shared/src/main/resources/i18n/messages.properties index 748b66a28..3f6bd4409 100644 --- a/desktop-shared/src/main/resources/i18n/messages.properties +++ b/desktop-shared/src/main/resources/i18n/messages.properties @@ -949,6 +949,12 @@ provider.openai.setup.help=💡To use OpenAI, you need to provide an API key.\n\ provider.anthropic.apikey.description=Your Anthropic API key from https://console.anthropic.com/ provider.anthropic.apikey.stored=API key already stored securely. Leave blank to keep existing key, or enter a new one to update. provider.anthropic.setup.help=💡To use Anthropic, you need to provide an API key.\n\n1. Get your API key from: https://console.anthropic.com/\n2. Then set it in the Settings or using: :change-settings +provider.anthropic.max_tokens.label=Max Tokens +provider.anthropic.max_tokens.description=Maximum tokens the model can generate per response (thinking + answer combined). +provider.anthropic.thinking_budget_tokens.label=Thinking Budget Tokens +provider.anthropic.thinking_budget_tokens.description=Max tokens Anthropic can spend on thinking. Set to 0 to let the model decide automatically (recommended). +provider.anthropic.thinking_max_tokens.label=Thinking Max Tokens +provider.anthropic.thinking_max_tokens.description=Total token cap when thinking is active (thinking + answer). Set to 0 to use Max Tokens instead. # Provider Configuration - Gemini provider.gemini.apikey.description=Your Google AI Studio API key from https://aistudio.google.com/app/apikey diff --git a/desktop-shared/src/main/resources/i18n/messages_de.properties b/desktop-shared/src/main/resources/i18n/messages_de.properties index 72d67c25f..2641ddcf5 100644 --- a/desktop-shared/src/main/resources/i18n/messages_de.properties +++ b/desktop-shared/src/main/resources/i18n/messages_de.properties @@ -909,6 +909,12 @@ provider.openai.setup.help=💡 Für die Nutzung von OpenAI benötigen Sie einen provider.anthropic.apikey.description=Ihr Anthropic API-Schlüssel: https://console.anthropic.com/ provider.anthropic.apikey.stored=API-Schlüssel ist bereits gespeichert. provider.anthropic.setup.help=💡 Für die Nutzung von Anthropic benötigen Sie einen API-Schlüssel.\n\n1. Hier erhalten: https://console.anthropic.com/\n2. In den Einstellungen eintragen oder :change-settings verwenden +provider.anthropic.max_tokens.label=Max Tokens +provider.anthropic.max_tokens.description=Maximale Anzahl an Tokens, die das Modell pro Antwort generieren kann (Denken + Antwort kombiniert). +provider.anthropic.thinking_budget_tokens.label=Thinking Budget Tokens +provider.anthropic.thinking_budget_tokens.description=Maximale Anzahl an Tokens, die Anthropic für den Denkprozess verwenden kann. Auf 0 setzen, damit das Modell dies automatisch entscheidet (empfohlen). +provider.anthropic.thinking_max_tokens.label=Thinking Max Tokens +provider.anthropic.thinking_max_tokens.description=Gesamtes Token-Limit bei aktivem Denkprozess (Denken + Antwort). Auf 0 setzen, um stattdessen "Max Tokens" zu verwenden. # Provider Configuration - Gemini provider.gemini.apikey.description=Ihr Google AI Studio API-Schlüssel: https://aistudio.google.com/app/apikey diff --git a/desktop-shared/src/main/resources/i18n/messages_es.properties b/desktop-shared/src/main/resources/i18n/messages_es.properties index a76830c52..097dde212 100644 --- a/desktop-shared/src/main/resources/i18n/messages_es.properties +++ b/desktop-shared/src/main/resources/i18n/messages_es.properties @@ -907,6 +907,12 @@ provider.openai.setup.help=💡 Para usar OpenAI necesitas una clave API.\n\n1. provider.anthropic.apikey.description=Tu clave API de Anthropic: https://console.anthropic.com/ provider.anthropic.apikey.stored=Clave API ya almacenada. provider.anthropic.setup.help=💡 Para usar Anthropic, necesitas una clave API.\n\n1. Consíguela en: https://console.anthropic.com/\n2. Configúrala en los ajustes o con :change-settings +provider.anthropic.max_tokens.label=Max Tokens +provider.anthropic.max_tokens.description=Máximo de tokens que el modelo puede generar por respuesta (pensamiento + respuesta combinados). +provider.anthropic.thinking_budget_tokens.label=Tokens de presupuesto de pensamiento +provider.anthropic.thinking_budget_tokens.description=Máximo de tokens que Anthropic puede dedicar al pensamiento. Configura en 0 para dejar que el modelo decida automáticamente (recomendado). +provider.anthropic.thinking_max_tokens.label=Máximo de tokens de pensamiento +provider.anthropic.thinking_max_tokens.description=Límite total de tokens cuando el pensamiento está activo (pensamiento + respuesta). Configura en 0 para usar Max Tokens en su lugar. # Provider Configuration - Gemini provider.gemini.apikey.description=Tu clave API de Google AI Studio: https://aistudio.google.com/app/apikey diff --git a/desktop-shared/src/main/resources/i18n/messages_fr.properties b/desktop-shared/src/main/resources/i18n/messages_fr.properties index 7f75a4c09..159aba040 100644 --- a/desktop-shared/src/main/resources/i18n/messages_fr.properties +++ b/desktop-shared/src/main/resources/i18n/messages_fr.properties @@ -907,6 +907,12 @@ provider.openai.setup.help=💡 Pour utiliser OpenAI, une clé API est nécessai provider.anthropic.apikey.description=Votre clé API Anthropic : https://console.anthropic.com/ provider.anthropic.apikey.stored=Clé API déjà stockée. provider.anthropic.setup.help=💡 Pour utiliser Anthropic, une clé API est nécessaire.\n\n1. Obtenez-la ici : https://console.anthropic.com/\n2. Configurez-la dans les paramètres ou via :change-settings +provider.anthropic.max_tokens.label=Max Tokens +provider.anthropic.max_tokens.description=Nombre maximal de jetons que le modèle peut générer par réponse (réflexion + réponse combinées). +provider.anthropic.thinking_budget_tokens.label=Jetons de budget de réflexion +provider.anthropic.thinking_budget_tokens.description=Nombre maximal de jetons qu'Anthropic peut consacrer à la réflexion. Définissez sur 0 pour laisser le modèle décider automatiquement (recommandé). +provider.anthropic.thinking_max_tokens.label=Max de jetons de réflexion +provider.anthropic.thinking_max_tokens.description=Plafond total de jetons lorsque la réflexion est activée (réflexion + réponse). Définissez sur 0 pour utiliser Max Tokens à la place. # Provider Configuration - Gemini provider.gemini.apikey.description=Votre clé API Google AI Studio : https://aistudio.google.com/app/apikey diff --git a/desktop-shared/src/main/resources/i18n/messages_ja_JP.properties b/desktop-shared/src/main/resources/i18n/messages_ja_JP.properties index d0af266cc..3fb2ac4b9 100644 --- a/desktop-shared/src/main/resources/i18n/messages_ja_JP.properties +++ b/desktop-shared/src/main/resources/i18n/messages_ja_JP.properties @@ -907,6 +907,12 @@ provider.openai.setup.help=💡 OpenAI を使うには API キーが必要です provider.anthropic.apikey.description=Anthropic API キー: https://console.anthropic.com/ provider.anthropic.apikey.stored=API キーは既に保存されています。 provider.anthropic.setup.help=💡 Anthropic を使うには API キーが必要です。\n\n1. https://console.anthropic.com/ で取得\n2. 設定で構成するか :change-settings を使用 +provider.anthropic.max_tokens.label=最大トークン数 +provider.anthropic.max_tokens.description=モデルが応答ごとに生成できる最大トークン数(思考 + 回答の合計)。 +provider.anthropic.thinking_budget_tokens.label=思考予算トークン数 +provider.anthropic.thinking_budget_tokens.description=Anthropicが思考に費やすことのできる最大トークン数。0に設定すると、モデルが自動的に決定します(推奨)。 +provider.anthropic.thinking_max_tokens.label=思考時最大トークン数 +provider.anthropic.thinking_max_tokens.description=思考がアクティブな時の合計トークン上限(思考 + 回答)。0に設定すると、代わりに「Max Tokens」が使用されます。 # Provider Configuration - Gemini provider.gemini.apikey.description=Google AI Studio API キー: https://aistudio.google.com/app/apikey diff --git a/desktop-shared/src/main/resources/i18n/messages_ko_KR.properties b/desktop-shared/src/main/resources/i18n/messages_ko_KR.properties index 27e9e7a09..eb1f795f9 100644 --- a/desktop-shared/src/main/resources/i18n/messages_ko_KR.properties +++ b/desktop-shared/src/main/resources/i18n/messages_ko_KR.properties @@ -907,6 +907,12 @@ provider.openai.setup.help=💡 OpenAI 사용을 위해 API 키가 필요합니 provider.anthropic.apikey.description=Anthropic API 키: https://console.anthropic.com/ provider.anthropic.apikey.stored=API 키가 이미 저장됨. provider.anthropic.setup.help=💡 Anthropic 사용을 위해 API 키가 필요합니다.\n\n1. https://console.anthropic.com/ 에서 키를 받으세요\n2. 설정 또는 :change-settings 명령으로 구성하세요 +provider.anthropic.max_tokens.label=최대 토큰 수 +provider.anthropic.max_tokens.description=모델이 응답당 생성할 수 있는 최대 토큰 수 (생각 + 답변 합계). +provider.anthropic.thinking_budget_tokens.label=생각 예산 토큰 수 +provider.anthropic.thinking_budget_tokens.description=Anthropic이 생각하는 데 사용할 수 있는 최대 토큰 수입니다. 0으로 설정하면 모델이 자동으로 결정합니다 (권장). +provider.anthropic.thinking_max_tokens.label=생각 최대 토큰 수 +provider.anthropic.thinking_max_tokens.description=생각 기능이 활성화되었을 때의 총 토큰 제한 (생각 + 답변). 0으로 설정하면 대신 최대 토큰 수(Max Tokens)를 사용합니다. # Provider Configuration - Gemini provider.gemini.apikey.description=Google AI Studio API 키: https://aistudio.google.com/app/apikey diff --git a/desktop-shared/src/main/resources/i18n/messages_pt_BR.properties b/desktop-shared/src/main/resources/i18n/messages_pt_BR.properties index 8222f5d8c..8ec92557e 100644 --- a/desktop-shared/src/main/resources/i18n/messages_pt_BR.properties +++ b/desktop-shared/src/main/resources/i18n/messages_pt_BR.properties @@ -910,6 +910,12 @@ provider.openai.setup.help=💡 Para usar o OpenAI, forneça sua API Key.\n\n1. provider.anthropic.apikey.description=Sua API Key do Anthropic: https://console.anthropic.com/ provider.anthropic.apikey.stored=API Key já armazenada. provider.anthropic.setup.help=💡 Para usar o Anthropic, forneça a API Key.\n\n1. Obtenha em: https://console.anthropic.com/\n2. Configure nas Configurações ou usando :change-settings +provider.anthropic.max_tokens.label=Max Tokens +provider.anthropic.max_tokens.description=Máximo de tokens que o modelo pode gerar por resposta (pensamento + resposta combinados). +provider.anthropic.thinking_budget_tokens.label=Tokens de orçamento de pensamento +provider.anthropic.thinking_budget_tokens.description=Máximo de tokens que a Anthropic pode gastar pensando. Defina como 0 para permitir que o modelo decida automaticamente (recomendado). +provider.anthropic.thinking_max_tokens.label=Máximo de tokens de pensamento +provider.anthropic.thinking_max_tokens.description=Limite total de tokens quando o pensamento está ativo (pensamento + resposta). Defina como 0 para usar Max Tokens em vez disso. # Provider Configuration - Gemini provider.gemini.apikey.description=Sua API Key do Google AI Studio: https://aistudio.google.com/app/apikey diff --git a/desktop-shared/src/main/resources/i18n/messages_vi_VN.properties b/desktop-shared/src/main/resources/i18n/messages_vi_VN.properties index f3121ff04..bd3078ef0 100644 --- a/desktop-shared/src/main/resources/i18n/messages_vi_VN.properties +++ b/desktop-shared/src/main/resources/i18n/messages_vi_VN.properties @@ -906,6 +906,12 @@ provider.openai.setup.help=💡 Để sử dụng OpenAI, bạn cần API key.\n provider.anthropic.apikey.description=API key Anthropic từ https://console.anthropic.com/ provider.anthropic.apikey.stored=API key đã lưu. Để trống để giữ nguyên hoặc nhập mới để cập nhật. provider.anthropic.setup.help=💡 Để sử dụng Anthropic, bạn cần API key.\n\n1. Lấy từ https://console.anthropic.com/\n2. Thiết lập trong Cài đặt hoặc dùng :change-settings +provider.anthropic.max_tokens.label=Số Token Tối đa +provider.anthropic.max_tokens.description=Số lượng token tối đa mà mô hình có thể tạo cho mỗi phản hồi (suy nghĩ + câu trả lời được kết hợp). +provider.anthropic.thinking_budget_tokens.label=Token Ngân sách Suy nghĩ +provider.anthropic.thinking_budget_tokens.description=Số lượng token tối đa mà Anthropic có thể dành cho việc suy nghĩ. Đặt là 0 để để mô hình tự quyết định (khuyên dùng). +provider.anthropic.thinking_max_tokens.label=Token Tối đa cho Suy nghĩ +provider.anthropic.thinking_max_tokens.description=Giới hạn token tổng khi tính năng suy nghĩ đang hoạt động (suy nghĩ + câu trả lời). Đặt là 0 để sử dụng Số Token Tối đa thay thế. # Provider Configuration - Gemini provider.gemini.apikey.description=API key Google AI Studio từ https://aistudio.google.com/app/apikey diff --git a/desktop-shared/src/main/resources/i18n/messages_zh_CN.properties b/desktop-shared/src/main/resources/i18n/messages_zh_CN.properties index bc0eb89bd..4b096b2e8 100644 --- a/desktop-shared/src/main/resources/i18n/messages_zh_CN.properties +++ b/desktop-shared/src/main/resources/i18n/messages_zh_CN.properties @@ -907,6 +907,12 @@ provider.openai.setup.help=使用 OpenAI 需要 API Key。\n\n1. 获取:https: provider.anthropic.apikey.description=Anthropic API Key:https://console.anthropic.com/ provider.anthropic.apikey.stored=API Key 已保存。 provider.anthropic.setup.help=使用 Anthropic 需要 API Key。\n\n1. 获取:https://console.anthropic.com/\n2. 在设置中配置或使用 :change-settings +provider.anthropic.max_tokens.label=最大 Token 数 +provider.anthropic.max_tokens.description=模型每次响应可生成的最大 Token 数(思考 + 回答的总和)。 +provider.anthropic.thinking_budget_tokens.label=思考预算 Token 数 +provider.anthropic.thinking_budget_tokens.description=Anthropic 用于思考的最大 Token 数。设置为 0 可让模型自动决定(推荐)。 +provider.anthropic.thinking_max_tokens.label=思考时最大 Token 数 +provider.anthropic.thinking_max_tokens.description=启用思考功能时的总 Token 上限(思考 + 回答)。设置为 0 以使用“最大 Token 数”代替。 # Provider Configuration - Gemini provider.gemini.apikey.description=Google AI Studio API Key:https://aistudio.google.com/app/apikey diff --git a/desktop-shared/src/main/resources/i18n/messages_zh_TW.properties b/desktop-shared/src/main/resources/i18n/messages_zh_TW.properties index 794bf65f0..afb09f7c8 100644 --- a/desktop-shared/src/main/resources/i18n/messages_zh_TW.properties +++ b/desktop-shared/src/main/resources/i18n/messages_zh_TW.properties @@ -907,6 +907,12 @@ provider.openai.setup.help=💡 若要使用 OpenAI,您必須提供 API 金鑰 provider.anthropic.apikey.description=您的 Anthropic API 金鑰(https://console.anthropic.com/) provider.anthropic.apikey.stored=API 金鑰已儲存。留白代表保持現有設定。 provider.anthropic.setup.help=💡 若要使用 Anthropic,請提供 API 金鑰。\n\n1. 取得位置:https://console.anthropic.com/\n2. 在設定中輸入或使用 :change-settings +provider.anthropic.max_tokens.label=最大 Token 數 +provider.anthropic.max_tokens.description=模型每次回應可生成的最大 Token 數(思考 + 回答的總和)。 +provider.anthropic.thinking_budget_tokens.label=思考預算 Token 數 +provider.anthropic.thinking_budget_tokens.description=Anthropic 用於思考的最大 Token 數。設定為 0 可讓模型自動決定(推薦)。 +provider.anthropic.thinking_max_tokens.label=思考時最大 Token 數 +provider.anthropic.thinking_max_tokens.description=啟用思考功能時的總 Token 上限(思考 + 回答)。設定為 0 以使用「最大 Token 數」代替。 # Provider Configuration - Gemini provider.gemini.apikey.description=您的 Google AI Studio API 金鑰(https://aistudio.google.com/app/apikey) diff --git a/desktop/src/main/kotlin/io/askimo/desktop/settings/AIProviderSettingsSection.kt b/desktop/src/main/kotlin/io/askimo/desktop/settings/AIProviderSettingsSection.kt index e60ba9b2e..4c08e518a 100644 --- a/desktop/src/main/kotlin/io/askimo/desktop/settings/AIProviderSettingsSection.kt +++ b/desktop/src/main/kotlin/io/askimo/desktop/settings/AIProviderSettingsSection.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollbarAdapter +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add @@ -41,9 +42,11 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.askimo.core.AppConstants.DOMAIN +import io.askimo.core.i18n.LocalizationManager import io.askimo.core.providers.ChatModelFactory import io.askimo.core.providers.ModelDTO import io.askimo.core.providers.ModelProvider @@ -317,6 +320,20 @@ private fun providerModelConfigCard(instance: ProviderInstance, viewModel: AIPro ) } + // Provider-specific configurable fields (e.g. maxTokens for Anthropic) + val configurableFields = instance.settings.getConfigurableFields { key -> LocalizationManager.getString(key) } + if (configurableFields.isNotEmpty()) { + HorizontalDivider(color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.15f)) + configurableFields.forEach { field -> + providerConfigurableField( + field = field, + onValueChange = { newValue -> + viewModel.updateInstanceModelOverride(instance.id, field.name, newValue) + }, + ) + } + } + // Embedding model — only for supported providers if (supportsEmbedding) { HorizontalDivider(color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.15f)) @@ -591,3 +608,73 @@ private fun providerModelTypePickerDialog( } } } + +/** + * Renders a single provider-specific configurable field (NumberField or TextField). + * Called generically for every entry in [ProviderSettings.getConfigurableFields] — + * no per-provider branching needed in the UI. + */ +@Composable +private fun providerConfigurableField( + field: SettingField, + onValueChange: (String) -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.weight(1f).padding(end = Spacing.large), + verticalArrangement = Arrangement.spacedBy(Spacing.extraSmall), + ) { + Text( + text = field.label, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer, + ) + Text( + text = field.description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.6f), + ) + } + + when (field) { + is SettingField.NumberField -> { + var text by remember(field.value) { mutableStateOf(field.value.toString()) } + OutlinedTextField( + value = text, + onValueChange = { newVal -> + text = newVal + newVal.toIntOrNull()?.let { onValueChange(it.toString()) } + }, + modifier = Modifier.widthIn(min = 100.dp, max = 160.dp), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + colors = AppComponents.outlinedTextFieldColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) + } + + is SettingField.TextField -> { + var text by remember(field.value) { mutableStateOf(field.value) } + OutlinedTextField( + value = text, + onValueChange = { newVal -> + text = newVal + onValueChange(newVal) + }, + modifier = Modifier.widthIn(min = 100.dp, max = 200.dp), + singleLine = true, + colors = AppComponents.outlinedTextFieldColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) + } + + else -> Unit + } + } +} diff --git a/desktop/src/main/kotlin/io/askimo/desktop/settings/AdvancedSettingsSection.kt b/desktop/src/main/kotlin/io/askimo/desktop/settings/AdvancedSettingsSection.kt index a07e5b28d..c053474ec 100644 --- a/desktop/src/main/kotlin/io/askimo/desktop/settings/AdvancedSettingsSection.kt +++ b/desktop/src/main/kotlin/io/askimo/desktop/settings/AdvancedSettingsSection.kt @@ -179,9 +179,7 @@ private fun logLevelCard() { modifier = Modifier .fillMaxWidth() .clickableCard { logLevelDropdownExpanded = true }, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface, - ), + colors = CardDefaults.cardColors(), ) { Row( modifier = Modifier diff --git a/desktop/src/main/kotlin/io/askimo/desktop/settings/SettingsConfigDialog.kt b/desktop/src/main/kotlin/io/askimo/desktop/settings/SettingsConfigDialog.kt index 73a58fb1b..9b7f0462b 100644 --- a/desktop/src/main/kotlin/io/askimo/desktop/settings/SettingsConfigDialog.kt +++ b/desktop/src/main/kotlin/io/askimo/desktop/settings/SettingsConfigDialog.kt @@ -122,6 +122,18 @@ fun settingsConfigDialog( } } } + + is SettingField.NumberField -> { + OutlinedTextField( + value = field.value.toString(), + onValueChange = { viewModel.updateSettingsField(field.name, it) }, + label = { Text(field.label) }, + placeholder = { Text(field.description) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + colors = AppComponents.outlinedTextFieldColors(), + ) + } } } } diff --git a/shared/src/main/kotlin/io/askimo/core/providers/ProviderSettings.kt b/shared/src/main/kotlin/io/askimo/core/providers/ProviderSettings.kt index 8848db227..2f3d23d75 100644 --- a/shared/src/main/kotlin/io/askimo/core/providers/ProviderSettings.kt +++ b/shared/src/main/kotlin/io/askimo/core/providers/ProviderSettings.kt @@ -70,6 +70,18 @@ interface ProviderSettings { @JsonIgnore fun getFields(): List + /** + * Returns provider-specific tuning fields that users can configure per instance + * (e.g. maxTokens, thinkingBudgetTokens for Anthropic). + * + * Default is empty — only providers with extra tuneable params need to override this. + * The UI renders these generically in the model config card without any per-provider branching. + * + * @param messageResolver resolves i18n keys to localized strings (same contract as [getConfigFields]). + */ + @JsonIgnore + fun getConfigurableFields(messageResolver: (String) -> String): List = emptyList() + fun updateField(fieldName: String, value: String): ProviderSettings @JsonIgnore diff --git a/shared/src/main/kotlin/io/askimo/core/providers/SettingField.kt b/shared/src/main/kotlin/io/askimo/core/providers/SettingField.kt index bf0643b55..c844bde6b 100644 --- a/shared/src/main/kotlin/io/askimo/core/providers/SettingField.kt +++ b/shared/src/main/kotlin/io/askimo/core/providers/SettingField.kt @@ -20,6 +20,11 @@ sealed class SettingField { const val VISION_MODEL = "visionModel" const val IMAGE_MODEL = "imageModel" const val EMBEDDING_MODEL = "embeddingModel" + + // Provider-specific configurable fields + const val MAX_TOKENS = "maxTokens" + const val THINKING_BUDGET_TOKENS = "thinkingBudgetTokens" + const val THINKING_MAX_TOKENS = "thinkingMaxTokens" } data class TextField( @@ -30,6 +35,13 @@ sealed class SettingField { val isPassword: Boolean = false, ) : SettingField() + data class NumberField( + override val name: String, + override val label: String, + override val description: String, + val value: Int, + ) : SettingField() + data class EnumField( override val name: String, override val label: String, diff --git a/shared/src/main/kotlin/io/askimo/core/providers/anthropic/AnthropicModelFactory.kt b/shared/src/main/kotlin/io/askimo/core/providers/anthropic/AnthropicModelFactory.kt index d2807a575..e023c4697 100644 --- a/shared/src/main/kotlin/io/askimo/core/providers/anthropic/AnthropicModelFactory.kt +++ b/shared/src/main/kotlin/io/askimo/core/providers/anthropic/AnthropicModelFactory.kt @@ -140,9 +140,8 @@ class AnthropicModelFactory : ChatModelFactory { .apiKey(safeApiKey(settings.apiKey)) .modelName(settings.defaultModel) .baseUrl(settings.baseUrl) - .thinkingType("enabled") // adaptive, enable, disabled - .thinkingBudgetTokens(1024) - .maxTokens(1025) // must be > thinkingBudgetTokens to properly test + .thinkingType("adaptive") // adaptive, disabled + .maxTokens(1024) .sendThinking(true) .returnThinking(true) .build() @@ -155,7 +154,7 @@ class AnthropicModelFactory : ChatModelFactory { log.info("Model '${settings.defaultModel}' supports thinking — thinking enabled") true } catch (e: Exception) { - log.info("Model '${settings.defaultModel}' does not support thinking: ${e.message} — thinking disabled") + log.info("Model '${settings.defaultModel}' does not support thinking: ${e.message} — thinking disabled", e) false } @@ -189,16 +188,22 @@ class AnthropicModelFactory : ChatModelFactory { if (supportsThinking) { if (reasoningLevel.isEnabled) { thinkingType("adaptive") + // Only set explicit budget if user configured one (0 = let Anthropic decide) + if (settings.thinkingBudgetTokens > 0) { + thinkingBudgetTokens(settings.thinkingBudgetTokens) + } + // Use thinkingMaxTokens if set, otherwise fall back to maxTokens + val effectiveMaxTokens = if (settings.thinkingMaxTokens > 0) settings.thinkingMaxTokens else settings.maxTokens + maxTokens(effectiveMaxTokens) sendThinking(true) returnThinking(true) - .maxTokens(32_000) } else { // OFF — explicitly disable extended thinking thinkingType("disabled") - maxTokens(8192) + maxTokens(settings.maxTokens) } } else { - maxTokens(8192) + maxTokens(settings.maxTokens) } } .build() diff --git a/shared/src/main/kotlin/io/askimo/core/providers/anthropic/AnthropicSettings.kt b/shared/src/main/kotlin/io/askimo/core/providers/anthropic/AnthropicSettings.kt index b5131f5be..e1bd67366 100644 --- a/shared/src/main/kotlin/io/askimo/core/providers/anthropic/AnthropicSettings.kt +++ b/shared/src/main/kotlin/io/askimo/core/providers/anthropic/AnthropicSettings.kt @@ -17,6 +17,9 @@ data class AnthropicSettings( override val visionModel: String = "", override val imageModel: String = "", override val embeddingModel: String = "", + val maxTokens: Int = 16_000, + val thinkingBudgetTokens: Int = 0, + val thinkingMaxTokens: Int = 0, ) : ProviderSettings, HasApiKey { override fun describe(): List = listOf( @@ -42,6 +45,27 @@ data class AnthropicSettings( ), ) + override fun getConfigurableFields(messageResolver: (String) -> String): List = listOf( + SettingField.NumberField( + name = SettingField.MAX_TOKENS, + label = messageResolver("provider.anthropic.max_tokens.label"), + description = messageResolver("provider.anthropic.max_tokens.description"), + value = maxTokens, + ), + SettingField.NumberField( + name = SettingField.THINKING_BUDGET_TOKENS, + label = messageResolver("provider.anthropic.thinking_budget_tokens.label"), + description = messageResolver("provider.anthropic.thinking_budget_tokens.description"), + value = thinkingBudgetTokens, + ), + SettingField.NumberField( + name = SettingField.THINKING_MAX_TOKENS, + label = messageResolver("provider.anthropic.thinking_max_tokens.label"), + description = messageResolver("provider.anthropic.thinking_max_tokens.description"), + value = thinkingMaxTokens, + ), + ) + override fun updateField(fieldName: String, value: String): ProviderSettings = when (fieldName) { SettingField.API_KEY -> copy(apiKey = value) SettingField.BASE_URL -> copy(baseUrl = value) @@ -50,6 +74,9 @@ data class AnthropicSettings( SettingField.VISION_MODEL -> copy(visionModel = value) SettingField.IMAGE_MODEL -> copy(imageModel = value) SettingField.EMBEDDING_MODEL -> copy(embeddingModel = value) + SettingField.MAX_TOKENS -> copy(maxTokens = value.toIntOrNull() ?: maxTokens) + SettingField.THINKING_BUDGET_TOKENS -> copy(thinkingBudgetTokens = value.toIntOrNull() ?: thinkingBudgetTokens) + SettingField.THINKING_MAX_TOKENS -> copy(thinkingMaxTokens = value.toIntOrNull() ?: thinkingMaxTokens) else -> this }